package api import ( "errors" "net/http" "github.com/gin-gonic/gin" "gitea.stevedudenhoeffer.com/steve/pansy/internal/domain" "gitea.stevedudenhoeffer.com/steve/pansy/internal/service" ) // gardenFields is the shared mutable field set for the gardens endpoints. // Dimensions are centimeters (the API is metric-only; imperial is a display // concern). On create, omitted dimensions default server-side. type gardenFields struct { Name string `json:"name" binding:"required"` WidthCM float64 `json:"widthCm"` HeightCM float64 `json:"heightCm"` UnitPref string `json:"unitPref"` Notes string `json:"notes"` } func (f gardenFields) toInput() service.GardenInput { return service.GardenInput{Name: f.Name, WidthCM: f.WidthCM, HeightCM: f.HeightCM, UnitPref: f.UnitPref, Notes: f.Notes} } // gardenCreateRequest / gardenUpdateRequest are the JSON bodies. Update adds a // required current version (>= 1) for the optimistic-concurrency guard. type gardenCreateRequest struct { gardenFields } type gardenUpdateRequest struct { gardenFields Version int64 `json:"version" binding:"required,min=1"` } // listGardens returns the actor's gardens as a JSON array (always an array, // never null). func (h *handlers) listGardens(c *gin.Context) { gardens, err := h.svc.ListGardens(c.Request.Context(), mustActor(c).ID) if err != nil { writeServiceError(c, err) return } c.JSON(http.StatusOK, gardens) } func (h *handlers) createGarden(c *gin.Context) { var req gardenCreateRequest if err := c.ShouldBindJSON(&req); err != nil { writeAPIError(c, http.StatusBadRequest, "INVALID_INPUT", "a garden name is required") return } g, err := h.svc.CreateGarden(c.Request.Context(), mustActor(c).ID, req.toInput()) if err != nil { writeServiceError(c, err) return } c.JSON(http.StatusCreated, g) } func (h *handlers) getGarden(c *gin.Context) { id, ok := parseIDParam(c, "id") if !ok { return } g, err := h.svc.GetGarden(c.Request.Context(), mustActor(c).ID, id) if err != nil { writeServiceError(c, err) return } c.JSON(http.StatusOK, g) } func (h *handlers) updateGarden(c *gin.Context) { id, ok := parseIDParam(c, "id") if !ok { return } var req gardenUpdateRequest if err := c.ShouldBindJSON(&req); err != nil { writeAPIError(c, http.StatusBadRequest, "INVALID_INPUT", "name and a current version are required") return } g, err := h.svc.UpdateGarden(c.Request.Context(), mustActor(c).ID, id, req.toInput(), req.Version) if err != nil { // On a version conflict the service returns the current row so the client // can rebase; everything else is a plain error. if errors.Is(err, domain.ErrVersionConflict) { writeVersionConflict(c, g) return } writeServiceError(c, err) return } c.JSON(http.StatusOK, g) } func (h *handlers) deleteGarden(c *gin.Context) { id, ok := parseIDParam(c, "id") if !ok { return } if err := h.svc.DeleteGarden(c.Request.Context(), mustActor(c).ID, id); err != nil { writeServiceError(c, err) return } c.Status(http.StatusNoContent) }