125 lines
6.1 KiB
Go
125 lines
6.1 KiB
Go
package storage
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
"gardomatic.kleiax.de/internal/platform/validate"
|
|
)
|
|
|
|
// SpeciesModelInterface persists global and garden-specific species data.
|
|
type SpeciesModelInterface interface {
|
|
Insert(species Species) (Species, error)
|
|
Get(gardenID, id int) (Species, error)
|
|
GetAllForGarden(gardenID int) ([]Species, error)
|
|
Update(gardenID int, species Species) (Species, error)
|
|
Delete(gardenID, id int) error
|
|
}
|
|
|
|
// Species contains reusable botanical and cultivation master data.
|
|
type Species struct {
|
|
ID int `json:"id"`
|
|
GardenID *int `json:"garden_id,omitempty"`
|
|
CommonName string `json:"common_name"`
|
|
Cultivar string `json:"cultivar"`
|
|
BotanicalName string `json:"botanical_name"`
|
|
CategoryID *int `json:"category_id,omitempty"`
|
|
Category string `json:"category"`
|
|
SunExposure *string `json:"sun_exposure,omitempty"`
|
|
SoilCondition *string `json:"soil_condition,omitempty"`
|
|
SoilReaction *string `json:"soil_reaction,omitempty"`
|
|
WinterProtection *string `json:"winter_protection,omitempty"`
|
|
SpacingCM *int `json:"spacing_cm,omitempty"`
|
|
// HeightCM is retained as a deprecated API alias for HeightCMTo.
|
|
HeightCM *int `json:"height_cm,omitempty"`
|
|
HeightCMFrom *int `json:"height_cm_from,omitempty"`
|
|
HeightCMTo *int `json:"height_cm_to,omitempty"`
|
|
WidthCMFrom *int `json:"width_cm_from,omitempty"`
|
|
WidthCMTo *int `json:"width_cm_to,omitempty"`
|
|
SowMonthFrom *int `json:"sow_month_from,omitempty"`
|
|
SowDayFrom *int `json:"sow_day_from,omitempty"`
|
|
SowMonthTo *int `json:"sow_month_to,omitempty"`
|
|
SowDayTo *int `json:"sow_day_to,omitempty"`
|
|
PlantingMonthFrom *int `json:"planting_month_from,omitempty"`
|
|
PlantingDayFrom *int `json:"planting_day_from,omitempty"`
|
|
PlantingMonthTo *int `json:"planting_month_to,omitempty"`
|
|
PlantingDayTo *int `json:"planting_day_to,omitempty"`
|
|
HarvestMonthFrom *int `json:"harvest_month_from,omitempty"`
|
|
HarvestDayFrom *int `json:"harvest_day_from,omitempty"`
|
|
HarvestMonthTo *int `json:"harvest_month_to,omitempty"`
|
|
HarvestDayTo *int `json:"harvest_day_to,omitempty"`
|
|
Notes string `json:"notes"`
|
|
ImageData string `json:"image_data,omitempty"`
|
|
ImageID *int `json:"image_id,omitempty"`
|
|
Attributes json.RawMessage `json:"attributes"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Version int `json:"version"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
CreatedBy int `json:"created_by"`
|
|
UpdatedBy int `json:"updated_by"`
|
|
}
|
|
|
|
// ValidateSpecies applies persistence-independent species validation rules.
|
|
func ValidateSpecies(v *validate.Validator, species Species) {
|
|
v.Check(strings.TrimSpace(species.CommonName) != "", "common_name", "must be provided")
|
|
v.Check(len(species.CommonName) <= 500, "common_name", "must not be more than 500 bytes long")
|
|
v.Check(len(species.Cultivar) <= 500, "cultivar", "must not be more than 500 bytes long")
|
|
v.Check(len(species.BotanicalName) <= 500, "botanical_name", "must not be more than 500 bytes long")
|
|
if species.CategoryID != nil {
|
|
v.Check(*species.CategoryID > 0, "category_id", "must be a positive integer")
|
|
}
|
|
v.Check(len(species.Notes) <= 10_000, "notes", "must not be more than 10000 bytes long")
|
|
v.Check(len(species.Attributes) == 0 || json.Valid(species.Attributes), "attributes", "must be valid JSON")
|
|
ValidateTags(v, species.Tags)
|
|
validateOptionalEnum(v, "sun_exposure", species.SunExposure, "sunny", "partial_shade", "shade")
|
|
validateOptionalEnum(v, "soil_condition", species.SoilCondition, "dry", "moist", "boggy")
|
|
validateOptionalEnum(v, "soil_reaction", species.SoilReaction, "alkaline", "acidic", "neutral")
|
|
validateOptionalPositive(v, "spacing_cm", species.SpacingCM)
|
|
heightTo := species.HeightCMTo
|
|
if heightTo == nil {
|
|
heightTo = species.HeightCM
|
|
}
|
|
validateOptionalRange(v, "height_cm", species.HeightCMFrom, heightTo)
|
|
validateOptionalRange(v, "width_cm", species.WidthCMFrom, species.WidthCMTo)
|
|
validateOptionalCalendarPart(v, "sow_month_from", species.SowMonthFrom, 12)
|
|
validateOptionalCalendarPart(v, "sow_day_from", species.SowDayFrom, 31)
|
|
validateOptionalCalendarPart(v, "sow_month_to", species.SowMonthTo, 12)
|
|
validateOptionalCalendarPart(v, "sow_day_to", species.SowDayTo, 31)
|
|
validateOptionalCalendarPart(v, "planting_month_from", species.PlantingMonthFrom, 12)
|
|
validateOptionalCalendarPart(v, "planting_day_from", species.PlantingDayFrom, 31)
|
|
validateOptionalCalendarPart(v, "planting_month_to", species.PlantingMonthTo, 12)
|
|
validateOptionalCalendarPart(v, "planting_day_to", species.PlantingDayTo, 31)
|
|
validateOptionalCalendarPart(v, "harvest_month_from", species.HarvestMonthFrom, 12)
|
|
validateOptionalCalendarPart(v, "harvest_day_from", species.HarvestDayFrom, 31)
|
|
validateOptionalCalendarPart(v, "harvest_month_to", species.HarvestMonthTo, 12)
|
|
validateOptionalCalendarPart(v, "harvest_day_to", species.HarvestDayTo, 31)
|
|
}
|
|
|
|
func validateOptionalEnum(v *validate.Validator, field string, value *string, allowed ...string) {
|
|
if value != nil {
|
|
v.Check(validate.PermittedValue(*value, allowed...), field, "is invalid")
|
|
}
|
|
}
|
|
|
|
func validateOptionalPositive(v *validate.Validator, field string, value *int) {
|
|
if value != nil {
|
|
v.Check(*value > 0, field, "must be greater than zero")
|
|
}
|
|
}
|
|
|
|
func validateOptionalRange(v *validate.Validator, field string, from, to *int) {
|
|
validateOptionalPositive(v, field+"_from", from)
|
|
validateOptionalPositive(v, field+"_to", to)
|
|
if from != nil && to != nil {
|
|
v.Check(*from <= *to, field+"_from", "must not be greater than "+field+"_to")
|
|
}
|
|
}
|
|
|
|
func validateOptionalCalendarPart(v *validate.Validator, field string, value *int, maximum int) {
|
|
if value != nil {
|
|
v.Check(*value >= 1 && *value <= maximum, field, "is outside the valid range")
|
|
}
|
|
}
|