refactor(config): generate scalar validation boundaries
This commit is contained in:
1 parent
afafd126d7
commit
8134235f48
29 files changed
+1008
-480
No files matched your search
@@ -2,7 +2,7 @@ use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::Result;
|
||||
|
||||
use crate::parse::{Check, Model};
|
||||
use crate::parse::{Check, Model, ModelKind};
|
||||
|
||||
pub(crate) fn expand(model: Model) -> Result<TokenStream> {
|
||||
let Model {
|
||||
@@ -10,26 +10,77 @@ pub(crate) fn expand(model: Model) -> Result<TokenStream> {
|
||||
generics,
|
||||
crate_path,
|
||||
post_validate,
|
||||
kind,
|
||||
fields,
|
||||
} = model;
|
||||
match kind {
|
||||
ModelKind::Scalars => expand_scalars(ident, generics, crate_path, post_validate, fields),
|
||||
ModelKind::Sections => expand_sections(ident, generics, crate_path, post_validate, fields),
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_scalars(
|
||||
ident: syn::Ident,
|
||||
generics: syn::Generics,
|
||||
crate_path: syn::Path,
|
||||
post_validate: Option<syn::Path>,
|
||||
fields: Vec<crate::parse::FieldModel>,
|
||||
) -> Result<TokenStream> {
|
||||
let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
|
||||
let mut checks = Vec::new();
|
||||
let mut descriptors = Vec::new();
|
||||
for field in fields {
|
||||
let mut defaults = Vec::new();
|
||||
let mut reads = Vec::new();
|
||||
let mut writes = Vec::new();
|
||||
for field in &fields {
|
||||
let FieldMetadata {
|
||||
descriptor,
|
||||
checks: field_checks,
|
||||
} = expand_field(&crate_path, &field)?;
|
||||
} = expand_field(&crate_path, field)?;
|
||||
if let Some(descriptor) = descriptor {
|
||||
descriptors.push(descriptor);
|
||||
}
|
||||
checks.extend(field_checks);
|
||||
let id = field.id.expect("ConfigValidate requires configured fields");
|
||||
let default = field
|
||||
.default
|
||||
.as_ref()
|
||||
.expect("ConfigValidate requires defaults");
|
||||
let (kind, value) = config_value_tokens(&crate_path, &field.ty)?;
|
||||
let field_ident = &field.ident;
|
||||
defaults.push(quote! { #field_ident: #default });
|
||||
reads.push(quote! {
|
||||
#id => Some(#value(self.#field_ident)),
|
||||
});
|
||||
writes.push(quote! {
|
||||
#id => {
|
||||
let next = match value {
|
||||
#value(next) => next,
|
||||
actual => {
|
||||
return Err(#crate_path::FieldWriteError::WrongType {
|
||||
field: #crate_path::FieldId::new(#id),
|
||||
expected: #kind,
|
||||
actual: actual.kind(),
|
||||
});
|
||||
}
|
||||
};
|
||||
let previous = self.#field_ident;
|
||||
self.#field_ident = next;
|
||||
if let Err(violation) =
|
||||
<Self as #crate_path::ConfigSection>::validate_local_fields(self)
|
||||
{
|
||||
self.#field_ident = previous;
|
||||
return Err(#crate_path::FieldWriteError::Violation(violation));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
}
|
||||
let post_validate = post_validate.map(|hook| quote! { #hook(self)?; });
|
||||
Ok(quote! {
|
||||
impl #impl_generics #crate_path::ValidateFields for #ident #type_generics #where_clause {
|
||||
fn validate_fields(&self) -> Result<(), #crate_path::ConfigViolation> {
|
||||
#(#checks)*
|
||||
<Self as #crate_path::ConfigSection>::validate_local_fields(self)?;
|
||||
#post_validate
|
||||
Ok(())
|
||||
}
|
||||
@@ -41,6 +92,133 @@ pub(crate) fn expand(model: Model) -> Result<TokenStream> {
|
||||
#(#descriptors),*
|
||||
];
|
||||
}
|
||||
|
||||
impl #impl_generics #crate_path::ConfigSection for #ident #type_generics #where_clause {
|
||||
const FIELD_COUNT: usize = Self::DESCRIPTORS.len();
|
||||
|
||||
fn default_section() -> Result<Self, #crate_path::ConfigViolation> {
|
||||
let section = Self {
|
||||
#(#defaults),*
|
||||
};
|
||||
<Self as #crate_path::ValidateFields>::validate_fields(§ion)?;
|
||||
Ok(section)
|
||||
}
|
||||
|
||||
fn field_value(&self, id: #crate_path::FieldId) -> Option<#crate_path::ConfigValue> {
|
||||
match id.get() {
|
||||
#(#reads)*
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_field(
|
||||
&mut self,
|
||||
id: #crate_path::FieldId,
|
||||
value: #crate_path::ConfigValue,
|
||||
) -> Result<(), #crate_path::FieldWriteError> {
|
||||
match id.get() {
|
||||
#(#writes,)*
|
||||
_ => Err(#crate_path::FieldWriteError::UnknownField(id)),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_local_fields(&self) -> Result<(), #crate_path::ConfigViolation> {
|
||||
#(#checks)*
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn expand_sections(
|
||||
ident: syn::Ident,
|
||||
generics: syn::Generics,
|
||||
crate_path: syn::Path,
|
||||
post_validate: Option<syn::Path>,
|
||||
fields: Vec<crate::parse::FieldModel>,
|
||||
) -> Result<TokenStream> {
|
||||
let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
|
||||
let validations = fields.iter().map(|field| {
|
||||
let field_ident = &field.ident;
|
||||
let ty = &field.ty;
|
||||
quote! {
|
||||
<#ty as #crate_path::ValidateFields>::validate_fields(&self.#field_ident)?;
|
||||
}
|
||||
});
|
||||
let defaults = fields.iter().map(|field| {
|
||||
let field_ident = &field.ident;
|
||||
let ty = &field.ty;
|
||||
quote! {
|
||||
#field_ident: <#ty as #crate_path::ConfigSection>::default_section()?
|
||||
}
|
||||
});
|
||||
let reads = fields.iter().map(|field| {
|
||||
let field_ident = &field.ident;
|
||||
let ty = &field.ty;
|
||||
quote! {
|
||||
if let Some(value) = <#ty as #crate_path::ConfigSection>::field_value(
|
||||
&self.#field_ident,
|
||||
id,
|
||||
) {
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
let writes = fields.iter().map(|field| {
|
||||
let field_ident = &field.ident;
|
||||
let ty = &field.ty;
|
||||
quote! {
|
||||
if <#ty as #crate_path::ConfigSection>::field_value(&self.#field_ident, id).is_some() {
|
||||
return <#ty as #crate_path::ConfigSection>::set_field(
|
||||
&mut self.#field_ident,
|
||||
id,
|
||||
value,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
let counts = fields.iter().map(|field| {
|
||||
let ty = &field.ty;
|
||||
quote! { <#ty as #crate_path::ConfigSection>::FIELD_COUNT }
|
||||
});
|
||||
let post_validate = post_validate.map(|hook| quote! { #hook(self)?; });
|
||||
Ok(quote! {
|
||||
impl #impl_generics #crate_path::ValidateFields for #ident #type_generics #where_clause {
|
||||
fn validate_fields(&self) -> Result<(), #crate_path::ConfigViolation> {
|
||||
#(#validations)*
|
||||
#post_validate
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl #impl_generics #ident #type_generics #where_clause {
|
||||
/// Number of fields aggregated from derived nested sections.
|
||||
pub const FIELD_COUNT: usize = 0 #(+ #counts)*;
|
||||
}
|
||||
|
||||
impl #impl_generics #crate_path::ConfigAggregate for #ident #type_generics #where_clause {
|
||||
fn default_aggregate() -> Result<Self, #crate_path::ConfigViolation> {
|
||||
let aggregate = Self {
|
||||
#(#defaults),*
|
||||
};
|
||||
<Self as #crate_path::ValidateFields>::validate_fields(&aggregate)?;
|
||||
Ok(aggregate)
|
||||
}
|
||||
|
||||
fn field_value(&self, id: #crate_path::FieldId) -> Option<#crate_path::ConfigValue> {
|
||||
#(#reads)*
|
||||
None
|
||||
}
|
||||
|
||||
fn set_field(
|
||||
&mut self,
|
||||
id: #crate_path::FieldId,
|
||||
value: #crate_path::ConfigValue,
|
||||
) -> Result<(), #crate_path::FieldWriteError> {
|
||||
#(#writes)*
|
||||
Err(#crate_path::FieldWriteError::UnknownField(id))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,16 @@ pub(crate) struct Model {
|
||||
pub(crate) generics: syn::Generics,
|
||||
pub(crate) crate_path: Path,
|
||||
pub(crate) post_validate: Option<Path>,
|
||||
pub(crate) kind: ModelKind,
|
||||
pub(crate) fields: Vec<FieldModel>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub(crate) enum ModelKind {
|
||||
Scalars,
|
||||
Sections,
|
||||
}
|
||||
|
||||
pub(crate) struct FieldModel {
|
||||
pub(crate) ident: Ident,
|
||||
pub(crate) ty: Type,
|
||||
@@ -19,6 +26,7 @@ pub(crate) struct FieldModel {
|
||||
pub(crate) unit: Option<syn::LitStr>,
|
||||
pub(crate) default: Option<Expr>,
|
||||
pub(crate) checks: Vec<Check>,
|
||||
pub(crate) section: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -44,6 +52,12 @@ impl Model {
|
||||
"ConfigValidate requires named struct fields",
|
||||
));
|
||||
};
|
||||
if fields.named.is_empty() {
|
||||
return Err(syn::Error::new(
|
||||
input.ident.span(),
|
||||
"ConfigValidate requires at least one configured field or section",
|
||||
));
|
||||
}
|
||||
let mut models = Vec::with_capacity(fields.named.len());
|
||||
let mut ids = Vec::new();
|
||||
for field in fields.named {
|
||||
@@ -59,8 +73,22 @@ impl Model {
|
||||
}
|
||||
models.push(model);
|
||||
}
|
||||
for field in &models {
|
||||
if field.id.is_some() {
|
||||
let kind = if models.iter().all(|field| field.section) {
|
||||
ModelKind::Sections
|
||||
} else if models.iter().all(|field| field.id.is_some()) {
|
||||
ModelKind::Scalars
|
||||
} else {
|
||||
let field = models
|
||||
.iter()
|
||||
.find(|field| field.section || field.id.is_none())
|
||||
.expect("mixed configuration model has one invalid field");
|
||||
return Err(syn::Error::new(
|
||||
field.ident.span(),
|
||||
"ConfigValidate records must contain either scalar config fields or config(section) fields exclusively",
|
||||
));
|
||||
};
|
||||
if kind == ModelKind::Scalars {
|
||||
for field in &models {
|
||||
if field.display_key.is_none() {
|
||||
return Err(syn::Error::new(
|
||||
field.ident.span(),
|
||||
@@ -79,6 +107,7 @@ impl Model {
|
||||
"configuration fields require config(default = ...)",
|
||||
));
|
||||
}
|
||||
validate_static_constraints(field)?;
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
@@ -86,6 +115,7 @@ impl Model {
|
||||
generics: input.generics,
|
||||
crate_path,
|
||||
post_validate,
|
||||
kind,
|
||||
fields: models,
|
||||
})
|
||||
}
|
||||
@@ -135,12 +165,35 @@ fn parse_field(field: Field) -> Result<FieldModel> {
|
||||
let mut nonzero = false;
|
||||
let mut range_minimum = false;
|
||||
let mut range_maximum = false;
|
||||
let mut section = false;
|
||||
for attribute in field
|
||||
.attrs
|
||||
.iter()
|
||||
.filter(|attribute| attribute.path().is_ident("config"))
|
||||
{
|
||||
attribute.parse_nested_meta(|meta| {
|
||||
if section && !meta.path.is_ident("section") {
|
||||
return Err(meta.error(
|
||||
"config(section) cannot be combined with scalar id, metadata, or validation rules",
|
||||
));
|
||||
}
|
||||
if meta.path.is_ident("section") {
|
||||
if section {
|
||||
return Err(meta.error("duplicate config(section)"));
|
||||
}
|
||||
if id.is_some()
|
||||
|| display_key.is_some()
|
||||
|| unit.is_some()
|
||||
|| default.is_some()
|
||||
|| !checks.is_empty()
|
||||
{
|
||||
return Err(meta.error(
|
||||
"config(section) cannot be combined with scalar id, metadata, or validation rules",
|
||||
));
|
||||
}
|
||||
section = true;
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("id") {
|
||||
if id.is_some() {
|
||||
return Err(meta.error("duplicate configuration field id"));
|
||||
@@ -224,12 +277,24 @@ fn parse_field(field: Field) -> Result<FieldModel> {
|
||||
))
|
||||
})?;
|
||||
}
|
||||
if !checks.is_empty() && id.is_none() {
|
||||
if !section && !checks.is_empty() && id.is_none() {
|
||||
return Err(syn::Error::new(
|
||||
ident.span(),
|
||||
"validated configuration fields require config(id = N)",
|
||||
));
|
||||
}
|
||||
if section
|
||||
&& (id.is_some()
|
||||
|| display_key.is_some()
|
||||
|| unit.is_some()
|
||||
|| default.is_some()
|
||||
|| !checks.is_empty())
|
||||
{
|
||||
return Err(syn::Error::new(
|
||||
ident.span(),
|
||||
"config(section) cannot be combined with scalar id, metadata, or validation rules",
|
||||
));
|
||||
}
|
||||
Ok(FieldModel {
|
||||
ident,
|
||||
ty: field.ty,
|
||||
@@ -238,6 +303,7 @@ fn parse_field(field: Field) -> Result<FieldModel> {
|
||||
unit,
|
||||
default,
|
||||
checks,
|
||||
section,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -300,6 +366,276 @@ fn validate_default_literal(expression: &Expr, ty: &Type) -> Result<()> {
|
||||
validate_numeric_literal(expression)
|
||||
}
|
||||
|
||||
fn validate_static_constraints(field: &FieldModel) -> Result<()> {
|
||||
let ty = scalar_type_name(&field.ty).ok_or_else(|| {
|
||||
syn::Error::new(
|
||||
field.ty.span(),
|
||||
"configuration metadata requires a supported scalar type",
|
||||
)
|
||||
})?;
|
||||
if ty == "bool" {
|
||||
return Ok(());
|
||||
}
|
||||
let default = scalar_literal(
|
||||
field.default.as_ref().expect("scalar fields have defaults"),
|
||||
ty,
|
||||
)?;
|
||||
let minimum = field.checks.iter().find_map(|check| match check {
|
||||
Check::Minimum(value) => Some(value),
|
||||
_ => None,
|
||||
});
|
||||
let maximum = field.checks.iter().find_map(|check| match check {
|
||||
Check::Maximum(value) => Some(value),
|
||||
_ => None,
|
||||
});
|
||||
let minimum = minimum.map(|value| scalar_literal(value, ty)).transpose()?;
|
||||
let maximum = maximum.map(|value| scalar_literal(value, ty)).transpose()?;
|
||||
if minimum.is_some_and(|value| !value.is_finite())
|
||||
|| maximum.is_some_and(|value| !value.is_finite())
|
||||
{
|
||||
return Err(syn::Error::new(
|
||||
field.ident.span(),
|
||||
"configuration range bounds must be finite",
|
||||
));
|
||||
}
|
||||
if let (Some(minimum), Some(maximum)) = (minimum, maximum)
|
||||
&& compare_scalar(minimum, maximum).is_gt()
|
||||
{
|
||||
return Err(syn::Error::new(
|
||||
field.ident.span(),
|
||||
"configuration range minimum exceeds maximum",
|
||||
));
|
||||
}
|
||||
if field
|
||||
.checks
|
||||
.iter()
|
||||
.any(|check| matches!(check, Check::Finite))
|
||||
&& !default.is_finite()
|
||||
{
|
||||
return Err(syn::Error::new(
|
||||
field
|
||||
.default
|
||||
.as_ref()
|
||||
.expect("scalar fields have defaults")
|
||||
.span(),
|
||||
"configuration default violates finite",
|
||||
));
|
||||
}
|
||||
if !default.is_finite() {
|
||||
return Err(syn::Error::new(
|
||||
field
|
||||
.default
|
||||
.as_ref()
|
||||
.expect("scalar fields have defaults")
|
||||
.span(),
|
||||
"configuration default is not finite for its field type",
|
||||
));
|
||||
}
|
||||
if field
|
||||
.checks
|
||||
.iter()
|
||||
.any(|check| matches!(check, Check::Nonzero))
|
||||
&& default.is_zero()
|
||||
{
|
||||
return Err(syn::Error::new(
|
||||
field
|
||||
.default
|
||||
.as_ref()
|
||||
.expect("scalar fields have defaults")
|
||||
.span(),
|
||||
"configuration default violates nonzero",
|
||||
));
|
||||
}
|
||||
if let Some(minimum) = minimum
|
||||
&& compare_scalar(default, minimum).is_lt()
|
||||
{
|
||||
return Err(syn::Error::new(
|
||||
field
|
||||
.default
|
||||
.as_ref()
|
||||
.expect("scalar fields have defaults")
|
||||
.span(),
|
||||
"configuration default is below its declared minimum",
|
||||
));
|
||||
}
|
||||
if let Some(maximum) = maximum
|
||||
&& compare_scalar(default, maximum).is_gt()
|
||||
{
|
||||
return Err(syn::Error::new(
|
||||
field
|
||||
.default
|
||||
.as_ref()
|
||||
.expect("scalar fields have defaults")
|
||||
.span(),
|
||||
"configuration default is above its declared maximum",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ScalarLiteral {
|
||||
Signed(i128),
|
||||
Unsigned(u128),
|
||||
Float(f64),
|
||||
}
|
||||
|
||||
impl ScalarLiteral {
|
||||
fn is_finite(self) -> bool {
|
||||
match self {
|
||||
Self::Signed(_) | Self::Unsigned(_) => true,
|
||||
Self::Float(value) => value.is_finite(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_zero(self) -> bool {
|
||||
match self {
|
||||
Self::Signed(value) => value == 0,
|
||||
Self::Unsigned(value) => value == 0,
|
||||
Self::Float(value) => value == 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scalar_literal(expression: &Expr, ty: &str) -> Result<ScalarLiteral> {
|
||||
match ty {
|
||||
"i32" => {
|
||||
let value = signed_literal(expression)?;
|
||||
i32::try_from(value).map_err(|_| {
|
||||
syn::Error::new(
|
||||
expression.span(),
|
||||
"configuration literal is outside i32 range",
|
||||
)
|
||||
})?;
|
||||
Ok(ScalarLiteral::Signed(value))
|
||||
}
|
||||
"u32" => {
|
||||
let value = unsigned_literal(expression)?;
|
||||
u32::try_from(value).map_err(|_| {
|
||||
syn::Error::new(
|
||||
expression.span(),
|
||||
"configuration literal is outside u32 range",
|
||||
)
|
||||
})?;
|
||||
Ok(ScalarLiteral::Unsigned(value))
|
||||
}
|
||||
"u64" => {
|
||||
let value = unsigned_literal(expression)?;
|
||||
u64::try_from(value).map_err(|_| {
|
||||
syn::Error::new(
|
||||
expression.span(),
|
||||
"configuration literal is outside u64 range",
|
||||
)
|
||||
})?;
|
||||
Ok(ScalarLiteral::Unsigned(value))
|
||||
}
|
||||
"f32" => Ok(ScalarLiteral::Float(f64::from(
|
||||
float_literal(expression)? as f32
|
||||
))),
|
||||
"f64" => float_literal(expression).map(ScalarLiteral::Float),
|
||||
_ => Err(syn::Error::new(
|
||||
expression.span(),
|
||||
"configuration metadata supports bool, i32, u32, u64, f32, and f64",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn signed_literal(expression: &Expr) -> Result<i128> {
|
||||
match expression {
|
||||
Expr::Lit(expression) => match &expression.lit {
|
||||
Lit::Int(value) => value.base10_parse(),
|
||||
_ => Err(syn::Error::new(
|
||||
expression.span(),
|
||||
"expected an integer literal",
|
||||
)),
|
||||
},
|
||||
Expr::Unary(expression) if matches!(expression.op, UnOp::Neg(_)) => {
|
||||
let Expr::Lit(value) = expression.expr.as_ref() else {
|
||||
return Err(syn::Error::new(
|
||||
expression.span(),
|
||||
"expected an integer literal",
|
||||
));
|
||||
};
|
||||
let Lit::Int(value) = &value.lit else {
|
||||
return Err(syn::Error::new(value.span(), "expected an integer literal"));
|
||||
};
|
||||
value
|
||||
.base10_parse::<i128>()
|
||||
.map(|value| -value)
|
||||
.map_err(|error| syn::Error::new(value.span(), error))
|
||||
}
|
||||
_ => Err(syn::Error::new(
|
||||
expression.span(),
|
||||
"expected an integer literal",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn unsigned_literal(expression: &Expr) -> Result<u128> {
|
||||
match expression {
|
||||
Expr::Lit(expression) => match &expression.lit {
|
||||
Lit::Int(value) => value
|
||||
.base10_parse()
|
||||
.map_err(|error| syn::Error::new(value.span(), error)),
|
||||
_ => Err(syn::Error::new(
|
||||
expression.span(),
|
||||
"expected an unsigned integer literal",
|
||||
)),
|
||||
},
|
||||
_ => Err(syn::Error::new(
|
||||
expression.span(),
|
||||
"expected an unsigned integer literal",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn float_literal(expression: &Expr) -> Result<f64> {
|
||||
let (negative, literal) = match expression {
|
||||
Expr::Lit(expression) => (false, &expression.lit),
|
||||
Expr::Unary(expression) if matches!(expression.op, UnOp::Neg(_)) => {
|
||||
let Expr::Lit(value) = expression.expr.as_ref() else {
|
||||
return Err(syn::Error::new(
|
||||
expression.span(),
|
||||
"expected a float literal",
|
||||
));
|
||||
};
|
||||
(true, &value.lit)
|
||||
}
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
expression.span(),
|
||||
"expected a float literal",
|
||||
));
|
||||
}
|
||||
};
|
||||
let value = match literal {
|
||||
Lit::Int(value) => value
|
||||
.base10_parse::<f64>()
|
||||
.map_err(|error| syn::Error::new(value.span(), error))?,
|
||||
Lit::Float(value) => value
|
||||
.base10_parse::<f64>()
|
||||
.map_err(|error| syn::Error::new(value.span(), error))?,
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
expression.span(),
|
||||
"expected a float literal",
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(if negative { -value } else { value })
|
||||
}
|
||||
|
||||
fn compare_scalar(left: ScalarLiteral, right: ScalarLiteral) -> core::cmp::Ordering {
|
||||
match (left, right) {
|
||||
(ScalarLiteral::Signed(left), ScalarLiteral::Signed(right)) => left.cmp(&right),
|
||||
(ScalarLiteral::Unsigned(left), ScalarLiteral::Unsigned(right)) => left.cmp(&right),
|
||||
(ScalarLiteral::Float(left), ScalarLiteral::Float(right)) => {
|
||||
left.partial_cmp(&right).expect("finite literals compare")
|
||||
}
|
||||
_ => unreachable!("field literals share their scalar type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn require_float(ty: &Type, span: &impl Spanned) -> Result<()> {
|
||||
if scalar_type_name(ty).is_some_and(|name| name == "f32" || name == "f64") {
|
||||
Ok(())
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
@@ -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,
|
||||
| ^^^^^
|
||||
Reference in new issue
Block a user