Artifact 16f3257588a49eb87b0b44a705a6851c76ad7489ed16b36c9a04452b0ea050ce:
- File
internal/api/router.go
— part of check-in
[bf969e9d1a]
at
2026-08-15 02:10:44
on branch trunk
— phase 0 exit: serve, HTML page via op, harness loop, provenance gate
API router + web page as registry consumers; disposability loop over all surfaces; third_party ledger enforced by arch test. (user: dmestas, size: 3162) [annotate] [blame] [check-ins using]
// router.go — the API consumer of the registry (0001 §9). Every registered // op is mounted on a stdlib ServeMux; input crosses through the shared // codecs and validator, output leaves as JSON. The generated OpenAPI // document is served from the same registry at /api/v1/openapi.json. package api import ( "encoding/json" "fmt" "mime" "net/http" "net/url" "reflect" "strings" "craftdesign.group/quarry/internal/ops" ) // Router mounts every op in r plus the OpenAPI document. version flows // into the spec's info block. func Router(r *ops.Registry, version string) (http.Handler, error) { mux := http.NewServeMux() for _, d := range r.All() { mux.Handle(d.Method+" "+d.Pattern, opHandler(r, d)) } spec, err := ops.Spec(r, "quarry", version) if err != nil { return nil, err } specJSON, err := json.MarshalIndent(spec, "", " ") if err != nil { return nil, err } mux.HandleFunc("GET /api/v1/openapi.json", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write(specJSON) }) return mux, nil } func opHandler(r *ops.Registry, d *ops.Descriptor) http.Handler { wild := ops.Wildcards(d.Pattern) return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { in, err := decodeIn(req, d, wild) if err != nil { writeError(w, http.StatusBadRequest, err) return } if err := ops.Validate(in); err != nil { writeError(w, http.StatusBadRequest, err) return } out, err := r.Invoke(req.Context(), d.Name, in) if err != nil { writeError(w, http.StatusInternalServerError, err) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(out) }) } // decodeIn assembles the op's In struct from the request: query string // (GET) or body (POST, form or JSON by content type), with path wildcards // merged last so the routed path is always authoritative. func decodeIn(req *http.Request, d *ops.Descriptor, wild []string) (any, error) { inPtr := d.NewIn() switch { case d.Method == http.MethodGet: if err := ops.DecodeValues(req.URL.Query(), inPtr); err != nil { return nil, err } case isJSON(req): if err := json.NewDecoder(req.Body).Decode(inPtr); err != nil { return nil, fmt.Errorf("body: %w", err) } default: if err := req.ParseForm(); err != nil { return nil, fmt.Errorf("form: %w", err) } if err := ops.DecodeValues(req.PostForm, inPtr); err != nil { return nil, err } } if len(wild) > 0 { pathVals := url.Values{} for _, name := range wild { pathVals.Set(name, req.PathValue(name)) } if err := ops.DecodeValues(pathVals, inPtr); err != nil { return nil, err } } return deref(inPtr), nil } func deref(p any) any { return reflect.ValueOf(p).Elem().Interface() } func isJSON(req *http.Request) bool { ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")) return strings.HasSuffix(ct, "/json") || strings.HasSuffix(ct, "+json") } func writeError(w http.ResponseWriter, code int, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) }