# Nickel Roundtrip Workflow

Complete guide to the TypeDialog + Nickel configuration roundtrip for {{ project_name }}.

## Overview

The **Nickel Roundtrip** workflow integrates TypeDialog interactive forms with Nickel's powerful type system and validation:

```
TypeDialog Form → JSON → Nickel → Validation → Export (JSON/YAML/TOML)
      ↑                                                        ↓
      └────────────────── Edit & Re-import ──────────────────┘
```

This enables:
- ✅ User-friendly configuration via TypeDialog forms
- ✅ Type-safe configuration with Nickel contracts
- ✅ Validation against schemas and custom predicates
- ✅ Round-trip editing (export, edit, re-import)

## Workflow Steps

### 1. Collect User Input (TypeDialog)

Run the interactive form:

```bash
typedialog config-form.toml \
  --output-format json \
  --output generated/user-input.json
```

TypeDialog presents form fields, validates input, and outputs JSON.

### 2. Convert JSON to Nickel

Embed user values into Nickel configuration:

```bash
./scripts/json-to-nickel.sh generated/user-input.json generated/config.ncl
```

Generated `config.ncl`:
```nickel
let schemas = import "../schemas/config.ncl" in
let defaults = import "../defaults/config.ncl" in

let user_values = {
  # JSON values embedded here
  server = {
    bind_address = "0.0.0.0:8080",
    max_connections = 100,
  },
} in

let final_config = std.record.merge defaults user_values in
final_config | schemas.Config
```

### 3. Validate with Nickel

Type-check the configuration:

```bash
nickel typecheck generated/config.ncl
```

Nickel verifies:
- All required fields present
- Type contracts satisfied (e.g., `ValidPort`)
- Custom validators pass
- Imports resolve correctly

### 4. Export to Target Format

Export validated configuration:

**JSON:**
```bash
nickel export --format json generated/config.ncl > generated/config.json
```

**YAML:**
```bash
nickel export --format yaml generated/config.ncl > generated/config.yaml
```

**TOML (via JSON):**
```bash
nickel export --format json generated/config.ncl | json2toml > generated/config.toml
```

### 5. Apply to Infrastructure

Use exported configuration with your deployment tool:

```bash
# Terraform
terraform apply -var-file="generated/config.tfvars"

# Ansible
ansible-playbook deploy.yml -e @generated/config.yaml

# Docker Compose
docker-compose -f service.yml --env-file generated/config.env up
```

## Round-Trip Editing

### Export → Edit → Re-import

**Step 1: Export current config**
```bash
nickel export --format json values/config.ncl > /tmp/config.json
```

**Step 2: Edit JSON**
```bash
# Edit with jq
jq '.server.max_connections = 200' /tmp/config.json > /tmp/config-modified.json

# Or edit manually
vim /tmp/config-modified.json
```

**Step 3: Re-import to Nickel**
```bash
./scripts/json-to-nickel.sh /tmp/config-modified.json values/config.ncl
```

**Step 4: Validate**
```bash
nickel typecheck values/config.ncl
```

If validation passes, your edited config is ready!

## Advanced Workflows

### Merging Multiple Configurations

Combine base config with environment-specific overrides:

```nickel
let base = import "./config.ncl" in
let production_overrides = {
  server.max_connections = 500,
  logging.level = "warn",
} in

std.record.merge base production_overrides
```

### Custom Validators

Add domain-specific validation in `validators/`:

```nickel
# validators/custom.ncl
{
  ValidTrackerArray = fun label =>
    std.contract.from_predicate (fun trackers =>
      std.is_array trackers &&
      let bind_addresses = std.array.map (fun t => t.bind_address) trackers in
      # Ensure all bind addresses are unique
      std.array.length bind_addresses ==
        std.array.length (std.array.sort bind_addresses)
    ) {
      label = label,
      message = "tracker bind addresses must be unique",
    },
}
```

Use in schema:

```nickel
let validators = import "../validators/custom.ncl" in

{
  Config = {
    trackers | validators.ValidTrackerArray "config.trackers",
  },
}
```

### Conditional Configuration

Use Nickel's `if-then-else` for environment-specific logic:

```nickel
let env = "production" in  # or "development"

{
  server = {
    max_connections = if env == "production" then 500 else 50,
    logging_level = if env == "production" then "warn" else "debug",
  },
}
```

### Schema Imports and Composition

Organize schemas into modules:

```nickel
# schemas/server.ncl
{
  Server = {
    bind_address | String,
    port | Number,
    max_connections | Number,
  },
}

# schemas/database.ncl
{
  Database = {
    url | String,
    pool_size | Number,
  },
}

# schemas/config.ncl
let server_schema = import "./server.ncl" in
let database_schema = import "./database.ncl" in

{
  Config = {
    server | server_schema.Server,
    database | database_schema.Database | optional,
  },
}
```

## Integration with CI/CD

### Pre-commit Hook

Validate Nickel config before commit:

```bash
#!/bin/bash
# .git/hooks/pre-commit

if ! nickel typecheck values/config.ncl; then
  echo "❌ Nickel validation failed!"
  exit 1
fi

echo "✅ Nickel validation passed"
```

### CI Pipeline (GitHub Actions)

``yaml
name: Validate Config

on: [push, pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install Nickel
        run: cargo install nickel

      - name: Validate configuration
        run: nickel typecheck provisioning/values/config.ncl

      - name: Export to JSON
        run: nickel export provisioning/values/config.ncl > config.json
```

## Troubleshooting

### Error: "contract violated"

**Example:**
```
Error: contract violated by value
  --> bind_address: "invalid"
  expected: ValidBindAddress
```

**Fix:** The value doesn't match the validator. Check `validators/common.ncl` for the contract definition.

### Error: "infinite recursion detected"

**Cause:** Circular imports in Nickel files.

**Fix:** Ensure import graph is acyclic:
```
schemas/config.ncl
  ├→ schemas/server.ncl
  └→ schemas/database.ncl
```

Never create cycles like:
```
server.ncl → database.ncl → server.ncl  # ❌ BAD
```

### Error: "field not found"

**Cause:** Missing required field in configuration.

**Fix:** Add the field or mark it as `optional` in schema:

```nickel
{
  Config = {
    required_field | String,
    optional_field | String | optional,  # Won't error if missing
  },
}
```

## Best Practices

1. **Always validate before exporting:**
   ```bash
   nickel typecheck config.ncl && nickel export config.ncl
   ```

2. **Use defaults for common values:**
   ```nickel
   let defaults = {max_connections = 100} in
   std.record.merge defaults user_values
   ```

3. **Separate schemas from values:**
   - `schemas/` → Type contracts (versioned, stable)
   - `values/` → Runtime configuration (user-editable)

4. **Document contracts:**
   ```nickel
   {
     # Port number must be 1-65535
     port | ValidPort "config.port",
   }
   ```

5. **Test with `nickel query`:**
   ```bash
   nickel query --field server.port config.ncl
   ```

## Resources

- [Nickel Language Documentation](https://nickel-lang.org/user-manual/)
- [TypeDialog Forms Guide](https://github.com/yourusername/typedialog)
- [Example: torrust-tracker-deployer](https://github.com/torrust/torrust-tracker-deployer)

---

**Generated by typedialog-prov-gen for {{ project_name }}**
