Files
core/internal/api/registry.go

421 lines
11 KiB
Go

package api
import (
"net/http"
"net/url"
"strings"
"reanimator/internal/domain"
"reanimator/internal/repository/registry"
)
type RegistryDependencies struct {
Customers *registry.CustomerRepository
Projects *registry.ProjectRepository
Assets *registry.AssetRepository
Components *registry.ComponentRepository
Lots *registry.LotRepository
LotMappings *registry.LotModelMappingRepository
}
type registryHandlers struct {
deps RegistryDependencies
}
func RegisterRegistryRoutes(mux *http.ServeMux, deps RegistryDependencies) {
h := registryHandlers{deps: deps}
mux.HandleFunc("/customers", h.handleCustomers)
mux.HandleFunc("/customers/", h.handleCustomerByID)
mux.HandleFunc("/projects", h.handleProjects)
mux.HandleFunc("/projects/", h.handleProjectByID)
mux.HandleFunc("/assets", h.handleAssets)
mux.HandleFunc("/components", h.handleComponents)
mux.HandleFunc("/lots", h.handleLots)
mux.HandleFunc("/lot-mappings", h.handleLotMappings)
mux.HandleFunc("/lot-mappings/", h.handleLotMappingByModel)
}
func (h registryHandlers) handleCustomers(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
items, err := h.deps.Customers.List(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, "list customers 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.Customers.Create(r.Context(), strings.TrimSpace(req.Name))
if err != nil {
switch err {
case registry.ErrConflict:
writeError(w, http.StatusConflict, "customer conflict")
default:
writeError(w, http.StatusInternalServerError, "create customer failed")
}
return
}
writeJSON(w, http.StatusCreated, item)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func (h registryHandlers) handleCustomerByID(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
id, ok := parseID(r.URL.Path, "/customers/")
if !ok {
writeError(w, http.StatusNotFound, "customer not found")
return
}
item, err := h.deps.Customers.Get(r.Context(), id)
if err != nil {
switch err {
case registry.ErrNotFound:
writeError(w, http.StatusNotFound, "customer not found")
default:
writeError(w, http.StatusInternalServerError, "get customer failed")
}
return
}
writeJSON(w, http.StatusOK, item)
}
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 {
CustomerID string `json:"customer_id"`
Name string `json:"name"`
}
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if req.CustomerID == "" {
writeError(w, http.StatusBadRequest, "customer_id is required")
return
}
if strings.TrimSpace(req.Name) == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
item, err := h.deps.Projects.Create(r.Context(), req.CustomerID, 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"`
LocationID *string `json:"location_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 req.ProjectID == "" {
writeError(w, http.StatusBadRequest, "project_id is required")
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
}
asset := domain.Asset{
ProjectID: req.ProjectID,
LocationID: req.LocationID,
Name: strings.TrimSpace(req.Name),
Vendor: req.Vendor,
Model: req.Model,
VendorSerial: strings.TrimSpace(req.VendorSerial),
MachineTag: 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) 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 {
LotID *string `json:"lot_id"`
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{
LotID: req.LotID,
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 (h registryHandlers) handleLots(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if h.deps.Lots == nil {
writeError(w, http.StatusInternalServerError, "lots unavailable")
return
}
items, err := h.deps.Lots.List(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, "list lots failed")
return
}
writeJSON(w, http.StatusOK, items)
}
func (h registryHandlers) handleLotMappings(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if h.deps.LotMappings == nil {
writeError(w, http.StatusInternalServerError, "lot mappings unavailable")
return
}
items, err := h.deps.LotMappings.List(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, "list lot mappings failed")
return
}
writeJSON(w, http.StatusOK, items)
}
func (h registryHandlers) handleLotMappingByModel(w http.ResponseWriter, r *http.Request) {
if h.deps.Lots == nil || h.deps.LotMappings == nil {
writeError(w, http.StatusInternalServerError, "lot mapping dependencies unavailable")
return
}
model, ok := parseModelPathParam(r.URL.Path, "/lot-mappings/")
if !ok {
writeError(w, http.StatusBadRequest, "model is required")
return
}
switch r.Method {
case http.MethodPut:
var req struct {
LotCode string `json:"lot_code"`
}
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
lotCode := normalizeMappingValue(req.LotCode)
if lotCode == "" {
writeError(w, http.StatusBadRequest, "lot_code is required")
return
}
lot, err := h.deps.Lots.EnsureByCode(r.Context(), lotCode)
if err != nil {
if err == registry.ErrConflict {
writeError(w, http.StatusConflict, "lot conflict")
return
}
writeError(w, http.StatusInternalServerError, "ensure lot failed")
return
}
item, created, affectedParts, err := h.deps.LotMappings.UpsertByModelWithBackfill(r.Context(), model, lot.ID)
if err != nil {
if err == registry.ErrConflict {
writeError(w, http.StatusConflict, "lot mapping conflict")
return
}
writeError(w, http.StatusInternalServerError, "upsert lot mapping failed")
return
}
status := http.StatusOK
if created {
status = http.StatusCreated
}
writeJSON(w, status, map[string]any{
"mapping": item,
"affected_parts_count": affectedParts,
})
case http.MethodDelete:
affectedParts, err := h.deps.LotMappings.DeleteByModelWithReset(r.Context(), model)
if err != nil {
switch err {
case registry.ErrNotFound:
writeError(w, http.StatusNotFound, "lot mapping not found")
default:
writeError(w, http.StatusInternalServerError, "delete lot mapping failed")
}
return
}
writeJSON(w, http.StatusOK, map[string]any{
"deleted": true,
"affected_parts_count": affectedParts,
})
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func parseModelPathParam(path, prefix string) (string, bool) {
if !strings.HasPrefix(path, prefix) {
return "", false
}
raw := strings.TrimPrefix(path, prefix)
if raw == "" || strings.Contains(raw, "/") {
return "", false
}
decoded, err := url.PathUnescape(raw)
if err != nil {
return "", false
}
model := normalizeMappingValue(decoded)
if model == "" {
return "", false
}
return model, true
}
func normalizeMappingValue(value string) string {
trimmed := strings.TrimSpace(value)
if len(trimmed) >= 2 {
if (strings.HasPrefix(trimmed, "\"") && strings.HasSuffix(trimmed, "\"")) ||
(strings.HasPrefix(trimmed, "'") && strings.HasSuffix(trimmed, "'")) {
trimmed = strings.TrimSpace(trimmed[1 : len(trimmed)-1])
}
}
return trimmed
}