api.go

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

Source Files