//! 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())); }