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, } #[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, }; assert_eq!( section.field_value(FieldId::new(20)), Some(ConfigValue::U32(7)) ); assert_eq!( CompactSection::DESCRIPTORS[0].minimum, Some(ConfigValue::U32(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::U32(3)) .unwrap(); assert_eq!(section.mode, TestMode::Loud); assert!(matches!( section.set_field(FieldId::new(21), ConfigValue::U32(2)), Err(FieldWriteError::Violation(issue)) if issue.rule() == ValidationRule::InvalidEnumValue )); assert!(matches!( section.set_field(FieldId::new(20), ConfigValue::U32(256)), Err(FieldWriteError::Violation(issue)) if issue.rule() == ValidationRule::AboveMaximum )); } #[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() { 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 )); }