Add contract parsing, i18n extraction, template rendering, and roundtrip support for Nickel. Update backends and form parser for integration. Add testing and build infrastructure.
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