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 }