Entity Validators
Overview
Entity Validators are responsible for validating domain entities at the business logic level, ensuring data integrity and enforcing business rules that go beyond simple form validation. They operate on entity models and provide comprehensive validation for complex business scenarios.
Purpose
Entity Validators serve several key functions: - Business Rule Enforcement - Apply domain-specific validation rules - Data Integrity - Ensure entity data consistency - Cross-Entity Validation - Validate relationships between entities - State Validation - Verify entity state transitions are valid - Constraint Enforcement - Apply database and domain constraints
Structure
Each entity validator typically follows this pattern:
type PlantEntityValidator struct {
Validator *validator.Validator
PlantFetcher *fetchers.PlantFetcher
GardenFetcher *fetchers.GardenFetcher
}
func (this *PlantEntityValidator) ValidateForCreate(ctx context.Context, entity *mdl.Plant) error {
bag := this.Validator.MakeEntityBag()
this.validateRequiredFields(entity, bag)
this.validateBusinessRules(ctx, entity, bag)
this.validateConstraints(ctx, entity, bag)
if !this.Validator.IsValid(bag) {
return this.Validator.NewEntityValidationError(bag)
}
return nil
}
Validation Types
Required Field Validation
func (this *PlantEntityValidator) validateRequiredFields(entity *mdl.Plant, bag *validator.EntityBag) {
if strings.TrimSpace(entity.Name) == "" {
this.Validator.AddEntityError(bag, "name", "Plant name is required")
}
if strings.TrimSpace(entity.Species) == "" {
this.Validator.AddEntityError(bag, "species", "Plant species is required")
}
if entity.GardenId == "" {
this.Validator.AddEntityError(bag, "garden_id", "Garden assignment is required")
}
}
Business Rule Validation
func (this *PlantEntityValidator) validateBusinessRules(ctx context.Context, entity *mdl.Plant, bag *validator.EntityBag) {
// Perennial plants must be of minimum size
if entity.Perennial && entity.Size < 20 {
this.Validator.AddEntityError(bag, "size", "Perennial plants must be at least 20cm")
}
// Harvest dates must be logical
if !entity.PlantedAt.IsZero() && !entity.HarvestedAt.IsZero() {
if entity.HarvestedAt.Before(entity.PlantedAt) {
this.Validator.AddEntityError(bag, "harvested_at", "Harvest date cannot be before planting date")
}
}
// Edible plants require additional data
if entity.Edible && entity.DaysToHarvest == 0 {
this.Validator.AddEntityError(bag, "days_to_harvest", "Edible plants must specify days to harvest")
}
}
Relationship Validation
func (this *PlantEntityValidator) validateConstraints(ctx context.Context, entity *mdl.Plant, bag *validator.EntityBag) {
// Validate garden exists and is active
if entity.GardenId != "" {
garden, exists, err := this.GardenFetcher.FindOneById(ctx, entity.GardenId, nil)
if err != nil {
this.Validator.AddEntityError(bag, "garden_id", "Unable to validate garden")
return
}
if !exists {
this.Validator.AddEntityError(bag, "garden_id", "Garden does not exist")
return
}
if !garden.Active {
this.Validator.AddEntityError(bag, "garden_id", "Cannot add plants to inactive garden")
}
// Check garden capacity
if err := this.validateGardenCapacity(ctx, garden, entity, bag); err != nil {
this.Validator.AddEntityError(bag, "garden_id", "Garden capacity validation failed")
}
}
}
Uniqueness Validation
func (this *PlantEntityValidator) validateUniqueness(ctx context.Context, entity *mdl.Plant, bag *validator.EntityBag) {
// Check for unique name within garden
mod := this.PlantFetcher.Mod()
mod.ExactStringValueFilter("garden_id", entity.GardenId)
mod.ExactStringValueFilter("name", entity.Name)
if entity.Id != "" {
mod.UnequalStringValueFilter("id", entity.Id) // Exclude self for updates
}
existing, err := this.PlantFetcher.FindSet(ctx, mod)
if err != nil {
this.Validator.AddEntityError(bag, "name", "Unable to validate plant name uniqueness")
return
}
if len(existing) > 0 {
this.Validator.AddEntityError(bag, "name", "Plant name must be unique within garden")
}
}
State-Specific Validation
Create Validation
func (this *PlantEntityValidator) ValidateForCreate(ctx context.Context, entity *mdl.Plant) error {
bag := this.Validator.MakeEntityBag()
this.validateRequiredFields(entity, bag)
this.validateBusinessRules(ctx, entity, bag)
this.validateConstraints(ctx, entity, bag)
this.validateUniqueness(ctx, entity, bag)
if !this.Validator.IsValid(bag) {
return this.Validator.NewEntityValidationError(bag)
}
return nil
}
Update Validation
func (this *PlantEntityValidator) ValidateForUpdate(ctx context.Context, entity *mdl.Plant, original *mdl.Plant) error {
bag := this.Validator.MakeEntityBag()
this.validateRequiredFields(entity, bag)
this.validateBusinessRules(ctx, entity, bag)
this.validateConstraints(ctx, entity, bag)
this.validateUniqueness(ctx, entity, bag)
this.validateStateTransitions(entity, original, bag)
if !this.Validator.IsValid(bag) {
return this.Validator.NewEntityValidationError(bag)
}
return nil
}
State Transition Validation
func (this *PlantEntityValidator) validateStateTransitions(entity *mdl.Plant, original *mdl.Plant, bag *validator.EntityBag) {
// Cannot unharvest a plant
if !original.HarvestedAt.IsZero() && entity.HarvestedAt.IsZero() {
this.Validator.AddEntityError(bag, "harvested_at", "Cannot unharvest a plant")
}
// Cannot change garden if plant has been harvested
if !original.HarvestedAt.IsZero() && entity.GardenId != original.GardenId {
this.Validator.AddEntityError(bag, "garden_id", "Cannot move harvested plants")
}
// Cannot reduce size of living plants
if original.HarvestedAt.IsZero() && entity.Size < original.Size {
this.Validator.AddEntityError(bag, "size", "Cannot reduce size of living plants")
}
}
Delete Validation
func (this *PlantEntityValidator) ValidateForDelete(ctx context.Context, entity *mdl.Plant) error {
bag := this.Validator.MakeEntityBag()
// Check if plant has active tasks
if hasActiveTasks, err := this.hasActiveTasks(ctx, entity); err != nil {
this.Validator.AddEntityError(bag, "id", "Unable to validate plant dependencies")
} else if hasActiveTasks {
this.Validator.AddEntityError(bag, "id", "Cannot delete plant with active tasks")
}
// Check if plant is referenced in seed programs
if isInSeedPrograms, err := this.isInSeedPrograms(ctx, entity); err != nil {
this.Validator.AddEntityError(bag, "id", "Unable to validate seed program references")
} else if isInSeedPrograms {
this.Validator.AddEntityError(bag, "id", "Cannot delete plant referenced in seed programs")
}
if !this.Validator.IsValid(bag) {
return this.Validator.NewEntityValidationError(bag)
}
return nil
}
Complex Business Rules
Seasonal Validation
func (this *PlantEntityValidator) validateSeasonalRules(entity *mdl.Plant, bag *validator.EntityBag) {
if !entity.PlantedAt.IsZero() {
month := entity.PlantedAt.Month()
// Check if planting season is appropriate for this species
if entity.Species == "Tomato" {
if month < time.April || month > time.June {
this.Validator.AddEntityError(bag, "planted_at", "Tomatoes should be planted between April and June")
}
}
if entity.Species == "Lettuce" {
if month < time.March || month > time.September {
this.Validator.AddEntityError(bag, "planted_at", "Lettuce should be planted between March and September")
}
}
}
}
Capacity Validation
func (this *PlantEntityValidator) validateGardenCapacity(ctx context.Context, garden *mdl.Garden, plant *mdl.Plant, bag *validator.EntityBag) error {
// Get current plants in garden
mod := this.PlantFetcher.Mod()
mod.ExactStringValueFilter("garden_id", garden.Id)
mod.AbsentValueFilter("harvested_at") // Only count living plants
if plant.Id != "" {
mod.UnequalStringValueFilter("id", plant.Id) // Exclude self for updates
}
currentPlants, err := this.PlantFetcher.FindSet(ctx, mod)
if err != nil {
return err
}
// Calculate space usage
currentSpace := 0
for _, p := range currentPlants {
currentSpace += p.Size
}
totalSpace := currentSpace + plant.Size
if totalSpace > garden.MaxCapacity {
this.Validator.AddEntityError(bag, "size", fmt.Sprintf("Garden capacity exceeded. Available: %d, Required: %d", garden.MaxCapacity-currentSpace, plant.Size))
}
return nil
}
Integration with Maestros
Entity validators are typically used in Maestro operations:
func (this *CreatePlantMae) Execute(ctx context.Context, input *maes.CreatePlantMaeInput) (*maes.CreatePlantMaeOutput, error) {
// Transform form to entity
plant, err := this.PlantMolder.MoldFromCreateForm(input.Form)
if err != nil {
return nil, fmt.Errorf("failed to mold plant: %w", err)
}
// Validate entity
if err := this.PlantEntityValidator.ValidateForCreate(ctx, plant); err != nil {
return nil, fmt.Errorf("plant validation failed: %w", err)
}
// Create plant
createdPlant, err := this.PlantHandler.Create(ctx, plant, nil)
if err != nil {
return nil, fmt.Errorf("failed to create plant: %w", err)
}
return &maes.CreatePlantMaeOutput{Plant: createdPlant}, nil
}
Error Handling
Entity Validation Errors
type EntityValidationError struct {
Errors map[string][]string
}
func (e *EntityValidationError) Error() string {
var messages []string
for field, errors := range e.Errors {
messages = append(messages, fmt.Sprintf("%s: %s", field, strings.Join(errors, ", ")))
}
return fmt.Sprintf("Entity validation failed: %s", strings.Join(messages, "; "))
}
Graceful Error Handling
func (this *PlantEntityValidator) validateWithFallback(ctx context.Context, entity *mdl.Plant, bag *validator.EntityBag) {
defer func() {
if r := recover(); r != nil {
this.Validator.AddEntityError(bag, "validation", "Internal validation error occurred")
}
}()
// Perform validation...
}
Testing Entity Validators
func TestPlantEntityValidator_ValidateForCreate_Success(t *testing.T) {
validator := setupPlantEntityValidator()
plant := &mdl.Plant{
Name: "Tomato",
Species: "Solanum lycopersicum",
GardenId: "garden123",
Size: 50,
Perennial: false,
}
err := validator.ValidateForCreate(context.Background(), plant)
assert.NoError(t, err)
}
func TestPlantEntityValidator_ValidateForCreate_ValidationErrors(t *testing.T) {
validator := setupPlantEntityValidator()
plant := &mdl.Plant{
Name: "", // Invalid - required
Size: -5, // Invalid - negative
Perennial: true,
}
err := validator.ValidateForCreate(context.Background(), plant)
assert.Error(t, err)
var validationErr *EntityValidationError
assert.True(t, errors.As(err, &validationErr))
assert.Contains(t, validationErr.Errors, "name")
assert.Contains(t, validationErr.Errors, "size")
}
Best Practices
Performance
- Validate cheapest rules first
- Cache expensive lookups when possible
- Avoid N+1 queries in relationship validation
Maintainability
- Keep validation rules close to business logic
- Use descriptive error messages
- Extract common validation patterns
Reliability
- Handle database errors gracefully
- Provide fallbacks for external dependencies
- Log validation failures for monitoring
Consistency
- Use consistent error message formats
- Apply the same validation rules across all operations
- Maintain validation rule documentation
Entity Validators ensure business rule compliance and data integrity at the domain level, providing a robust validation layer that maintains application consistency and reliability.