TypeDialog/tests/nickel_integration.rs

657 lines
21 KiB
Rust
Raw Normal View History

//! Integration tests for Nickel ↔ typedialog bidirectional workflows
//!
//! Tests the complete workflow:
//! 1. Nickel schema → metadata extraction
//! 2. Metadata → TOML form generation
//! 3. Form results → Nickel output serialization
//! 4. Template rendering with form results
use typedialog::nickel::{
NickelSchemaIR, NickelFieldIR, NickelType, MetadataParser, TomlGenerator,
NickelSerializer, ContractValidator, TemplateEngine,
};
use typedialog::form_parser;
use serde_json::json;
use std::collections::HashMap;
#[test]
fn test_simple_schema_roundtrip() {
// Create a simple schema
let schema = NickelSchemaIR {
name: "user_config".to_string(),
description: Some("Simple user configuration".to_string()),
fields: vec![
NickelFieldIR {
path: vec!["username".to_string()],
flat_name: "username".to_string(),
nickel_type: NickelType::String,
doc: Some("User login name".to_string()),
default: Some(json!("admin")),
optional: false,
contract: Some("String | std.string.NonEmpty".to_string()),
group: None,
},
NickelFieldIR {
path: vec!["email".to_string()],
flat_name: "email".to_string(),
nickel_type: NickelType::String,
doc: Some("User email address".to_string()),
default: None,
optional: true,
contract: None,
group: None,
},
],
};
// Generate form
let form = TomlGenerator::generate(&schema, false, false).expect("Form generation failed");
assert_eq!(form.name, "user_config");
assert_eq!(form.fields.len(), 2);
// Verify form fields have nickel metadata
assert_eq!(form.fields[0].nickel_contract, Some("String | std.string.NonEmpty".to_string()));
assert_eq!(form.fields[0].nickel_path, Some(vec!["username".to_string()]));
// Create form results
let mut results = HashMap::new();
results.insert("username".to_string(), json!("alice"));
results.insert("email".to_string(), json!("alice@example.com"));
// Serialize to Nickel
let nickel_output = NickelSerializer::serialize(&results, &schema)
.expect("Serialization failed");
// Verify output contains expected content
assert!(nickel_output.contains("alice"));
assert!(nickel_output.contains("alice@example.com"));
assert!(nickel_output.contains("String | std.string.NonEmpty"));
}
#[test]
fn test_nested_schema_with_flatten() {
// Create schema with nested structure
let schema = NickelSchemaIR {
name: "server_config".to_string(),
description: None,
fields: vec![
NickelFieldIR {
path: vec!["server".to_string(), "hostname".to_string()],
flat_name: "server_hostname".to_string(),
nickel_type: NickelType::String,
doc: Some("Server hostname".to_string()),
default: None,
optional: false,
contract: None,
group: Some("server".to_string()),
},
NickelFieldIR {
path: vec!["server".to_string(), "port".to_string()],
flat_name: "server_port".to_string(),
nickel_type: NickelType::Number,
doc: Some("Server port".to_string()),
default: Some(json!(8080)),
optional: false,
contract: None,
group: Some("server".to_string()),
},
NickelFieldIR {
path: vec!["database".to_string(), "host".to_string()],
flat_name: "database_host".to_string(),
nickel_type: NickelType::String,
doc: Some("Database host".to_string()),
default: None,
optional: false,
contract: None,
group: Some("database".to_string()),
},
],
};
// Generate form with grouping
let form = TomlGenerator::generate(&schema, false, true)
.expect("Form generation failed");
// Verify groups are created
assert!(form.items.len() > 0);
// Verify fields have groups
let server_hostname = form.fields.iter().find(|f| f.name == "server_hostname").unwrap();
assert_eq!(server_hostname.group, Some("server".to_string()));
// Create results
let mut results = HashMap::new();
results.insert("server_hostname".to_string(), json!("api.example.com"));
results.insert("server_port".to_string(), json!(9000));
results.insert("database_host".to_string(), json!("db.example.com"));
// Serialize and verify nested structure is restored
let nickel_output = NickelSerializer::serialize(&results, &schema)
.expect("Serialization failed");
assert!(nickel_output.contains("server"));
assert!(nickel_output.contains("database"));
assert!(nickel_output.contains("api.example.com"));
assert!(nickel_output.contains("db.example.com"));
assert!(nickel_output.contains("9000"));
}
#[test]
fn test_array_field_serialization() {
let schema = NickelSchemaIR {
name: "array_config".to_string(),
description: None,
fields: vec![
NickelFieldIR {
path: vec!["tags".to_string()],
flat_name: "tags".to_string(),
nickel_type: NickelType::Array(Box::new(NickelType::String)),
doc: Some("Configuration tags".to_string()),
default: None,
optional: false,
contract: None,
group: None,
},
],
};
let mut results = HashMap::new();
results.insert("tags".to_string(), json!(["prod", "critical", "network"]));
let nickel_output = NickelSerializer::serialize(&results, &schema)
.expect("Serialization failed");
assert!(nickel_output.contains("prod"));
assert!(nickel_output.contains("critical"));
assert!(nickel_output.contains("network"));
assert!(nickel_output.contains("["));
assert!(nickel_output.contains("]"));
}
#[test]
fn test_contract_validation_non_empty_string() {
// Valid non-empty string
let result = ContractValidator::validate(
&json!("hello"),
"String | std.string.NonEmpty",
);
assert!(result.is_ok());
// Empty string should fail
let result = ContractValidator::validate(
&json!(""),
"String | std.string.NonEmpty",
);
assert!(result.is_err());
}
#[test]
fn test_contract_validation_number_range() {
// Valid number in range
let result = ContractValidator::validate(
&json!(50),
"Number | std.number.between 0 100",
);
assert!(result.is_ok());
// Number out of range
let result = ContractValidator::validate(
&json!(150),
"Number | std.number.between 0 100",
);
assert!(result.is_err());
}
#[test]
fn test_contract_validation_string_length() {
// Valid length
let result = ContractValidator::validate(
&json!("hello"),
"String | std.string.length.min 3",
);
assert!(result.is_ok());
// Too short
let result = ContractValidator::validate(
&json!("hi"),
"String | std.string.length.min 3",
);
assert!(result.is_err());
// Valid max length
let result = ContractValidator::validate(
&json!("hi"),
"String | std.string.length.max 5",
);
assert!(result.is_ok());
// Too long
let result = ContractValidator::validate(
&json!("hello world"),
"String | std.string.length.max 5",
);
assert!(result.is_err());
}
#[test]
fn test_form_definition_from_schema_ir() {
// Create schema
let schema = NickelSchemaIR {
name: "test_form".to_string(),
description: Some("Test form".to_string()),
fields: vec![
NickelFieldIR {
path: vec!["name".to_string()],
flat_name: "name".to_string(),
nickel_type: NickelType::String,
doc: Some("Your name".to_string()),
default: None,
optional: false,
contract: Some("String | std.string.NonEmpty".to_string()),
group: None,
},
],
};
// Generate form
let form = TomlGenerator::generate(&schema, false, false)
.expect("Form generation failed");
// Convert to TOML
let toml_str = toml::to_string_pretty(&form)
.expect("TOML serialization failed");
// Parse back
let parsed_form: form_parser::FormDefinition = toml::from_str(&toml_str)
.expect("TOML parsing failed");
// Verify round-trip
assert_eq!(parsed_form.name, "test_form");
assert_eq!(parsed_form.fields.len(), 1);
assert_eq!(parsed_form.fields[0].name, "name");
assert_eq!(
parsed_form.fields[0].nickel_contract,
Some("String | std.string.NonEmpty".to_string())
);
}
#[test]
fn test_metadata_extraction_with_optional_fields() {
// Parse JSON metadata (simulating nickel query output)
let metadata = json!({
"name": {
"doc": "User full name",
"type": "String",
"default": "Anonymous"
},
"age": {
"doc": "User age in years",
"type": "Number",
"optional": true
},
"active": {
"type": "Bool",
"optional": false,
"default": true
}
});
let schema = MetadataParser::parse(metadata)
.expect("Metadata parsing failed");
// Verify fields
assert_eq!(schema.fields.len(), 3);
// Check optional flags
let name_field = schema.fields.iter().find(|f| f.flat_name == "name").unwrap();
assert!(!name_field.optional);
let age_field = schema.fields.iter().find(|f| f.flat_name == "age").unwrap();
assert!(age_field.optional);
let active_field = schema.fields.iter().find(|f| f.flat_name == "active").unwrap();
assert!(!active_field.optional);
}
#[test]
fn test_type_mapping_all_types() {
let schema = NickelSchemaIR {
name: "types_test".to_string(),
description: None,
fields: vec![
NickelFieldIR {
path: vec!["str_field".to_string()],
flat_name: "str_field".to_string(),
nickel_type: NickelType::String,
doc: None,
default: None,
optional: false,
contract: None,
group: None,
},
NickelFieldIR {
path: vec!["num_field".to_string()],
flat_name: "num_field".to_string(),
nickel_type: NickelType::Number,
doc: None,
default: None,
optional: false,
contract: None,
group: None,
},
NickelFieldIR {
path: vec!["bool_field".to_string()],
flat_name: "bool_field".to_string(),
nickel_type: NickelType::Bool,
doc: None,
default: None,
optional: false,
contract: None,
group: None,
},
NickelFieldIR {
path: vec!["array_field".to_string()],
flat_name: "array_field".to_string(),
nickel_type: NickelType::Array(Box::new(NickelType::String)),
doc: None,
default: None,
optional: false,
contract: None,
group: None,
},
],
};
let form = TomlGenerator::generate(&schema, false, false)
.expect("Form generation failed");
// Verify type mapping
assert_eq!(form.fields[0].field_type, form_parser::FieldType::Text);
assert_eq!(form.fields[1].field_type, form_parser::FieldType::Custom);
assert_eq!(form.fields[1].custom_type, Some("f64".to_string()));
assert_eq!(form.fields[2].field_type, form_parser::FieldType::Confirm);
assert_eq!(form.fields[3].field_type, form_parser::FieldType::Editor);
}
#[test]
fn test_enum_options_extraction_from_doc() {
let field = NickelFieldIR {
path: vec!["status".to_string()],
flat_name: "status".to_string(),
nickel_type: NickelType::Array(Box::new(NickelType::String)),
doc: Some("Status selection. Options: pending, active, completed".to_string()),
default: None,
optional: false,
contract: None,
group: None,
};
let schema = NickelSchemaIR {
name: "test".to_string(),
description: None,
fields: vec![field],
};
let form = TomlGenerator::generate(&schema, false, false)
.expect("Form generation failed");
// Verify options extracted
let status_field = &form.fields[0];
assert_eq!(
status_field.options,
Some(vec![
"pending".to_string(),
"active".to_string(),
"completed".to_string(),
])
);
}
#[test]
fn test_template_rendering_simple() {
#[cfg(feature = "templates")]
{
let mut engine = TemplateEngine::new();
let mut values = HashMap::new();
values.insert("hostname".to_string(), json!("server1"));
values.insert("port".to_string(), json!(8080));
let template = r#"
server {
hostname : String = "{{ hostname }}"
port : Number = {{ port }}
}
"#;
let result = engine.render_str(template, &values);
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("server1"));
assert!(output.contains("8080"));
}
}
#[test]
fn test_template_rendering_with_loop() {
#[cfg(feature = "templates")]
{
let mut engine = TemplateEngine::new();
let mut values = HashMap::new();
values.insert("servers".to_string(), json!([
{"name": "web-01", "ip": "192.168.1.10"},
{"name": "web-02", "ip": "192.168.1.11"},
]));
let template = r#"servers = [
{% for server in servers %}
{ name = "{{ server.name }}", ip = "{{ server.ip }}" },
{% endfor %}
]"#;
let result = engine.render_str(template, &values);
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("web-01"));
assert!(output.contains("web-02"));
assert!(output.contains("192.168.1.10"));
}
}
#[test]
fn test_template_rendering_with_conditional() {
#[cfg(feature = "templates")]
{
let mut engine = TemplateEngine::new();
let mut values = HashMap::new();
values.insert("production".to_string(), json!(true));
values.insert("replicas".to_string(), json!(3));
let template = r#"{% if production %}
replicas : Number = {{ replicas }}
{% else %}
replicas : Number = 1
{% endif %}"#;
let result = engine.render_str(template, &values);
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("replicas"));
assert!(output.contains("3"));
assert!(!output.contains("= 1"));
}
}
#[test]
fn test_full_workflow_integration() {
// Create a realistic schema
let schema = NickelSchemaIR {
name: "app_config".to_string(),
description: Some("Application configuration".to_string()),
fields: vec![
NickelFieldIR {
path: vec!["app".to_string(), "name".to_string()],
flat_name: "app_name".to_string(),
nickel_type: NickelType::String,
doc: Some("Application name".to_string()),
default: None,
optional: false,
contract: Some("String | std.string.NonEmpty".to_string()),
group: Some("app".to_string()),
},
NickelFieldIR {
path: vec!["app".to_string(), "version".to_string()],
flat_name: "app_version".to_string(),
nickel_type: NickelType::String,
doc: Some("Application version (e.g., 1.0.0)".to_string()),
default: Some(json!("1.0.0")),
optional: false,
contract: None,
group: Some("app".to_string()),
},
NickelFieldIR {
path: vec!["server".to_string(), "host".to_string()],
flat_name: "server_host".to_string(),
nickel_type: NickelType::String,
doc: Some("Server hostname".to_string()),
default: Some(json!("localhost")),
optional: false,
contract: None,
group: Some("server".to_string()),
},
NickelFieldIR {
path: vec!["server".to_string(), "port".to_string()],
flat_name: "server_port".to_string(),
nickel_type: NickelType::Number,
doc: Some("Server port".to_string()),
default: Some(json!(8000)),
optional: false,
contract: Some("Number | std.number.between 1 65535".to_string()),
group: Some("server".to_string()),
},
],
};
// Step 1: Generate form
let form = TomlGenerator::generate(&schema, false, true)
.expect("Form generation failed");
assert_eq!(form.fields.len(), 4);
// Step 2: Serialize to TOML and parse back
let toml_str = toml::to_string_pretty(&form)
.expect("TOML serialization failed");
let parsed_form: form_parser::FormDefinition = toml::from_str(&toml_str)
.expect("TOML parsing failed");
assert_eq!(parsed_form.fields.len(), 4);
// Step 3: Create form results
let mut results = HashMap::new();
results.insert("app_name".to_string(), json!("MyApp"));
results.insert("app_version".to_string(), json!("2.0.0"));
results.insert("server_host".to_string(), json!("0.0.0.0"));
results.insert("server_port".to_string(), json!(3000));
// Step 4: Validate contracts
assert!(ContractValidator::validate(&json!("MyApp"), "String | std.string.NonEmpty").is_ok());
assert!(ContractValidator::validate(&json!(3000), "Number | std.number.between 1 65535").is_ok());
// Step 5: Serialize to Nickel
let nickel_output = NickelSerializer::serialize(&results, &schema)
.expect("Serialization failed");
// Step 6: Verify output
assert!(nickel_output.contains("MyApp"));
assert!(nickel_output.contains("2.0.0"));
assert!(nickel_output.contains("0.0.0.0"));
assert!(nickel_output.contains("3000"));
assert!(nickel_output.contains("String | std.string.NonEmpty"));
assert!(nickel_output.contains("Number | std.number.between 1 65535"));
// Verify nested structure
assert!(nickel_output.contains("app"));
assert!(nickel_output.contains("server"));
}
#[test]
fn test_optional_fields_handling() {
let schema = NickelSchemaIR {
name: "optional_test".to_string(),
description: None,
fields: vec![
NickelFieldIR {
path: vec!["required_field".to_string()],
flat_name: "required_field".to_string(),
nickel_type: NickelType::String,
doc: None,
default: None,
optional: false,
contract: None,
group: None,
},
NickelFieldIR {
path: vec!["optional_field".to_string()],
flat_name: "optional_field".to_string(),
nickel_type: NickelType::String,
doc: None,
default: None,
optional: true,
contract: None,
group: None,
},
],
};
let form = TomlGenerator::generate(&schema, false, false)
.expect("Form generation failed");
// Check required field
let required = form.fields.iter().find(|f| f.name == "required_field").unwrap();
assert_eq!(required.required, Some(true));
// Check optional field
let optional = form.fields.iter().find(|f| f.name == "optional_field").unwrap();
assert_eq!(optional.required, Some(false));
}
#[test]
fn test_defaults_preservation() {
let schema = NickelSchemaIR {
name: "defaults_test".to_string(),
description: None,
fields: vec![
NickelFieldIR {
path: vec!["string_with_default".to_string()],
flat_name: "string_with_default".to_string(),
nickel_type: NickelType::String,
doc: None,
default: Some(json!("default_value")),
optional: false,
contract: None,
group: None,
},
NickelFieldIR {
path: vec!["number_with_default".to_string()],
flat_name: "number_with_default".to_string(),
nickel_type: NickelType::Number,
doc: None,
default: Some(json!(42)),
optional: false,
contract: None,
group: None,
},
],
};
let form = TomlGenerator::generate(&schema, false, false)
.expect("Form generation failed");
// Check defaults are preserved
let string_field = form.fields.iter().find(|f| f.name == "string_with_default").unwrap();
assert_eq!(string_field.default, Some("default_value".to_string()));
let number_field = form.fields.iter().find(|f| f.name == "number_with_default").unwrap();
assert_eq!(number_field.default, Some("42".to_string()));
}