package control import ( "errors" "net/http" "github.com/tegwick/fluid-core/internal/contract" "github.com/tegwick/fluid-core/internal/intent" "github.com/tegwick/fluid-core/internal/policy" ) // IntentAPI implements ArchitectureBlueprint.md section 44.5: read active // intent, read historical intent, validate a candidate against intent, propose // an amendment. // // Amendment is deliberately a proposal and not an edit. Blueprint section 22 is // blunt about it: the Daimon must not silently expand its own mission, and // intent changes require a separate governance process. This API records that // an amendment was proposed; it never enacts one. type IntentAPI struct { store *intent.Store gate *policy.Gate } // NewIntentAPI returns the intent API. func NewIntentAPI(store *intent.Store, gate *policy.Gate) *IntentAPI { return &IntentAPI{store: store, gate: gate} } // IntentResponse is a recorded intent version. type IntentResponse struct { Version string `json:"version"` Digest contract.Digest `json:"digest"` Mode string `json:"mode"` Document string `json:"document"` } func (a *IntentAPI) handleCollection(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: a.record(w, r) default: writeError(w, http.StatusMethodNotAllowed, "method not allowed") } } func (a *IntentAPI) handleActive(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method not allowed") return } v, err := a.store.Active(r.Context()) if err != nil { if errors.Is(err, intent.ErrNoActive) { writeError(w, http.StatusNotFound, "no active interface evolution intent") return } writeError(w, http.StatusInternalServerError, "could not read active intent") return } writeJSON(w, http.StatusOK, toResponse(v)) } func (a *IntentAPI) handleItem(w http.ResponseWriter, r *http.Request) { version := pathTail(r.URL.Path, "/control/v1/intents") if version == "" || version == "active" { a.handleActive(w, r) return } switch r.Method { case http.MethodGet: v, err := a.store.Get(r.Context(), version) if err != nil { writeError(w, statusForStoreError(err), "intent version not found") return } writeJSON(w, http.StatusOK, toResponse(v)) default: writeError(w, http.StatusMethodNotAllowed, "method not allowed") } } // RecordIntentRequest records a new intent version. type RecordIntentRequest struct { Version string `json:"version"` Document string `json:"document"` Activate bool `json:"activate,omitempty"` } func (a *IntentAPI) record(w http.ResponseWriter, r *http.Request) { var req RecordIntentRequest if err := decodeBody(r, &req); err != nil { writeError(w, http.StatusBadRequest, "could not decode request", err.Error()) return } v, err := a.store.Put(r.Context(), req.Version, req.Document) if err != nil { if errors.Is(err, intent.ErrImmutable) { // Rewriting a recorded version would change what already-published // revisions were governed by, so it is a conflict, not a bad request. writeError(w, http.StatusConflict, err.Error()) return } writeError(w, http.StatusBadRequest, err.Error()) return } if req.Activate { if err := a.store.SetActive(r.Context(), v.Version); err != nil { writeError(w, http.StatusInternalServerError, "recorded but not activated", err.Error()) return } } writeJSON(w, http.StatusCreated, toResponse(v)) } func toResponse(v intent.Version) IntentResponse { return IntentResponse{ Version: v.Version, Digest: v.Digest, Mode: v.Mode.String(), Document: v.Document, } }