133 lines
4.0 KiB
Go
133 lines
4.0 KiB
Go
// Package controllers holds the HTTP-facing controllers for the /api/v1
|
|
// surface. Each controller groups one resource's routes, exposes a Register
|
|
// method, and depends on exactly one resource-level Manager interface — never
|
|
// directly on stores, lifecycle reducers, or adapters.
|
|
package controllers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/apispec"
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/envelope"
|
|
projectsvc "github.com/aoagents/agent-orchestrator/backend/internal/service/project"
|
|
)
|
|
|
|
// ProjectsController owns the /projects routes. The controller depends only on
|
|
// projectsvc.Manager; nil keeps routes registered but returns OpenAPI-backed 501s.
|
|
type ProjectsController struct {
|
|
Mgr projectsvc.Manager
|
|
}
|
|
|
|
// Register mounts the project routes on the supplied router.
|
|
func (c *ProjectsController) Register(r chi.Router) {
|
|
r.Get("/projects", c.list)
|
|
r.Post("/projects", c.add)
|
|
r.Get("/projects/{id}", c.get)
|
|
r.Put("/projects/{id}/config", c.setConfig)
|
|
r.Delete("/projects/{id}", c.remove)
|
|
}
|
|
|
|
func (c *ProjectsController) list(w http.ResponseWriter, r *http.Request) {
|
|
if c.Mgr == nil {
|
|
apispec.NotImplemented(w, r, "GET", "/api/v1/projects")
|
|
return
|
|
}
|
|
projects, err := c.Mgr.List(r.Context())
|
|
if err != nil {
|
|
envelope.WriteError(w, r, err)
|
|
return
|
|
}
|
|
if projects == nil {
|
|
projects = []projectsvc.Summary{}
|
|
}
|
|
envelope.WriteJSON(w, http.StatusOK, ListProjectsResponse{Projects: projects})
|
|
}
|
|
|
|
func (c *ProjectsController) add(w http.ResponseWriter, r *http.Request) {
|
|
if c.Mgr == nil {
|
|
apispec.NotImplemented(w, r, "POST", "/api/v1/projects")
|
|
return
|
|
}
|
|
var in projectsvc.AddInput
|
|
if err := decodeJSONStrict(r, &in); err != nil {
|
|
envelope.WriteAPIError(w, r, http.StatusBadRequest, "bad_request", "INVALID_JSON", "Invalid JSON body", nil)
|
|
return
|
|
}
|
|
p, err := c.Mgr.Add(r.Context(), in)
|
|
if err != nil {
|
|
envelope.WriteError(w, r, err)
|
|
return
|
|
}
|
|
envelope.WriteJSON(w, http.StatusCreated, ProjectResponse{Project: p})
|
|
}
|
|
|
|
func (c *ProjectsController) get(w http.ResponseWriter, r *http.Request) {
|
|
if c.Mgr == nil {
|
|
apispec.NotImplemented(w, r, "GET", "/api/v1/projects/{id}")
|
|
return
|
|
}
|
|
got, err := c.Mgr.Get(r.Context(), projectID(r))
|
|
if err != nil {
|
|
envelope.WriteError(w, r, err)
|
|
return
|
|
}
|
|
resp, err := newGetProjectResponse(got)
|
|
if err != nil {
|
|
envelope.WriteAPIError(w, r, http.StatusInternalServerError, "internal", "INTERNAL_ERROR", "Internal server error", nil)
|
|
return
|
|
}
|
|
envelope.WriteJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (c *ProjectsController) setConfig(w http.ResponseWriter, r *http.Request) {
|
|
if c.Mgr == nil {
|
|
apispec.NotImplemented(w, r, "PUT", "/api/v1/projects/{id}/config")
|
|
return
|
|
}
|
|
var in projectsvc.SetConfigInput
|
|
if err := decodeJSONStrict(r, &in); err != nil {
|
|
envelope.WriteAPIError(w, r, http.StatusBadRequest, "bad_request", "INVALID_JSON", "Invalid JSON body", nil)
|
|
return
|
|
}
|
|
p, err := c.Mgr.SetConfig(r.Context(), projectID(r), in)
|
|
if err != nil {
|
|
envelope.WriteError(w, r, err)
|
|
return
|
|
}
|
|
envelope.WriteJSON(w, http.StatusOK, ProjectResponse{Project: p})
|
|
}
|
|
|
|
func (c *ProjectsController) remove(w http.ResponseWriter, r *http.Request) {
|
|
if c.Mgr == nil {
|
|
apispec.NotImplemented(w, r, "DELETE", "/api/v1/projects/{id}")
|
|
return
|
|
}
|
|
result, err := c.Mgr.Remove(r.Context(), projectID(r))
|
|
if err != nil {
|
|
envelope.WriteError(w, r, err)
|
|
return
|
|
}
|
|
envelope.WriteJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func projectID(r *http.Request) domain.ProjectID {
|
|
return domain.ProjectID(chi.URLParam(r, "id"))
|
|
}
|
|
|
|
func decodeJSON(r *http.Request, out any) error {
|
|
return json.NewDecoder(r.Body).Decode(out)
|
|
}
|
|
|
|
// decodeJSONStrict rejects request bodies that include keys outside the target
|
|
// type. Used on project add/set-config so a misspelled or removed config field
|
|
// surfaces as a 400 instead of being silently dropped.
|
|
func decodeJSONStrict(r *http.Request, out any) error {
|
|
dec := json.NewDecoder(r.Body)
|
|
dec.DisallowUnknownFields()
|
|
return dec.Decode(out)
|
|
}
|