8.3 KiB
8.3 KiB
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
# 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)
# 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
{
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
{
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
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
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
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
{
# 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
{
# 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:
# 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:
- Config composition - When config is evaluated
- Nickel typecheck - When config is typechecked
- Form submission - When TypeDialog form is submitted (constraints)
- TOML export - When Nickel is exported to TOML
Error Handling
Validation Errors
# If validation fails during config evaluation:
# Error: Port too high
Meaningful Messages
Always provide context in error messages:
# 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
- Reuse common validators - Build from common-validator.ncl
- Name clearly - Prefix with "Valid" (ValidPort, ValidWorkers, etc.)
- Error messages - Include valid range or enum in message
- Test edge cases - Verify min/max boundary values
- Document assumptions - Why a constraint exists
Testing Validators
# 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
-
Create validator function in appropriate file:
ValidMyValue = fun value => if value < minimum then error "Too low" else if value > maximum then error "Too high" else value, -
Add constraint to constraints.toml if needed:
[service.feature.my_value] min = 1 max = 100 -
Use in config:
my_value = validators.ValidMyValue 50, -
Add form constraint (if interactive):
[[elements]] name = "my_value" min = "${constraint.service.feature.my_value.min}" max = "${constraint.service.feature.my_value.max}" -
Test:
nickel typecheck configs/service.mode.ncl
Version: 1.0.0 Last Updated: 2025-01-05