use kaptan_config::{ Config, ConfigEnum, ConfigValue, ConfigViolation, FieldId, FieldWriteError, ValidationRule, }; #[derive(Config)] struct LocalSection { #[config(id = 1, key = "local.first", unit = "count", range(min = 1, max = 9))] first: u32, #[config(id = 2, key = "local.second", unit = "count", range(min = 1, max = 9))] second: u32, } #[derive(Config)] #[config(post = Self::validate_order)] struct OrderedSection { #[config( id = 3, key = "ordered.minimum", unit = "count", range(min = 0, max = 9) )] minimum: u32, #[config( id = 4, key = "ordered.maximum", unit = "count", range(min = 0, max = 9) )] maximum: u32, } impl OrderedSection { fn validate_order(&self) -> Result<(), ConfigViolation> { if self.minimum > self.maximum { return Err(ConfigViolation::related( FieldId::new(3), FieldId::new(4), ValidationRule::Inconsistent, )); } Ok(()) } } #[derive(Config)] struct Aggregate { local: LocalSection, ordered: OrderedSection, } #[derive(Config)] struct BoardOwnedSection { #[config( id = 10, key = "board.endpoint", unit = "id", range(min = 1, max = 255) )] endpoint: u32, } #[derive(Config)] struct BoardOwnedAggregate { endpoint: BoardOwnedSection, } #[derive(Clone, Copy, Debug, Eq, PartialEq, ConfigEnum)] #[repr(u8)] enum TestMode { Quiet = 1, Loud = 3, } #[derive(Config)] struct CompactSection { #[config(id = 20, key = "compact.endpoint", unit = "id", range(min = 1))] endpoint: u8, #[config(enum, id = 21, key = "compact.mode", unit = "mode")] mode: TestMode, #[config(id = 22, key = "compact.port", unit = "id", range(min = 1))] port: u16, } #[test] fn generated_section_owns_every_declared_scalar_path() { let mut section = LocalSection { first: 2, second: 4, }; assert_eq!(LocalSection::FIELD_COUNT, 2); assert_eq!( section.field_value(FieldId::new(2)), Some(ConfigValue::U32(4)) ); section .set_field(FieldId::new(2), ConfigValue::U32(8)) .unwrap(); assert_eq!( section.field_value(FieldId::new(2)), Some(ConfigValue::U32(8)) ); assert_eq!( section.set_field(FieldId::new(2), ConfigValue::U32(0)), Err(FieldWriteError::Violation(ConfigViolation::field( FieldId::new(2), ValidationRule::BelowMinimum, ))) ); assert_eq!( section.field_value(FieldId::new(2)), Some(ConfigValue::U32(8)) ); } #[test] fn narrow_scalars_and_compact_enums_keep_native_types_and_generic_metadata() { let mut section = CompactSection { endpoint: 7, mode: TestMode::Quiet, port: 4861, }; assert_eq!(CompactSection::SNAPSHOT_VALUE_BYTES, 1 + 1 + 2); assert_eq!( section.field_value(FieldId::new(20)), Some(ConfigValue::U8(7)) ); assert_eq!( CompactSection::DESCRIPTORS[0].minimum, Some(ConfigValue::U8(1)) ); assert_eq!(CompactSection::DESCRIPTORS[0].maximum, None); assert_eq!( CompactSection::DESCRIPTORS[1].enum_options, TestMode::CONFIG_ENUM_OPTIONS ); assert_eq!(TestMode::Quiet.wire_value(), 1); assert_eq!(TestMode::try_from_wire(3), Some(TestMode::Loud)); assert_eq!(TestMode::try_from_wire(2), None); section .set_field(FieldId::new(21), ConfigValue::U8(3)) .unwrap(); assert_eq!(section.mode, TestMode::Loud); assert!(matches!( section.set_field(FieldId::new(21), ConfigValue::U8(2)), Err(FieldWriteError::Violation(issue)) if issue.rule() == ValidationRule::InvalidEnumValue )); assert!(matches!( section.set_field(FieldId::new(20), ConfigValue::U8(0)), Err(FieldWriteError::Violation(issue)) if issue.rule() == ValidationRule::BelowMinimum )); assert_eq!( section.field_value(FieldId::new(22)), Some(ConfigValue::U16(4861)) ); assert_eq!( CompactSection::DESCRIPTORS[2].kind, kaptan_config::ValueKind::U16 ); } #[test] fn aggregate_routes_writes_and_defers_declared_post_validation() { let mut aggregate = Aggregate { local: LocalSection { first: 2, second: 4, }, ordered: OrderedSection { minimum: 1, maximum: 2, }, }; assert_eq!(Aggregate::FIELD_COUNT, 4); assert_eq!( aggregate.field_value(FieldId::new(2)), Some(ConfigValue::U32(4)) ); aggregate .set_field(FieldId::new(3), ConfigValue::U32(3)) .unwrap(); assert!(matches!( aggregate.validate(), Err(issue) if issue.primary() == Some(FieldId::new(3)) && issue.related_field() == Some(FieldId::new(4)) && issue.rule() == ValidationRule::Inconsistent )); aggregate .set_field(FieldId::new(4), ConfigValue::U32(3)) .unwrap(); aggregate.validate().unwrap(); } #[test] fn aggregate_derives_its_flattened_descriptor_order() { assert_eq!(Aggregate::SNAPSHOT_VALUE_BYTES, 4 * 4); let descriptors: Vec<_> = (0..Aggregate::FIELD_COUNT) .map(|index| Aggregate::descriptor(index).unwrap()) .collect(); assert_eq!( descriptors .iter() .map(|descriptor| descriptor.id.get()) .collect::>(), [1, 2, 3, 4] ); assert!(Aggregate::descriptor(Aggregate::FIELD_COUNT).is_none()); } #[test] fn factory_values_are_owned_outside_the_generated_schema() { let mut aggregate = BoardOwnedAggregate { endpoint: BoardOwnedSection { endpoint: 42 }, }; assert_eq!(BoardOwnedAggregate::FIELD_COUNT, 1); assert_eq!( aggregate.field_value(FieldId::new(10)), Some(ConfigValue::U32(42)) ); aggregate .set_field(FieldId::new(10), ConfigValue::U32(43)) .unwrap(); aggregate.validate().unwrap(); } #[derive(Config)] struct DuplicateChildren { first: LocalSection, second: LocalSection, } #[derive(Config)] struct TransparentRoot { local: LocalSection, } #[derive(Config)] struct FirstKeyOwner { #[config(id = 30, key = "shared.key", unit = "count")] value: u32, } #[derive(Config)] struct SecondKeyOwner { #[config(id = 31, key = "shared.key", unit = "count")] value: u32, } #[derive(Config)] struct DuplicateKeysAcrossChildren { first: FirstKeyOwner, second: SecondKeyOwner, } #[test] fn schema_fingerprint_is_generated_and_one_child_wrappers_are_transparent() { assert_eq!(TransparentRoot::SCHEMA_ID, LocalSection::SCHEMA_ID); assert_ne!(Aggregate::SCHEMA_ID, LocalSection::SCHEMA_ID); } #[test] fn recursive_tree_rejects_duplicate_field_ids() { let duplicate = DuplicateChildren { first: LocalSection { first: 2, second: 4, }, second: LocalSection { first: 2, second: 4, }, }; assert!(matches!( duplicate.validate(), Err(issue) if issue.rule() == ValidationRule::Inconsistent )); } #[test] fn recursive_tree_rejects_duplicate_display_keys() { let duplicate = DuplicateKeysAcrossChildren { first: FirstKeyOwner { value: 1 }, second: SecondKeyOwner { value: 2 }, }; assert!(matches!( duplicate.validate(), Err(issue) if issue.rule() == ValidationRule::Inconsistent )); } #[derive(Config)] struct FloatRules { #[config(id = 1, key = "finite", unit = "", finite)] finite: f32, #[config(id = 2, key = "nonzero", unit = "", nonzero)] nonzero: f64, #[config(id = 3, key = "lower", unit = "", range(min = 0.0))] lower: f32, #[config(id = 4, key = "upper", unit = "", range(max = 1.0))] upper: f64, #[config(id = 5, key = "free", unit = "")] free: f64, #[config(id = 6, key = "nonzero32", unit = "", nonzero)] nonzero32: f32, } #[test] fn scalar_rules_preserve_local_repair_and_float_domains() { let mut values = FloatRules { finite: f32::NAN, nonzero: 0.0, lower: f32::NAN, upper: f64::NAN, free: f64::NAN, nonzero32: 0.0, }; // Every other field is invalid. A local repair must still succeed. values .set_field(FieldId::new(1), ConfigValue::F32(1.0)) .unwrap(); for (id, value, rule) in [ ( 1, ConfigValue::F32(f32::INFINITY), ValidationRule::NonFinite, ), (2, ConfigValue::F64(0.0), ValidationRule::Zero), (2, ConfigValue::F64(-0.0), ValidationRule::Zero), (3, ConfigValue::F32(f32::NAN), ValidationRule::Unordered), (4, ConfigValue::F64(f64::NAN), ValidationRule::Unordered), (6, ConfigValue::F32(0.0), ValidationRule::Zero), (6, ConfigValue::F32(-0.0), ValidationRule::Zero), ] { let id = FieldId::new(id); let before = values.field_value(id); assert_eq!( values.set_field(id, value), Err(FieldWriteError::Violation(ConfigViolation::field(id, rule))) ); assert_eq!(values.field_value(id), before); } // Neither nonzero nor a one-sided range implies finite. for (id, value) in [ (2, ConfigValue::F64(f64::from_bits(0xfff0_0000_0000_0042))), (3, ConfigValue::F32(f32::INFINITY)), (4, ConfigValue::F64(f64::NEG_INFINITY)), (5, ConfigValue::F64(f64::from_bits(0x7ff8_0000_0000_1234))), (6, ConfigValue::F32(f32::INFINITY)), ] { values.set_field(FieldId::new(id), value).unwrap(); assert_eq!(values.field_value(FieldId::new(id)), Some(value)); } values.validate().unwrap(); } #[test] fn rule_metadata_changes_fingerprints() { let descriptor = FloatRules::DESCRIPTORS[0]; assert!(descriptor.finite); assert!(!descriptor.nonzero); assert!(FloatRules::DESCRIPTORS[1].nonzero); let original = kaptan_config::validation::__schema_id(&[descriptor]); assert_ne!( original, kaptan_config::validation::__schema_id(&[kaptan_config::FieldDescriptor { finite: false, ..descriptor }]) ); assert_ne!( original, kaptan_config::validation::__schema_id(&[kaptan_config::FieldDescriptor { nonzero: true, ..descriptor }]) ); }