agent-orchestrator/backend/internal/httpd/controllers/projects.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)
}