package httpapi import ( "encoding/json" "errors" "log/slog" "net/http" "github.com/dtoro/oikos/internal/domain" "github.com/dtoro/oikos/internal/httpapi/gen" ) // errNotImplemented marks endpoints stubbed for later phases. var errNotImplemented = errors.New("not implemented yet") // statusFor maps domain sentinel errors to HTTP status codes (plan SG11). func statusFor(err error) (status int, title string) { switch { case errors.Is(err, domain.ErrNotFound): return http.StatusNotFound, "not found" case errors.Is(err, domain.ErrInvalidTransition): return http.StatusConflict, "invalid lifecycle transition" case errors.Is(err, domain.ErrConflict), errors.Is(err, domain.ErrAlreadyExists): return http.StatusConflict, "conflict" case errors.Is(err, domain.ErrCardinality): return http.StatusConflict, "relationship cardinality violation" case errors.Is(err, domain.ErrAbstractType), errors.Is(err, domain.ErrInvalidEdge): return http.StatusUnprocessableEntity, "ontology validation failed" case errors.Is(err, domain.ErrInvalidInput): return http.StatusBadRequest, "invalid input" case errors.Is(err, domain.ErrApprovalRequired): return http.StatusForbidden, "operator approval required" case errors.Is(err, domain.ErrAutonomyBlocked): return http.StatusForbidden, "autonomy policy blocks this action" case errors.Is(err, domain.ErrCircuitOpen): return http.StatusServiceUnavailable, "circuit breaker open" case errors.Is(err, errNotImplemented): return http.StatusNotImplemented, "not implemented" default: return http.StatusInternalServerError, "internal error" } } // writeProblem writes an RFC 9457 problem+json response. func writeProblem(w http.ResponseWriter, r *http.Request, status int, title, detail string) { instance := r.URL.Path p := gen.Problem{ Status: status, Title: title, Instance: &instance, } if detail != "" { p.Detail = &detail } w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(status) json.NewEncoder(w).Encode(p) } // writeProblemFromErr maps an error to a problem+json response. Internal // error details are logged server-side, never leaked to clients. func writeProblemFromErr(w http.ResponseWriter, r *http.Request, err error) { status, title := statusFor(err) detail := "" if status != http.StatusInternalServerError { detail = err.Error() } else { slog.Error("internal error", "method", r.Method, "path", r.URL.Path, "error", err) } writeProblem(w, r, status, title, detail) }