# Validators Validation logic for configuration values using constraints and business rules. ## Purpose Validators provide: - **Constraint checking** - Numeric ranges, required fields - **Business logic validation** - Service-specific constraints - **Error messages** - Clear feedback on invalid values - **Composition with configs** - Validators applied during config generation ## File Organization ``` validators/ ├── README.md # This file ├── common-validator.ncl # Ports, positive numbers, strings ├── network-validator.ncl # IP addresses, bind addresses ├── path-validator.ncl # File paths, directories ├── resource-validator.ncl # CPU, memory, disk ├── string-validator.ncl # Workspace names, identifiers ├── orchestrator-validator.ncl # Queue, workflow validation ├── control-center-validator.ncl # RBAC, policy validation ├── mcp-server-validator.ncl # MCP tools, capabilities └── deployment-validator.ncl # Resource allocation ``` ## Validation Patterns ### 1. Basic Range Validation ```nickel # validators/common-validator.ncl let constraints = import "../constraints/constraints.toml" in { ValidPort = fun port => if port < constraints.common.server.port.min then std.contract.blame_with_message "Port < 1024" port else if port > constraints.common.server.port.max then std.contract.blame_with_message "Port > 65535" port else port, } ``` ### 2. Range Validator (Reusable) ```nickel # Reusable validator for any numeric range ValidRange = fun min max value => if value < min then std.contract.blame_with_message "Value < %{std.to_string min}" value else if value > max then std.contract.blame_with_message "Value > %{std.to_string max}" value else value, ``` ### 3. Enum Validation ```nickel { ValidStorageBackend = fun backend => if backend != 'filesystem && backend != 'rocksdb && backend != 'surrealdb && backend != 'postgres then std.contract.blame_with_message "Invalid backend" backend else backend, } ``` ### 4. String Validation ```nickel { ValidNonEmptyString = fun s => if s == "" then std.contract.blame_with_message "Cannot be empty" s else s, ValidWorkspaceName = fun name => if std.string.matches "^[a-z0-9_-]+$" name then name else std.contract.blame_with_message "Invalid workspace name" name, } ``` ## Common Validators ### common-validator.ncl ```nickel let constraints = import "../constraints/constraints.toml" in { # Port validation ValidPort = fun port => if port < constraints.common.server.port.min then error "Port too low" else if port > constraints.common.server.port.max then error "Port too high" else port, # Positive integer ValidPositiveNumber = fun n => if n <= 0 then error "Must be positive" else n, # Non-empty string ValidNonEmptyString = fun s => if s == "" then error "Cannot be empty" else s, # Generic range validator ValidRange = fun min max value => if value < min then error "Value below minimum" else if value > max then error "Value above maximum" else value, } ``` ### resource-validator.ncl ```nickel let constraints = import "../constraints/constraints.toml" in let common = import "./common-validator.ncl" in { # Validate CPU cores for deployment mode ValidCPUCores = fun mode cores => let limits = constraints.deployment.{mode} in common.ValidRange limits.cpu.min limits.cpu.max cores, # Validate memory allocation ValidMemory = fun mode memory_mb => let limits = constraints.deployment.{mode} in common.ValidRange limits.memory_mb.min limits.memory_mb.max memory_mb, } ``` ## Service-Specific Validators ### orchestrator-validator.ncl ```nickel let constraints = import "../constraints/constraints.toml" in let common = import "./common-validator.ncl" in { # Validate worker count ValidWorkers = fun workers => common.ValidRange constraints.orchestrator.workers.min constraints.orchestrator.workers.max workers, # Validate queue concurrency ValidConcurrentTasks = fun tasks => common.ValidRange constraints.orchestrator.queue.concurrent_tasks.min constraints.orchestrator.queue.concurrent_tasks.max tasks, # Validate batch parallelism ValidParallelLimit = fun limit => common.ValidRange constraints.orchestrator.batch.parallel_limit.min constraints.orchestrator.batch.parallel_limit.max limit, # Validate task timeout (ms) ValidTaskTimeout = fun timeout => if timeout < 1000 then error "Timeout < 1 second" else if timeout > 86400000 then error "Timeout > 24 hours" else timeout, } ``` ### control-center-validator.ncl ```nickel { # JWT token expiration ValidTokenExpiration = fun seconds => if seconds < 300 then error "Token expiration < 5 min" else if seconds > 604800 then error "Token expiration > 7 days" else seconds, # Rate limit threshold ValidRateLimit = fun requests_per_minute => if requests_per_minute < 10 then error "Rate limit too low" else if requests_per_minute > 10000 then error "Rate limit too high" else requests_per_minute, } ``` ### mcp-server-validator.ncl ```nickel { # Max concurrent tool executions ValidConcurrentTools = fun count => if count < 1 then error "Must allow >= 1 concurrent" else if count > 20 then error "Max 20 concurrent tools" else count, # Max resource size ValidMaxResourceSize = fun bytes => if bytes < 1048576 then error "Min 1 MB" else if bytes > 1073741824 then error "Max 1 GB" else bytes, } ``` ## Composition with Configs Validators are applied in config files: ```nickel # configs/orchestrator.solo.ncl let validators = import "../validators/orchestrator-validator.ncl" in { orchestrator = { server.workers = validators.ValidWorkers 2, # Validated queue.max_concurrent_tasks = validators.ValidConcurrentTasks 3, # Validated }, } ``` Validation happens at: 1. **Config composition** - When config is evaluated 2. **Nickel typecheck** - When config is typechecked 3. **Form submission** - When TypeDialog form is submitted (constraints) 4. **TOML export** - When Nickel is exported to TOML ## Error Handling ### Validation Errors ```nickel # If validation fails during config evaluation: # Error: Port too high ``` ### Meaningful Messages Always provide context in error messages: ```nickel # Bad std.contract.blame "Invalid" value # Good std.contract.blame_with_message "Port must be 1024-65535, got %{std.to_string value}" port ``` ## Best Practices 1. **Reuse common validators** - Build from common-validator.ncl 2. **Name clearly** - Prefix with "Valid" (ValidPort, ValidWorkers, etc.) 3. **Error messages** - Include valid range or enum in message 4. **Test edge cases** - Verify min/max boundary values 5. **Document assumptions** - Why a constraint exists ## Testing Validators ```bash # Test a single validator nickel eval -c 'import "validators/orchestrator-validator.ncl" as v in v.ValidWorkers 2' # Test config with validators nickel typecheck provisioning/.typedialog/provisioning/platform/configs/orchestrator.solo.ncl # Evaluate config (runs validators) nickel eval provisioning/.typedialog/provisioning/platform/configs/orchestrator.solo.ncl # Export to TOML (validates during export) nickel export --format toml provisioning/.typedialog/provisioning/platform/configs/orchestrator.solo.ncl ``` ## Adding a New Validator 1. **Create validator function** in appropriate file: ```nickel ValidMyValue = fun value => if value < minimum then error "Too low" else if value > maximum then error "Too high" else value, ``` 2. **Add constraint** to constraints.toml if needed: ```toml [service.feature.my_value] min = 1 max = 100 ``` 3. **Use in config**: ```nickel my_value = validators.ValidMyValue 50, ``` 4. **Add form constraint** (if interactive): ```toml [[elements]] name = "my_value" min = "${constraint.service.feature.my_value.min}" max = "${constraint.service.feature.my_value.max}" ``` 5. **Test**: ```bash nickel typecheck configs/service.mode.ncl ``` --- **Version**: 1.0.0 **Last Updated**: 2025-01-05