8.4 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.
use typedialog_core::nickel::RoundtripConfig;
let config = RoundtripConfig::default();
let schema = parser.parse_file("form.ncl")?;
let modified = transform(schema)?;
serializer.write_file("form.ncl", &modified, &config)?;
Preserves:
- Comments
- Formatting
- Import structure
- Custom layouts
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 = { ... }
}
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