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.