agent-orchestrator/backend/internal/httpd/api.go

85 lines
2.9 KiB
Go

package httpd
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/aoagents/agent-orchestrator/backend/internal/config"
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/apispec"
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/controllers"
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/envelope"
projectsvc "github.com/aoagents/agent-orchestrator/backend/internal/service/project"
)
// APIDeps bundles every Manager the API layer's controllers depend on.
// Controllers see only resource-level interfaces; they do not reach through to
// lifecycle reducers, adapters, or storage. A nil dependency keeps its routes
// registered but returns the OpenAPI-backed 501 response.
type APIDeps struct {
Projects projectsvc.Manager
Sessions controllers.SessionService
}
// API owns one controller per resource and is the single Register call the
// router invokes to mount the /api/v1 surface.
type API struct {
cfg config.Config
projects *controllers.ProjectsController
sessions *controllers.SessionsController
}
// NewAPI constructs the API surface from its dependencies. cfg carries the
// per-request timeout so the REST group can apply it without re-reading the
// environment.
func NewAPI(cfg config.Config, deps APIDeps) *API {
return &API{
cfg: cfg,
projects: &controllers.ProjectsController{
Mgr: deps.Projects,
},
sessions: &controllers.SessionsController{
Svc: deps.Sessions,
},
}
}
// Register mounts the bounded /api/v1 REST surface. Long-lived surfaces such
// as muxed terminal streams stay outside this timeout group.
func (a *API) Register(root chi.Router) {
timeout := a.cfg.RequestTimeout
if timeout <= 0 {
timeout = config.DefaultRequestTimeout
}
root.Route("/api/v1", func(r chi.Router) {
// Serve the OpenAPI document from the same origin as the routes it describes.
r.Get("/openapi.yaml", apispec.ServeYAML)
r.Group(func(r chi.Router) {
r.Use(middleware.Timeout(timeout))
a.projects.Register(r)
a.sessions.Register(r)
// Sibling REST controllers plug in here.
})
// Surfaces that intentionally bypass the REST timeout register at this level.
})
}
// notFoundJSON returns the locked envelope for unmatched routes. Chi's default
// 404 is a text/plain body; the API surface must answer JSON so consumers can
// parse it uniformly.
func notFoundJSON(w http.ResponseWriter, r *http.Request) {
envelope.WriteAPIError(w, r, http.StatusNotFound, "not_found", "ROUTE_NOT_FOUND",
r.Method+" "+r.URL.Path+" has no handler", nil)
}
// methodNotAllowedJSON returns the locked envelope when a method probes a
// known path without a matching verb (e.g. PUT /projects/{id} after we drop
// the legacy PUT alias).
func methodNotAllowedJSON(w http.ResponseWriter, r *http.Request) {
envelope.WriteAPIError(w, r, http.StatusMethodNotAllowed, "method_not_allowed", "METHOD_NOT_ALLOWED",
r.Method+" not allowed on "+r.URL.Path, nil)
}