# 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

```bash
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

```bash
# 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)

```bash
# 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

```json
{
  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

```json
{
  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

```javascript
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

```javascript
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

```javascript
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

```json
{
  # 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

```json
{
  # 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:

```toml
# 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

```bash
# If validation fails during config evaluation:
# Error: Port too high
```

### Meaningful Messages

Always provide context in error messages:

```bash
# 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
