api.go

v1.1.0
Doc Versions Source
1
package admin
2
3
import (
4
	"database/sql"
5
	"encoding/json"
6
	"errors"
7
	"fmt"
8
	"log"
9
	"net/http"
10
	"strconv"
11
	"strings"
12
13
	"example.com/curator/internal/store"
14
)
15
16
// APIHandler serves the REST API for module management.
17
type APIHandler struct {
18
	store *store.Store
19
}
20
21
// NewAPI creates a new API handler.
22
func NewAPI(st *store.Store) *APIHandler {
23
	return &APIHandler{store: st}
24
}
25
26
// ServeHTTP routes API requests.
27
func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
28
	path := strings.TrimPrefix(r.URL.Path, "/-/api")
29
30
	switch {
31
	case path == "/modules" && r.Method == http.MethodGet:
32
		h.listModules(w, r)
33
	case path == "/modules" && r.Method == http.MethodPost:
34
		h.addModule(w, r)
35
	case strings.HasPrefix(path, "/modules/") && r.Method == http.MethodDelete:
36
		name := strings.TrimPrefix(path, "/modules/")
37
		h.deleteModule(w, r, name)
38
	case path == "/patterns" && r.Method == http.MethodGet:
39
		h.listPatterns(w, r)
40
	case path == "/patterns" && r.Method == http.MethodPost:
41
		h.addPattern(w, r)
42
	case strings.HasPrefix(path, "/patterns/") && r.Method == http.MethodDelete:
43
		idStr := strings.TrimPrefix(path, "/patterns/")
44
		h.deletePattern(w, r, idStr)
45
	default:
46
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
47
	}
48
}
49
50
func (h *APIHandler) listModules(w http.ResponseWriter, r *http.Request) {
51
	modules, err := h.store.ListModules()
52
	if err != nil {
53
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
54
		return
55
	}
56
	if modules == nil {
57
		modules = []store.ModuleRow{}
58
	}
59
	writeJSON(w, http.StatusOK, modules)
60
}
61
62
func (h *APIHandler) addModule(w http.ResponseWriter, r *http.Request) {
63
	var m store.ModuleRow
64
	if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
65
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
66
		return
67
	}
68
69
	if m.Name == "" || m.Repo == "" {
70
		writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "name and repo are required"})
71
		return
72
	}
73
74
	if m.VCS == "" {
75
		m.VCS = "git"
76
	}
77
78
	// Verify the repository.
79
	if err := store.VerifyModule(m.Repo); err != nil {
80
		log.Printf("api: verify module %s: %v", m.Name, err)
81
		writeJSON(w, http.StatusUnprocessableEntity, map[string]string{
82
			"error": fmt.Sprintf("repository verification failed: %v", err),
83
		})
84
		return
85
	}
86
87
	if err := h.store.AddModule(m); err != nil {
88
		code := http.StatusInternalServerError
89
		if strings.Contains(err.Error(), "UNIQUE constraint") {
90
			code = http.StatusConflict
91
		}
92
		writeJSON(w, code, map[string]string{"error": err.Error()})
93
		return
94
	}
95
96
	writeJSON(w, http.StatusCreated, m)
97
}
98
99
func (h *APIHandler) deleteModule(w http.ResponseWriter, r *http.Request, name string) {
100
	if name == "" {
101
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
102
		return
103
	}
104
105
	if err := h.store.DeleteModule(name); err != nil {
106
		if errors.Is(err, sql.ErrNoRows) {
107
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
108
			return
109
		}
110
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
111
		return
112
	}
113
114
	w.WriteHeader(http.StatusNoContent)
115
}
116
117
func (h *APIHandler) listPatterns(w http.ResponseWriter, r *http.Request) {
118
	patterns, err := h.store.ListPatterns()
119
	if err != nil {
120
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
121
		return
122
	}
123
	if patterns == nil {
124
		patterns = []store.PatternRow{}
125
	}
126
	writeJSON(w, http.StatusOK, patterns)
127
}
128
129
func (h *APIHandler) addPattern(w http.ResponseWriter, r *http.Request) {
130
	var p store.PatternRow
131
	if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
132
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
133
		return
134
	}
135
136
	if p.Pattern == "" || p.Repo == "" {
137
		writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "pattern and repo are required"})
138
		return
139
	}
140
141
	if p.VCS == "" {
142
		p.VCS = "git"
143
	}
144
145
	if err := store.VerifyPattern(p.Pattern); err != nil {
146
		writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()})
147
		return
148
	}
149
150
	if err := h.store.AddPattern(p); err != nil {
151
		code := http.StatusInternalServerError
152
		if strings.Contains(err.Error(), "UNIQUE constraint") {
153
			code = http.StatusConflict
154
		}
155
		writeJSON(w, code, map[string]string{"error": err.Error()})
156
		return
157
	}
158
159
	writeJSON(w, http.StatusCreated, p)
160
}
161
162
func (h *APIHandler) deletePattern(w http.ResponseWriter, r *http.Request, idStr string) {
163
	id, err := strconv.ParseInt(idStr, 10, 64)
164
	if err != nil {
165
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid ID"})
166
		return
167
	}
168
169
	if err := h.store.DeletePattern(id); err != nil {
170
		if errors.Is(err, sql.ErrNoRows) {
171
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
172
			return
173
		}
174
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
175
		return
176
	}
177
178
	w.WriteHeader(http.StatusNoContent)
179
}
180
181
func writeJSON(w http.ResponseWriter, code int, v any) {
182
	w.Header().Set("Content-Type", "application/json")
183
	w.WriteHeader(code)
184
	json.NewEncoder(w).Encode(v)
185
}
186

Source Files