130 lines
3.9 KiB
Go
130 lines
3.9 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"
|
|
"errors"
|
|
"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.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 {
|
|
writeProjectError(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 := decodeJSON(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 {
|
|
writeProjectError(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 {
|
|
writeProjectError(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) 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 {
|
|
writeProjectError(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)
|
|
}
|
|
|
|
// writeProjectError maps a projectsvc.Error to its HTTP status, falling back to
|
|
// 500 for an unrecognized kind or a non-projectsvc.Error.
|
|
func writeProjectError(w http.ResponseWriter, r *http.Request, err error) {
|
|
var pe *projectsvc.Error
|
|
if errors.As(err, &pe) {
|
|
status := http.StatusInternalServerError
|
|
switch pe.Kind {
|
|
case "bad_request":
|
|
status = http.StatusBadRequest
|
|
case "not_found":
|
|
status = http.StatusNotFound
|
|
case "conflict":
|
|
status = http.StatusConflict
|
|
case "not_implemented":
|
|
status = http.StatusNotImplemented
|
|
case "internal":
|
|
status = http.StatusInternalServerError
|
|
}
|
|
envelope.WriteAPIError(w, r, status, pe.Kind, pe.Code, pe.Message, pe.Details)
|
|
return
|
|
}
|
|
envelope.WriteAPIError(w, r, http.StatusInternalServerError, "internal", "INTERNAL_ERROR", "Internal server error", nil)
|
|
}
|