Initial commit
CI / test (push) Canceled after 0s

This commit is contained in:
2026-09-12 22:22:17 +02:00
commit 904d14b64c
314 changed files with 31884 additions and 0 deletions
+195
View File
@@ -0,0 +1,195 @@
package api
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"gardomatic.kleiax.de/internal/platform/validate"
"gardomatic.kleiax.de/internal/storage"
"github.com/julienschmidt/httprouter"
)
type plantLocationInput struct {
LocationID *int `json:"location_id"`
Quantity *int `json:"quantity"`
PlantedAt *time.Time `json:"planted_at"`
ClearPlantedAt bool `json:"clear_planted_at"`
RemovedAt *time.Time `json:"removed_at"`
Notes *string `json:"notes"`
}
func (input plantLocationInput) apply(assignment *storage.PlantLocation) {
if input.LocationID != nil {
assignment.LocationID = *input.LocationID
}
if input.Quantity != nil {
assignment.Quantity = *input.Quantity
}
if input.PlantedAt != nil {
assignment.PlantedAt = input.PlantedAt
}
if input.ClearPlantedAt {
assignment.PlantedAt = nil
}
if input.RemovedAt != nil {
assignment.RemovedAt = input.RemovedAt
}
if input.Notes != nil {
assignment.Notes = strings.TrimSpace(*input.Notes)
}
}
func (app *application) createPlantLocationHandler(w http.ResponseWriter, r *http.Request) {
gardenID, _ := app.readGardenIDParam(r)
plantID, err := app.readIDParam(r)
if err != nil {
app.notFoundResponse(w, r)
return
}
if _, err := app.models.Plants.Get(gardenID, plantID); err != nil {
app.respondToEntityModelError(w, r, err)
return
}
var input plantLocationInput
if err := app.readJSON(w, r, &input); err != nil {
app.badRequestResponse(w, r, err)
return
}
assignment := storage.PlantLocation{PlantID: plantID, Quantity: 1}
input.apply(&assignment)
if !app.validatePlantLocationForGarden(w, r, gardenID, assignment) {
return
}
assignment, err = app.models.PlantLocations.Insert(gardenID, assignment)
if err != nil {
app.respondToEntityModelError(w, r, err)
return
}
headers := make(http.Header)
headers.Set("Location", fmt.Sprintf("/v1/gardens/%d/plants/%d/locations/%d", gardenID, plantID, assignment.ID))
if err := app.writeJSON(w, http.StatusCreated, envelope{"plant_location": assignment}, headers); err != nil {
app.serverErrorResponse(w, r, err)
}
}
func (app *application) listPlantLocationsHandler(w http.ResponseWriter, r *http.Request) {
gardenID, _ := app.readGardenIDParam(r)
plantID, err := app.readIDParam(r)
if err != nil {
app.notFoundResponse(w, r)
return
}
if _, err := app.models.Plants.Get(gardenID, plantID); err != nil {
app.respondToEntityModelError(w, r, err)
return
}
assignments, err := app.models.PlantLocations.GetAllForPlant(gardenID, plantID)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
if err := app.writeJSON(w, http.StatusOK, envelope{"plant_locations": assignments}, nil); err != nil {
app.serverErrorResponse(w, r, err)
}
}
func (app *application) updatePlantLocationHandler(w http.ResponseWriter, r *http.Request) {
gardenID, _ := app.readGardenIDParam(r)
plantID, assignmentID, ok := app.readPlantLocationIDs(w, r)
if !ok {
return
}
assignment, err := app.models.PlantLocations.Get(gardenID, assignmentID)
if err != nil || assignment.PlantID != plantID {
if err == nil {
err = storage.ErrRecordNotFound
}
app.respondToEntityModelError(w, r, err)
return
}
var input plantLocationInput
if err := app.readJSON(w, r, &input); err != nil {
app.badRequestResponse(w, r, err)
return
}
input.apply(&assignment)
if !app.validatePlantLocationForGarden(w, r, gardenID, assignment) {
return
}
assignment, err = app.models.PlantLocations.Update(gardenID, assignment)
if err != nil {
app.respondToEntityModelError(w, r, err)
return
}
if err := app.writeJSON(w, http.StatusOK, envelope{"plant_location": assignment}, nil); err != nil {
app.serverErrorResponse(w, r, err)
}
}
func (app *application) deletePlantLocationHandler(w http.ResponseWriter, r *http.Request) {
gardenID, _ := app.readGardenIDParam(r)
plantID, assignmentID, ok := app.readPlantLocationIDs(w, r)
if !ok {
return
}
assignment, err := app.models.PlantLocations.Get(gardenID, assignmentID)
if err != nil || assignment.PlantID != plantID {
if err == nil {
err = storage.ErrRecordNotFound
}
app.respondToEntityModelError(w, r, err)
return
}
if err := app.models.PlantLocations.Delete(gardenID, assignmentID); err != nil {
app.respondToEntityModelError(w, r, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (app *application) readPlantLocationIDs(w http.ResponseWriter, r *http.Request) (int, int, bool) {
plantID, err := app.readIDParam(r)
if err != nil {
app.notFoundResponse(w, r)
return 0, 0, false
}
id, err := app.readPathInt(r, "assignmentID")
if err != nil {
app.notFoundResponse(w, r)
return 0, 0, false
}
return plantID, id, true
}
func (app *application) readPathInt(r *http.Request, name string) (int, error) {
value := httprouter.ParamsFromContext(r.Context()).ByName(name)
id, err := strconv.Atoi(value)
if err != nil || id < 1 {
return 0, errors.New("invalid path parameter")
}
return id, nil
}
func (app *application) validatePlantLocationForGarden(w http.ResponseWriter, r *http.Request, gardenID int, assignment storage.PlantLocation) bool {
v := validate.New()
storage.ValidatePlantLocation(v, assignment)
if assignment.LocationID > 0 {
if _, err := app.models.Locations.Get(gardenID, assignment.LocationID); err != nil {
if errors.Is(err, storage.ErrRecordNotFound) {
v.AddError("location_id", "must refer to a location in this garden")
} else {
app.serverErrorResponse(w, r, err)
return false
}
}
}
if !v.Valid() {
app.failedValidationResponse(w, r, v.Errors)
return false
}
return true
}