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