TypeDialog/docs/nickel.md
2026-01-11 22:35:49 +00:00

17 KiB

TypeDialog Logo

Nickel Schema Support

Full support for type-safe form schemas using the Nickel language.

Overview

TypeDialog integrates Nickel for:

  • Type-safe schemas - Validate form structure at parse time
  • Contract validation - Enforce type and content contracts
  • I18n extraction - Auto-extract translatable strings
  • Template rendering - Generate forms via Tera templates
  • Roundtrip support - Idempotent read/write transformations

Quick Start

Prerequisites

Install Nickel CLI (optional, for schema validation):

cargo install nickel-lang
# Or download from: https://github.com/nickel-lang/nickel/releases
```text

### Run Examples

```bash
# List all Nickel examples
just test::list | grep nickel

# Run basic Nickel schema
cargo run --example basic_nickel_schema

# Run with roundtrip (read, transform, write)
cargo run --example nickel_roundtrip

# Run with i18n extraction
cargo run --example nickel_i18n_extraction
```text

See [examples/07-nickel-generation/](../examples/07-nickel-generation/) for complete examples.

## Core Modules

### contract_parser

Parse and validate Nickel contracts.

```rust
use typedialog_core::nickel::ContractParser;

let parser = ContractParser::new();
let contracts = parser.parse(nickel_schema)?;
```text

**Validates:**

- Type correctness
- Field constraints
- Required vs optional fields
- Custom predicates

### i18n_extractor

Automatically extract translatable strings from Nickel schemas.

```rust
use typedialog_core::nickel::I18nExtractor;

let extractor = I18nExtractor::new();
let strings = extractor.extract(nickel_ast)?;
// Output: Map of translatable strings with locations
```text

**Extracts:**

- Field labels
- Help text
- Error messages
- Custom metadata

### template_renderer

Render Nickel schemas using Tera templates.

```rust
use typedialog_core::nickel::NickelTemplateContext;

let context = NickelTemplateContext::from_schema(schema)?;
let rendered = template_engine.render("form.j2", &context)?;
```text

**Supports:**

- Tera template syntax
- Schema introspection
- Custom filters
- Conditional rendering

### roundtrip

Idempotent read/write for Nickel schemas - complete workflow from `.ncl` → form → `.ncl`.

```rust
use typedialog_core::nickel::RoundtripConfig;

let mut config = RoundtripConfig::with_template(
    input_ncl,  // Path to existing .ncl file
    form_toml,  // Path to form definition
    output_ncl, // Path for generated .ncl
    template,   // Optional .ncl.j2 template
);
config.validate = true;
config.verbose = true;

// Execute with any backend (CLI, TUI, Web)
let result = config.execute_with_backend(backend.as_mut()).await?;
```text

**Preserves:**

- Comments
- Formatting
- Import structure
- Custom layouts
- Field contracts and validators

**Returns:**

- `RoundtripSummary` with diff viewer
- Validation status
- Change detection (what fields changed)

## Schema Structure

### Basic Form

```nicl
{
  title = "User Registration",
  fields = {
    name = {
      type = "text",
      label = "Full Name",
      required = true,
    },
    email = {
      type = "email",
      label = "Email Address",
      required = true,
    },
  }
}
```text

### With Contracts

```nicl
{
  fields = {
    password = {
      type = "text",
      label = "Password",
      contracts = [
        { predicate = |s| string.length s >= 8, error = "Min 8 chars" },
        { predicate = |s| string.contains s "!", error = "Need special char" },
      ]
    }
  }
}
```text

### With I18n

```nicl
{
  i18n = "en-US",
  fields = {
    name = {
      label = "i18n:form.fields.name",
      help = "i18n:form.fields.name.help",
    }
  }
}
```text

### With Templates

```nicl
{
  template = "form.j2",
  context = {
    theme = "dark",
    layout = "grid",
  },
  fields = { ... }
}
```text

## Roundtrip Workflow

The roundtrip workflow enables interactive reconfiguration of existing Nickel files through forms, preserving structure and generating human-readable diffs.

### Roundtrip Overview

**Workflow:** `config.ncl` (input) → **Form** (edit)`config.ncl` (output)

1. **Load** existing `.ncl` configuration
2. **Extract** default values using `nickel_path` mappings
3. **Populate** form with current values
4. **Edit** via CLI/TUI/Web interface
5. **Generate** new `.ncl` using template (or preserve contracts)
6. **Validate** with `nickel typecheck`
7. **Show** summary with diff viewer

### The `nickel_path` Attribute

**Critical:** Fields MUST have `nickel_path` to participate in roundtrip.

Maps form field names to nested Nickel structure:

```toml
[[elements]]
name = "parallel_jobs"
type = "text"
prompt = "Parallel Jobs"
default = "4"
nickel_path = ["ci", "github_actions", "parallel_jobs"]
```text

Extracts from:

```nickel
{
  ci = {
    github_actions = {
      parallel_jobs = 4
    }
  }
}
```text

**Rules:**

- Array of strings representing path
- Each element is a nested key
- Top-level: `["field_name"]`
- Nested: `["parent", "child", "field"]`
- Arrays: Use RepeatingGroup with `nickel_path = ["array_name"]`

### Roundtrip with CLI Backend

```bash
typedialog nickel-roundtrip \
  --input config.ncl \
  --form ci-form.toml \
  --output config.ncl \
  --ncl-template config.ncl.j2 \
  --verbose
```text

**Output:**

```text
╔════════════════════════════════════════════════════════════╗
║  ✅ Configuration Saved Successfully!                     ║
╠════════════════════════════════════════════════════════════╣
║  📄 File: config.ncl                                       ║
║  ✓ Validation: ✓ PASSED                                   ║
║  📊 Fields: 5/27 changed, 22 unchanged                     ║
╠════════════════════════════════════════════════════════════╣
║  📋 What Changed:                                          ║
║    ├─ parallel_jobs: 48                                 ║
║    ├─ timeout_minutes: 60120                            ║
║    ├─ enable_clippy: truefalse                          ║
║    ├─ rust_version: stable → nightly                       ║
║    ├─ cache_enabled: falsetrue                          ║
╠════════════════════════════════════════════════════════════╣
║  💡 Next Steps:                                            ║
║    • Review: cat config.ncl                                ║
║    • Apply CI tools: ./setup-ci.sh                         ║
║    • Re-configure: ./ci-configure.sh                       ║
╚════════════════════════════════════════════════════════════╝
```text

### Roundtrip with TUI Backend

```bash
typedialog-tui nickel-roundtrip \
  --input config.ncl \
  --form ci-form.toml \
  --output config.ncl \
  --ncl-template config.ncl.j2
```text

Interactive TUI form + terminal summary (same as CLI).

### Roundtrip with Web Backend

```bash
typedialog-web nickel-roundtrip \
  --input config.ncl \
  --form ci-form.toml \
  --output config.ncl \
  --ncl-template config.ncl.j2 \
  --verbose
```text

**Features:**

- Opens browser to `http://localhost:8080`
- All fields pre-populated with current values
- Real-time validation
- **Summary page** on submit:
  - Visual diff viewer (old → new)
  - Field statistics
  - Download button for generated config
  - Auto-close after 30 seconds

**Terminal output:**

```text
Starting interactive form on http://localhost:8080
Complete the form and submit to continue...

Web UI available at http://localhost:8080
[web] Complete form initialized with 63 default values

╔════════════════════════════════════════════════════════════╗
║  ✅ Configuration Saved Successfully!                     ║
╠════════════════════════════════════════════════════════════╣
...
```text

### Form Requirements

For roundtrip to work, your form MUST:

1. **Include `nickel_path` on ALL fields:**

   ```toml
   [[elements]]
   name = "project_name"
   nickel_path = ["project", "name"]  # ✅ Required
  1. Use correct path syntax:

    # Flat field
    nickel_path = ["enable_ci"]
    
    # Nested field
    nickel_path = ["ci", "github_actions", "timeout"]
    
    # Array field (RepeatingGroup)
    nickel_path = ["ci", "tools", "linters"]
    
  2. Match template variables:

    # Template: config.ncl.j2
    {
      project = {
        name = "{{ project_name }}",  # ← Form field "project_name"
      },
      ci = {
        enabled = {{ enable_ci }},     # ← Form field "enable_ci"
      }
    }
    

Template Support

Use Tera templates (.ncl.j2) for complex output structures:

Template: config.ncl.j2

# Generated by TypeDialog
let imports = import "ci/lib.ncl"
let validators = import "ci/validators.ncl"

{
  project = {
    name = "{{ project_name }}",
    version = "{{ project_version }}",
  },

  ci = {
    github_actions = {
      enabled = {{ enable_github_actions }},
      parallel_jobs = {{ parallel_jobs }},
      timeout_minutes = {{ timeout_minutes }},

      {% if enable_cache %}
      cache = {
        enabled = true,
        paths = {{ cache_paths | json }},
      },
      {% endif %}
    },

    tools = {
      {% for tool in ci_tools %}
      {{ tool.name }} = {
        enabled = {{ tool.enabled }},
        version = "{{ tool.version }}",
      },
      {% endfor %}
    },
  }
} | validators.CiConfig
```text

**Form values are injected automatically.**

### Complete Example

See `examples/08-nickel-roundtrip/` for a full CI configuration workflow:

```bash
cd examples/08-nickel-roundtrip/

# Initial setup
./01-generate-initial-config.sh

# Edit with CLI
./02-roundtrip-cli.sh

# Edit with TUI
./03-roundtrip-tui.sh

# Edit with Web
./04-roundtrip-web.sh
```text

### Validation

Roundtrip automatically validates output:

```bash
typedialog nickel-roundtrip \
  --input config.ncl \
  --form form.toml \
  --output config.ncl \
  --ncl-template template.ncl.j2
# Runs: nickel typecheck config.ncl
```text

**Disable validation:**

```bash
typedialog nickel-roundtrip ... --no-validate
```text

### Summary Output

All backends generate summaries showing:

- **Total fields:** How many fields in the form
- **Changed fields:** Fields with different values
- **Unchanged fields:** Fields that kept the same value
- **Validation status:** Pass/fail from `nickel typecheck`
- **Change list:** Detailed old → new for each change

**Verbose mode** shows ALL changes (not just first 10):

```bash
typedialog nickel-roundtrip ... --verbose
```text

### Roundtrip Troubleshooting

**Issue: "No default values loaded"**

✓ Check all fields have `nickel_path`:

```bash
grep -r "nickel_path" form.toml
```text

**Issue: "Field not found in output"**

✓ Verify template includes the field:

```bash
grep "{{ field_name }}" template.ncl.j2
```text

**Issue: "Validation failed"**

✓ Check Nickel syntax manually:

```bash
nickel typecheck config.ncl
```text

**Issue: "Values not showing in web form"**

✓ Ensure `nickel export` works on input:

```bash
nickel export config.ncl
```text

## Building with Nickel

Build project with Nickel support:

```bash
# Build (includes Nickel feature by default)
just build::default

# Test Nickel modules
just test::core

# Generate docs
just dev::docs
```text

## Examples

Complete examples in [examples/07-nickel-generation/](../examples/07-nickel-generation/):

### Schemas

- **simple.ncl** - Basic form schema
- **complex.ncl** - Advanced with sections and groups
- **conditional.ncl** - Conditional field visibility
- **i18n.ncl** - Internationalization example

### Templates

- **config.ncl.j2** - Configuration form template
- **deployment.ncl.j2** - Deployment spec template
- **service_spec.ncl.j2** - Service specification template

### Usage

```bash
cd examples/07-nickel-generation/

# Validate schema
nickel export schemas/simple.ncl

# Parse and render
cargo run --manifest-path ../../Cargo.toml \
  --example nickel_form_generation < schemas/simple.ncl
```text

## Integration Patterns

### CLI Backend

```bash
typedialog --format nickel form.ncl
```text

### TUI Backend

```bash
typedialog-tui form.ncl
```text

### Web Backend

```bash
typedialog-web --config form.ncl
```text

## Advanced Topics

### Custom Contracts

Define custom validation predicates:

```nicl
{
  fields = {
    age = {
      type = "number",
      contracts = [
        {
          predicate = |n| n >= 18,
          error = "Must be 18+"
        }
      ]
    }
  }
}
```text

### Reusable Components

```nicl
let address_fields = {
  street = { type = "text", label = "Street" },
  city = { type = "text", label = "City" },
  zip = { type = "text", label = "ZIP" },
};

{
  fields = address_fields & {
    country = { type = "select", label = "Country" }
  }
}
```text

### Schema Inheritance

Extend base schemas:

```nicl
let base = import "schemas/base.ncl";

base & {
  fields = base.fields & {
    custom_field = { ... }
  }
}
```text

### I18n Management

Extract and manage translations:

```bash
# Extract all translatable strings
cargo run --example nickel_i18n_extraction < form.ncl > strings.json

# Update translations
# (use with translation management system)
```text

## Performance Considerations

### Parsing

- Schemas parsed once at startup
- AST cached in memory
- Validation is O(n) in field count

### Rendering

- Templates compiled once
- Context generation O(n)
- Output streamed for large forms

### Roundtrip

- Preserves source formatting
- Minimal allocations
- In-place transformations

## Troubleshooting

### Schema validation fails

Check syntax with Nickel CLI:

```bash
nickel export form.ncl
```text

### Contract errors

Verify predicates are boolean functions:

```nicl
# ❌ Wrong
contracts = [
  { predicate = |s| string.length s, error = "..." }
]

# ✓ Correct
contracts = [
  { predicate = |s| string.length s >= 8, error = "..." }
]
```text

### Template rendering fails

Ensure Tera template exists and context matches:

```bash
# Verify template
ls -la templates/form.j2

# Check context keys
cargo run --example nickel_template_context < form.ncl
```text

### I18n strings not extracted

Verify string format:

```nicl
# ❌ Plain string
label = "Name"

# ✓ I18n reference
label = "i18n:form.name"
```text

## Best Practices

1. **Schema Organization**
   - Keep schemas modular
   - Use `import` for shared components
   - Version schemas in git

2. **Contracts**
   - Keep predicates simple
   - Provide clear error messages
   - Test edge cases

3. **I18n**
   - Use consistent key naming
   - Extract before translations
   - Validate all strings are referenced

4. **Templates**
   - Keep templates simple
   - Use filters for formatting
   - Test with multiple data types

5. **Roundtrip**
   - Always test read/write cycles
   - Preserve comments with care
   - Validate output structure

## API Reference

### ContractParser

```rust
pub struct ContractParser;

impl ContractParser {
    pub fn new() -> Self;
    pub fn parse(&self, source: &str) -> Result<ParsedContracts>;
}

pub struct ParsedContracts {
    pub fields: Map<String, FieldContract>,
    pub global: Vec<Contract>,
}
```text

### I18nExtractor

```rust
pub struct I18nExtractor;

impl I18nExtractor {
    pub fn new() -> Self;
    pub fn extract(&self, ast: &NickelAst) -> Result<I18nMap>;
}
```text

### NickelTemplateContext

```rust
pub struct NickelTemplateContext {
    pub schema: NickelSchemaIR,
    pub fields: Map<String, FieldContext>,
}

impl NickelTemplateContext {
    pub fn from_schema(schema: NickelSchemaIR) -> Result<Self>;
}
```text

### RoundtripConfig

```rust
pub struct RoundtripConfig {
    pub preserve_comments: bool,
    pub preserve_formatting: bool,
    pub indent: String,
}

impl Default for RoundtripConfig {
    fn default() -> Self {
        Self {
            preserve_comments: true,
            preserve_formatting: true,
            indent: "  ".to_string(),
        }
    }
}
```text

## Next Steps

- [development.md](development.md) - Build and test
- [configuration.md](configuration.md) - Backend configuration
- [Examples](../examples/07-nickel-generation/) - Complete examples
- [Nickel Language](https://nickel-lang.org) - Language reference

---

**Latest Update**: December 2024
**Status**: Stable