330 lines
8.3 KiB
Markdown
Raw Normal View History

# 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
2026-01-12 04:41:31 +00:00
```plaintext
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
2026-01-12 04:41:31 +00:00
```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
2026-01-12 04:41:31 +00:00
```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
2026-01-12 04:41:31 +00:00
```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
2026-01-12 04:41:31 +00:00
```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
2026-01-12 04:41:31 +00:00
```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:
2026-01-12 04:41:31 +00:00
```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:
2026-01-12 04:41:31 +00:00
```toml
[service.feature.my_value]
min = 1
max = 100
```
3. **Use in config**:
2026-01-12 04:41:31 +00:00
```nickel
my_value = validators.ValidMyValue 50,
```
4. **Add form constraint** (if interactive):
2026-01-12 04:41:31 +00:00
```toml
[[elements]]
name = "my_value"
min = "${constraint.service.feature.my_value.min}"
max = "${constraint.service.feature.my_value.max}"
```
5. **Test**:
2026-01-12 04:41:31 +00:00
```bash
nickel typecheck configs/service.mode.ncl
```
---
**Version**: 1.0.0
**Last Updated**: 2025-01-05