use std::collections::BTreeMap; use decodal::{Constraint, Data, DecodalDecode, DecodalSchema, EmptyLoader, Engine, Value}; use decodal_derive::Decodal; #[derive(Debug, PartialEq, Decodal)] struct Service { name: String, #[decodal(gt = 443, default = 8443)] port: i64, #[decodal(default = true, rename = "feature.enable")] feature_enabled: bool, #[decodal(default)] tags: Vec, } #[derive(Debug, PartialEq, Decodal)] struct Worker { name: String, #[decodal(default = true)] enabled: bool, } #[derive(Debug, PartialEq, Decodal)] struct Fleet { workers: Vec, } #[derive(Debug, PartialEq, Decodal)] struct OpenConfig { name: String, #[decodal(default = true, rename = "feature.enabled")] enabled: bool, #[decodal(rest)] extra: BTreeMap, } #[derive(Debug, PartialEq, Decodal)] struct StringLabels { name: String, #[decodal(rest)] labels: BTreeMap, } #[derive(Debug, PartialEq, Decodal)] struct OpenBag { #[decodal(rest)] values: BTreeMap, } #[test] fn derives_schema_and_decode() { let mut engine = Engine::new(EmptyLoader); engine .bind_global("Service", Service::decodal_schema()) .unwrap(); let module = engine .add_root_source( "test", "test", r#" Service & { name = "api"; port = 9443; feature.enable = false; tags = ["web", "prod"]; } "#, ) .unwrap(); let value = engine.eval_module(module).unwrap(); let data = engine.materialize(&value).unwrap(); let service = Service::decodal_decode(&data).unwrap(); assert_eq!( service, Service { name: "api".into(), port: 9443, feature_enabled: false, tags: vec!["web".into(), "prod".into()], } ); } #[test] fn defaults_are_available_in_schema() { let mut engine = Engine::new(EmptyLoader); engine .bind_global("Service", Service::decodal_schema()) .unwrap(); let module = engine .add_root_source( "test", "test", r#" Service & { name = "api"; } "#, ) .unwrap(); let value = engine.eval_module(module).unwrap(); let data = engine.materialize(&value).unwrap(); let service = Service::decodal_decode(&data).unwrap(); assert_eq!(service.port, 8443); assert!(service.feature_enabled); assert!(service.tags.is_empty()); } #[test] fn decode_reports_field_path() { let data = Data::Object(vec![]); let error = Service::decodal_decode(&data).unwrap_err(); assert_eq!(error.path, "name"); } #[test] fn vec_schema_rejects_invalid_element_before_decode() { let mut engine = Engine::new(EmptyLoader); engine .bind_global("Service", Service::decodal_schema()) .unwrap(); let module = engine .add_root_source( "test", "test", r#" Service & { name = "api"; port = 9443; tags = ["web", 1]; } "#, ) .unwrap(); let error = engine.eval_module(module).unwrap_err(); assert!( error .labels .iter() .any(|label| label.message.contains("array element [1]")) ); } #[test] fn vec_schema_applies_nested_struct_defaults() { let mut engine = Engine::new(EmptyLoader); engine .bind_global("Fleet", Fleet::decodal_schema()) .unwrap(); let module = engine .add_root_source( "test", "test", r#" Fleet & { workers = [{ name = "api"; }]; } "#, ) .unwrap(); let value = engine.eval_module(module).unwrap(); let data = engine.materialize(&value).unwrap(); let fleet = Fleet::decodal_decode(&data).unwrap(); assert_eq!( fleet, Fleet { workers: vec![Worker { name: "api".into(), enabled: true, }], } ); } #[test] fn rest_map_opens_the_derived_object_and_collects_additional_fields() { let schema = OpenConfig::decodal_schema(); let Value::Object { fields, rest: Some(rest), } = schema else { panic!("derived schema should have an object rest range") }; assert_eq!(fields.len(), 2); assert!(matches!( *rest, Value::Range { ref constraints, .. } if constraints == &[Constraint::Unknown] )); let mut engine = Engine::new(EmptyLoader); engine .bind_global("OpenConfig", OpenConfig::decodal_schema()) .unwrap(); let module = engine .add_root_source( "test", "test", r#" { name = "api"; plugin = { name = "cache"; }; retries = 3; } as OpenConfig "#, ) .unwrap(); let value = engine.eval_module(module).unwrap(); let data = engine.materialize(&value).unwrap(); let config = OpenConfig::decodal_decode(&data).unwrap(); assert_eq!(config.name, "api"); assert!(config.enabled); assert_eq!(config.extra.get("retries"), Some(&Data::Int(3))); assert!(matches!(config.extra.get("plugin"), Some(Data::Object(_)))); assert!(!config.extra.contains_key("name")); assert!(!config.extra.contains_key("feature")); } #[test] fn typed_rest_map_constrains_each_additional_field() { let mut engine = Engine::new(EmptyLoader); engine .bind_global("StringLabels", StringLabels::decodal_schema()) .unwrap(); let module = engine .add_root_source( "test", "test", r#"{ name = "service"; region = "ap-northeast-1"; tier = "edge"; } as StringLabels"#, ) .unwrap(); let value = engine.eval_module(module).unwrap(); let data = engine.materialize(&value).unwrap(); let labels = StringLabels::decodal_decode(&data).unwrap(); assert_eq!(labels.labels.get("region"), Some(&"ap-northeast-1".into())); assert_eq!(labels.labels.get("tier"), Some(&"edge".into())); let module = engine .add_root_source( "invalid", "invalid", r#"{ name = "service"; priority = 1; } as StringLabels"#, ) .unwrap(); assert!(engine.eval_module(module).is_err()); } #[test] fn rest_only_struct_collects_every_field() { let schema = OpenBag::decodal_schema(); assert!(matches!( schema, Value::Object { ref fields, rest: Some(_), } if fields.is_empty() )); let data = Data::Object(vec![decodal::runtime::DataField { name: "answer".into(), value: Data::Int(42), }]); let bag = OpenBag::decodal_decode(&data).unwrap(); assert_eq!(bag.values.get("answer"), Some(&Data::Int(42))); }