package api import ( "net/http" "strings" "reanimator/internal/domain" "reanimator/internal/repository/registry" ) type RegistryDependencies struct { Projects *registry.ProjectRepository Assets *registry.AssetRepository Components *registry.ComponentRepository } type registryHandlers struct { deps RegistryDependencies } func RegisterRegistryRoutes(mux *http.ServeMux, deps RegistryDependencies) { h := registryHandlers{deps: deps} mux.HandleFunc("/projects", h.handleProjects) mux.HandleFunc("/projects/", h.handleProjectByID) mux.HandleFunc("/assets", h.handleAssets) mux.HandleFunc("/registry/assets/", h.handleAssetByID) mux.HandleFunc("/components", h.handleComponents) } func (h registryHandlers) handleProjects(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: items, err := h.deps.Projects.List(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "list projects failed") return } writeJSON(w, http.StatusOK, items) case http.MethodPost: var req struct { Name string `json:"name"` } if err := decodeJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if strings.TrimSpace(req.Name) == "" { writeError(w, http.StatusBadRequest, "name is required") return } item, err := h.deps.Projects.Create(r.Context(), strings.TrimSpace(req.Name)) if err != nil { switch err { case registry.ErrConflict: writeError(w, http.StatusConflict, "project conflict") default: writeError(w, http.StatusInternalServerError, "create project failed") } return } writeJSON(w, http.StatusCreated, item) default: w.WriteHeader(http.StatusMethodNotAllowed) } } func (h registryHandlers) handleProjectByID(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } id, ok := parseID(r.URL.Path, "/projects/") if !ok { writeError(w, http.StatusNotFound, "project not found") return } item, err := h.deps.Projects.Get(r.Context(), id) if err != nil { switch err { case registry.ErrNotFound: writeError(w, http.StatusNotFound, "project not found") default: writeError(w, http.StatusInternalServerError, "get project failed") } return } writeJSON(w, http.StatusOK, item) } func (h registryHandlers) handleAssets(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: items, err := h.deps.Assets.List(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "list assets failed") return } writeJSON(w, http.StatusOK, items) case http.MethodPost: var req struct { ProjectID *string `json:"project_id"` Name string `json:"name"` Vendor *string `json:"vendor"` Model *string `json:"model"` VendorSerial string `json:"vendor_serial"` AssetTag *string `json:"asset_tag"` } if err := decodeJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if strings.TrimSpace(req.Name) == "" { writeError(w, http.StatusBadRequest, "name is required") return } if strings.TrimSpace(req.VendorSerial) == "" { writeError(w, http.StatusBadRequest, "vendor_serial is required") return } projectID := "" if req.ProjectID != nil { projectID = strings.TrimSpace(*req.ProjectID) } asset := domain.Asset{ ProjectID: projectID, Name: strings.TrimSpace(req.Name), Vendor: normalizeOptionalString(req.Vendor), Model: normalizeOptionalString(req.Model), VendorSerial: strings.TrimSpace(req.VendorSerial), MachineTag: normalizeOptionalString(req.AssetTag), } item, err := h.deps.Assets.Create(r.Context(), asset) if err != nil { switch err { case registry.ErrConflict: writeError(w, http.StatusConflict, "asset conflict") default: writeError(w, http.StatusInternalServerError, "create asset failed") } return } writeJSON(w, http.StatusCreated, item) default: w.WriteHeader(http.StatusMethodNotAllowed) } } func (h registryHandlers) handleAssetByID(w http.ResponseWriter, r *http.Request) { id, ok := parseID(r.URL.Path, "/registry/assets/") if !ok { writeError(w, http.StatusNotFound, "asset not found") return } switch r.Method { case http.MethodGet: item, err := h.deps.Assets.Get(r.Context(), id) if err != nil { switch err { case registry.ErrNotFound: writeError(w, http.StatusNotFound, "asset not found") default: writeError(w, http.StatusInternalServerError, "get asset failed") } return } writeJSON(w, http.StatusOK, item) case http.MethodDelete: result, err := h.deps.Assets.DeleteWithDetails(r.Context(), id) if err != nil { switch err { case registry.ErrNotFound: writeError(w, http.StatusNotFound, "asset not found") default: writeError(w, http.StatusInternalServerError, "delete asset failed") } return } writeJSON(w, http.StatusOK, map[string]any{ "deleted": true, "deleted_parts": result.DeletedParts, }) default: w.WriteHeader(http.StatusMethodNotAllowed) } } func (h registryHandlers) handleComponents(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: items, err := h.deps.Components.List(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "list components failed") return } writeJSON(w, http.StatusOK, items) case http.MethodPost: var req struct { Vendor *string `json:"vendor"` Model *string `json:"model"` VendorSerial string `json:"vendor_serial"` } if err := decodeJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if strings.TrimSpace(req.VendorSerial) == "" { writeError(w, http.StatusBadRequest, "vendor_serial is required") return } component := domain.Component{ Vendor: req.Vendor, Model: req.Model, VendorSerial: strings.TrimSpace(req.VendorSerial), } item, err := h.deps.Components.Create(r.Context(), component) if err != nil { switch err { case registry.ErrConflict: writeError(w, http.StatusConflict, "component conflict") default: writeError(w, http.StatusInternalServerError, "create component failed") } return } writeJSON(w, http.StatusCreated, item) default: w.WriteHeader(http.StatusMethodNotAllowed) } } func normalizeOptionalString(value *string) *string { if value == nil { return nil } trimmed := strings.TrimSpace(*value) if trimmed == "" { return nil } return &trimmed }