17 KiB
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
Run Examples
# 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
See examples/07-nickel-generation/ for complete examples.
Core Modules
contract_parser
Parse and validate Nickel contracts.
use typedialog_core::nickel::ContractParser;
let parser = ContractParser::new();
let contracts = parser.parse(nickel_schema)?;
Validates:
- Type correctness
- Field constraints
- Required vs optional fields
- Custom predicates
i18n_extractor
Automatically extract translatable strings from Nickel schemas.
use typedialog_core::nickel::I18nExtractor;
let extractor = I18nExtractor::new();
let strings = extractor.extract(nickel_ast)?;
// Output: Map of translatable strings with locations
Extracts:
- Field labels
- Help text
- Error messages
- Custom metadata
template_renderer
Render Nickel schemas using Tera templates.
use typedialog_core::nickel::NickelTemplateContext;
let context = NickelTemplateContext::from_schema(schema)?;
let rendered = template_engine.render("form.j2", &context)?;
Supports:
- Tera template syntax
- Schema introspection
- Custom filters
- Conditional rendering
roundtrip
Idempotent read/write for Nickel schemas - complete workflow from .ncl → form → .ncl.
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?;
Preserves:
- Comments
- Formatting
- Import structure
- Custom layouts
- Field contracts and validators
Returns:
RoundtripSummarywith diff viewer- Validation status
- Change detection (what fields changed)
Schema Structure
Basic Form
{
title = "User Registration",
fields = {
name = {
type = "text",
label = "Full Name",
required = true,
},
email = {
type = "email",
label = "Email Address",
required = true,
},
}
}
With Contracts
{
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" },
]
}
}
}
With I18n
{
i18n = "en-US",
fields = {
name = {
label = "i18n:form.fields.name",
help = "i18n:form.fields.name.help",
}
}
}
With Templates
{
template = "form.j2",
context = {
theme = "dark",
layout = "grid",
},
fields = { ... }
}
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)
- Load existing
.nclconfiguration - Extract default values using
nickel_pathmappings - Populate form with current values
- Edit via CLI/TUI/Web interface
- Generate new
.nclusing template (or preserve contracts) - Validate with
nickel typecheck - 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:
[[elements]]
name = "parallel_jobs"
type = "text"
prompt = "Parallel Jobs"
default = "4"
nickel_path = ["ci", "github_actions", "parallel_jobs"]
Extracts from:
{
ci = {
github_actions = {
parallel_jobs = 4
}
}
}
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
typedialog nickel-roundtrip \
--input config.ncl \
--form ci-form.toml \
--output config.ncl \
--ncl-template config.ncl.j2 \
--verbose
Output:
╔════════════════════════════════════════════════════════════╗
║ ✅ Configuration Saved Successfully! ║
╠════════════════════════════════════════════════════════════╣
║ 📄 File: config.ncl ║
║ ✓ Validation: ✓ PASSED ║
║ 📊 Fields: 5/27 changed, 22 unchanged ║
╠════════════════════════════════════════════════════════════╣
║ 📋 What Changed: ║
║ ├─ parallel_jobs: 4 → 8 ║
║ ├─ timeout_minutes: 60 → 120 ║
║ ├─ enable_clippy: true → false ║
║ ├─ rust_version: stable → nightly ║
║ ├─ cache_enabled: false → true ║
╠════════════════════════════════════════════════════════════╣
║ 💡 Next Steps: ║
║ • Review: cat config.ncl ║
║ • Apply CI tools: ./setup-ci.sh ║
║ • Re-configure: ./ci-configure.sh ║
╚════════════════════════════════════════════════════════════╝
Roundtrip with TUI Backend
typedialog-tui nickel-roundtrip \
--input config.ncl \
--form ci-form.toml \
--output config.ncl \
--ncl-template config.ncl.j2
Interactive TUI form + terminal summary (same as CLI).
Roundtrip with Web Backend
typedialog-web nickel-roundtrip \
--input config.ncl \
--form ci-form.toml \
--output config.ncl \
--ncl-template config.ncl.j2 \
--verbose
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:
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! ║
╠════════════════════════════════════════════════════════════╣
...
Form Requirements
For roundtrip to work, your form MUST:
-
Include
nickel_pathon ALL fields:[[elements]] name = "project_name" nickel_path = ["project", "name"] # ✅ Required -
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"] -
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
Form values are injected automatically.
Complete Example
See examples/08-nickel-roundtrip/ for a full CI configuration workflow:
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
Validation
Roundtrip automatically validates output:
typedialog nickel-roundtrip \
--input config.ncl \
--form form.toml \
--output config.ncl \
--ncl-template template.ncl.j2
# Runs: nickel typecheck config.ncl
Disable validation:
typedialog nickel-roundtrip ... --no-validate
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):
typedialog nickel-roundtrip ... --verbose
Roundtrip Troubleshooting
Issue: "No default values loaded"
✓ Check all fields have nickel_path:
grep -r "nickel_path" form.toml
Issue: "Field not found in output"
✓ Verify template includes the field:
grep "{{ field_name }}" template.ncl.j2
Issue: "Validation failed"
✓ Check Nickel syntax manually:
nickel typecheck config.ncl
Issue: "Values not showing in web form"
✓ Ensure nickel export works on input:
nickel export config.ncl
Building with Nickel
Build project with Nickel support:
# Build (includes Nickel feature by default)
just build::default
# Test Nickel modules
just test::core
# Generate docs
just dev::docs
Examples
Complete examples in 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
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
Integration Patterns
CLI Backend
typedialog --format nickel form.ncl
TUI Backend
typedialog-tui form.ncl
Web Backend
typedialog-web --config form.ncl
Advanced Topics
Custom Contracts
Define custom validation predicates:
{
fields = {
age = {
type = "number",
contracts = [
{
predicate = |n| n >= 18,
error = "Must be 18+"
}
]
}
}
}
Reusable Components
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" }
}
}
Schema Inheritance
Extend base schemas:
let base = import "schemas/base.ncl";
base & {
fields = base.fields & {
custom_field = { ... }
}
}
I18n Management
Extract and manage translations:
# Extract all translatable strings
cargo run --example nickel_i18n_extraction < form.ncl > strings.json
# Update translations
# (use with translation management system)
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:
nickel export form.ncl
Contract errors
Verify predicates are boolean functions:
# ❌ Wrong
contracts = [
{ predicate = |s| string.length s, error = "..." }
]
# ✓ Correct
contracts = [
{ predicate = |s| string.length s >= 8, error = "..." }
]
Template rendering fails
Ensure Tera template exists and context matches:
# Verify template
ls -la templates/form.j2
# Check context keys
cargo run --example nickel_template_context < form.ncl
I18n strings not extracted
Verify string format:
# ❌ Plain string
label = "Name"
# ✓ I18n reference
label = "i18n:form.name"
Best Practices
-
Schema Organization
- Keep schemas modular
- Use
importfor shared components - Version schemas in git
-
Contracts
- Keep predicates simple
- Provide clear error messages
- Test edge cases
-
I18n
- Use consistent key naming
- Extract before translations
- Validate all strings are referenced
-
Templates
- Keep templates simple
- Use filters for formatting
- Test with multiple data types
-
Roundtrip
- Always test read/write cycles
- Preserve comments with care
- Validate output structure
API Reference
ContractParser
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>,
}
I18nExtractor
pub struct I18nExtractor;
impl I18nExtractor {
pub fn new() -> Self;
pub fn extract(&self, ast: &NickelAst) -> Result<I18nMap>;
}
NickelTemplateContext
pub struct NickelTemplateContext {
pub schema: NickelSchemaIR,
pub fields: Map<String, FieldContext>,
}
impl NickelTemplateContext {
pub fn from_schema(schema: NickelSchemaIR) -> Result<Self>;
}
RoundtripConfig
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(),
}
}
}
Next Steps
- development.md - Build and test
- configuration.md - Backend configuration
- Examples - Complete examples
- Nickel Language - Language reference
Latest Update: December 2024 Status: Stable