refactor(config): generate scalar validation boundaries

This commit is contained in:
Onur Özkan committed 2026-08-09 22:45:38 +03:00
1 parent afafd126d7
commit 8134235f48
29 files changed
+1008 -480

No files matched your search

@@ -0,0 +1,27 @@
use kaptan_config::{ConfigAggregate, ConfigSection, ConfigViolation, ValidateFields};
use kaptan_config_derive::ConfigValidate;
#[derive(ConfigValidate)]
struct Child {
#[config(id = 1, key = "child.value", unit = "count", default = 1, range(min = 0, max = 2))]
value: u32,
}
#[derive(ConfigValidate)]
#[config(post = Self::validate_complete)]
struct Root {
#[config(section)]
child: Child,
}
impl Root {
fn validate_complete(&self) -> Result<(), ConfigViolation> {
self.child.validate_fields()
}
}
fn main() {
let root = Root::default_aggregate().unwrap();
assert_eq!(Root::FIELD_COUNT, 1);
assert_eq!(root.child.field_value(kaptan_config::FieldId::new(1)), Some(kaptan_config::ConfigValue::U32(1)));
}
+119
View File
@@ -0,0 +1,119 @@
use kaptan_config::{
ConfigAggregate, ConfigSection, ConfigValue, ConfigViolation, FieldId, FieldWriteError,
ValidateFields, ValidationRule,
};
use kaptan_config_derive::ConfigValidate;
#[derive(ConfigValidate)]
struct LocalSection {
#[config(
id = 1,
key = "local.first",
unit = "count",
default = 2,
range(min = 1, max = 9)
)]
first: u32,
#[config(
id = 2,
key = "local.second",
unit = "count",
default = 4,
range(min = 1, max = 9)
)]
second: u32,
}
#[derive(ConfigValidate)]
#[config(post = Self::validate_order)]
struct OrderedSection {
#[config(
id = 3,
key = "ordered.minimum",
unit = "count",
default = 1,
range(min = 0, max = 9)
)]
minimum: u32,
#[config(
id = 4,
key = "ordered.maximum",
unit = "count",
default = 2,
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(ConfigValidate)]
struct Aggregate {
#[config(section)]
local: LocalSection,
#[config(section)]
ordered: OrderedSection,
}
#[test]
fn generated_section_owns_every_declared_scalar_path() {
let mut section = LocalSection::default_section().unwrap();
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 aggregate_routes_writes_and_defers_declared_post_validation() {
let mut aggregate = Aggregate::default_aggregate().unwrap();
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_fields(),
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_fields().unwrap();
}
@@ -0,0 +1,9 @@
use kaptan_config_derive::ConfigValidate;
#[derive(ConfigValidate)]
struct DefaultBelowMinimum {
#[config(id = 1, key = "value", unit = "count", default = 1, range(min = 2, max = 3))]
value: u32,
}
fn main() {}
@@ -0,0 +1,5 @@
error: configuration default is below its declared minimum
--> tests/ui/default_below_minimum.rs:5:63
|
5 | #[config(id = 1, key = "value", unit = "count", default = 1, range(min = 2, max = 3))]
| ^
@@ -0,0 +1,9 @@
use kaptan_config_derive::ConfigValidate;
#[derive(ConfigValidate)]
struct NonFiniteDefault {
#[config(id = 1, key = "value", unit = "ratio", default = 1e100, finite)]
value: f32,
}
fn main() {}
@@ -0,0 +1,5 @@
error: configuration default violates finite
--> tests/ui/default_violates_finite.rs:5:63
|
5 | #[config(id = 1, key = "value", unit = "ratio", default = 1e100, finite)]
| ^^^^^
@@ -0,0 +1,9 @@
use kaptan_config_derive::ConfigValidate;
#[derive(ConfigValidate)]
struct ZeroDefault {
#[config(id = 1, key = "value", unit = "count", default = 0, nonzero)]
value: u32,
}
fn main() {}
@@ -0,0 +1,5 @@
error: configuration default violates nonzero
--> tests/ui/default_violates_nonzero.rs:5:63
|
5 | #[config(id = 1, key = "value", unit = "count", default = 0, nonzero)]
| ^
@@ -0,0 +1,6 @@
use kaptan_config_derive::ConfigValidate;
#[derive(ConfigValidate)]
struct Empty {}
fn main() {}
@@ -0,0 +1,5 @@
error: ConfigValidate requires at least one configured field or section
--> tests/ui/empty.rs:4:8
|
4 | struct Empty {}
| ^^^^^
@@ -0,0 +1,11 @@
use kaptan_config_derive::ConfigValidate;
#[derive(ConfigValidate)]
struct MixedSectionAndScalar {
#[config(section)]
child: u32,
#[config(id = 1, key = "scalar", unit = "count", default = 1, range(min = 0, max = 2))]
scalar: u32,
}
fn main() {}
@@ -0,0 +1,5 @@
error: ConfigValidate records must contain either scalar config fields or config(section) fields exclusively
--> tests/ui/mixed_section_scalar.rs:6:5
|
6 | child: u32,
| ^^^^^
@@ -0,0 +1,9 @@
use kaptan_config_derive::ConfigValidate;
#[derive(ConfigValidate)]
struct InvalidRange {
#[config(id = 1, key = "value", unit = "count", default = 2, range(min = 3, max = 1))]
value: u32,
}
fn main() {}
@@ -0,0 +1,5 @@
error: configuration range minimum exceeds maximum
--> tests/ui/range_minimum_exceeds_maximum.rs:6:5
|
6 | value: u32,
| ^^^^^