use syn::{ Attribute, Data, DeriveInput, Expr, Field, Fields, Ident, Lit, Path, Result, Type, UnOp, spanned::Spanned, }; pub(crate) struct Model { pub(crate) ident: Ident, pub(crate) generics: syn::Generics, pub(crate) crate_path: Path, pub(crate) post_validate: Option, pub(crate) kind: ModelKind, pub(crate) fields: Vec, } #[derive(Clone, Copy, Eq, PartialEq)] pub(crate) enum ModelKind { Scalars, Sections, } pub(crate) struct FieldModel { pub(crate) ident: Ident, pub(crate) ty: Type, pub(crate) id: Option, pub(crate) display_key: Option, pub(crate) unit: Option, pub(crate) default: Option, pub(crate) checks: Vec, pub(crate) section: bool, } #[derive(Clone)] pub(crate) enum Check { Finite, Nonzero, Minimum(Expr), Maximum(Expr), } impl Model { pub(crate) fn parse(input: DeriveInput) -> Result { let (crate_path, post_validate) = parse_container_attributes(&input.attrs)?; let Data::Struct(data) = input.data else { return Err(syn::Error::new( input.ident.span(), "ConfigValidate can only be derived for structs", )); }; let Fields::Named(fields) = data.fields else { return Err(syn::Error::new( input.ident.span(), "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 { let model = parse_field(field)?; if let Some(id) = model.id { if ids.contains(&id) { return Err(syn::Error::new( model.ident.span(), "duplicate configuration field id", )); } ids.push(id); } models.push(model); } 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(), "configuration fields require config(key = \"...\")", )); } if field.unit.is_none() { return Err(syn::Error::new( field.ident.span(), "configuration fields require config(unit = \"...\")", )); } if field.default.is_none() { return Err(syn::Error::new( field.ident.span(), "configuration fields require config(default = ...)", )); } validate_static_constraints(field)?; } } Ok(Self { ident: input.ident, generics: input.generics, crate_path, post_validate, kind, fields: models, }) } } fn parse_container_attributes(attributes: &[Attribute]) -> Result<(Path, Option)> { let mut crate_path = syn::parse_quote!(::kaptan_config); let mut has_crate_path = false; let mut post_validate = None; for attribute in attributes .iter() .filter(|attribute| attribute.path().is_ident("config")) { attribute.parse_nested_meta(|meta| { if meta.path.is_ident("crate") { if has_crate_path { return Err(meta.error("duplicate crate path override")); } crate_path = meta.value()?.parse()?; has_crate_path = true; return Ok(()); } if meta.path.is_ident("post") { if post_validate.is_some() { return Err(meta.error("duplicate post validation hook")); } post_validate = Some(meta.value()?.parse()?); return Ok(()); } Err(meta.error("struct config options are crate = path and post = path")) })?; } Ok((crate_path, post_validate)) } fn parse_field(field: Field) -> Result { let ident = field .ident .clone() .ok_or_else(|| syn::Error::new(field.span(), "configuration fields must be named"))?; let mut checks = Vec::new(); let mut id = None; let mut display_key = None; let mut unit = None; let mut default = None; let mut finite = false; 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")); } let literal: syn::LitInt = meta.value()?.parse()?; let parsed = literal.base10_parse::()?; if parsed == 0 || parsed == u16::MAX { return Err(meta.error("configuration field id is reserved")); } id = Some(parsed); return Ok(()); } if meta.path.is_ident("key") { if display_key.is_some() { return Err(meta.error("duplicate configuration display key")); } display_key = Some(meta.value()?.parse()?); return Ok(()); } if meta.path.is_ident("unit") { if unit.is_some() { return Err(meta.error("duplicate configuration unit")); } unit = Some(meta.value()?.parse()?); return Ok(()); } if meta.path.is_ident("default") { if default.is_some() { return Err(meta.error("duplicate configuration default")); } let expression = meta.value()?.parse()?; validate_default_literal(&expression, &field.ty)?; default = Some(expression); return Ok(()); } if meta.path.is_ident("finite") { if finite { return Err(meta.error("duplicate finite validation rule")); } require_float(&field.ty, &meta.path)?; checks.push(Check::Finite); finite = true; return Ok(()); } if meta.path.is_ident("nonzero") { if nonzero { return Err(meta.error("duplicate nonzero validation rule")); } require_numeric(&field.ty, &meta.path)?; checks.push(Check::Nonzero); nonzero = true; return Ok(()); } if meta.path.is_ident("range") { require_numeric(&field.ty, &meta.path)?; let (minimum, maximum) = parse_range(meta)?; if minimum.is_none() && maximum.is_none() { return Err(syn::Error::new( field.span(), "range requires min and/or max", )); } if let Some(minimum) = minimum { if range_minimum { return Err(syn::Error::new(field.span(), "duplicate range minimum")); } checks.push(Check::Minimum(minimum)); range_minimum = true; } if let Some(maximum) = maximum { if range_maximum { return Err(syn::Error::new(field.span(), "duplicate range maximum")); } checks.push(Check::Maximum(maximum)); range_maximum = true; } return Ok(()); } Err(meta.error( "supported field config rules are id, key, unit, default, finite, nonzero, and range", )) })?; } 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, id, display_key, unit, default, checks, section, }) } fn parse_range(meta: syn::meta::ParseNestedMeta<'_>) -> Result<(Option, Option)> { let mut minimum = None; let mut maximum = None; meta.parse_nested_meta(|nested| { if nested.path.is_ident("min") { if minimum.is_some() { return Err(nested.error("duplicate range minimum")); } let expression = nested.value()?.parse()?; validate_numeric_literal(&expression)?; minimum = Some(expression); return Ok(()); } if nested.path.is_ident("max") { if maximum.is_some() { return Err(nested.error("duplicate range maximum")); } let expression = nested.value()?.parse()?; validate_numeric_literal(&expression)?; maximum = Some(expression); return Ok(()); } Err(nested.error("range accepts only min and max")) })?; Ok((minimum, maximum)) } fn validate_numeric_literal(expression: &Expr) -> Result<()> { match expression { Expr::Lit(expression) if matches!(expression.lit, Lit::Int(_) | Lit::Float(_)) => Ok(()), Expr::Unary(expression) if matches!(expression.op, UnOp::Neg(_)) && matches!( expression.expr.as_ref(), Expr::Lit(literal) if matches!(literal.lit, Lit::Int(_) | Lit::Float(_)) ) => { Ok(()) } _ => Err(syn::Error::new( expression.span(), "range bounds must be numeric literals", )), } } fn validate_default_literal(expression: &Expr, ty: &Type) -> Result<()> { if scalar_type_name(ty) == Some("bool") { return match expression { Expr::Lit(expression) if matches!(expression.lit, Lit::Bool(_)) => Ok(()), _ => Err(syn::Error::new( expression.span(), "bool configuration defaults must be bool literals", )), }; } 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 { 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 { 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::() .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 { 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 { 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::() .map_err(|error| syn::Error::new(value.span(), error))?, Lit::Float(value) => value .base10_parse::() .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(()) } else { Err(syn::Error::new( span.span(), "finite requires an f32 or f64 field", )) } } fn require_numeric(ty: &Type, span: &impl Spanned) -> Result<()> { if scalar_type_name(ty).is_some_and(|name| { matches!( name, "f32" | "f64" | "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128" | "usize" ) }) { Ok(()) } else { Err(syn::Error::new( span.span(), "nonzero and range require a primitive numeric field", )) } } fn scalar_type_name(ty: &Type) -> Option<&str> { let Type::Path(path) = ty else { return None; }; if path.qself.is_some() || path.path.segments.len() != 1 { return None; } let segment = path.path.segments.first()?; if !matches!(segment.arguments, syn::PathArguments::None) { return None; } Some(match &segment.ident { ident if ident == "bool" => "bool", ident if ident == "f32" => "f32", ident if ident == "f64" => "f64", ident if ident == "i8" => "i8", ident if ident == "i16" => "i16", ident if ident == "i32" => "i32", ident if ident == "i64" => "i64", ident if ident == "i128" => "i128", ident if ident == "isize" => "isize", ident if ident == "u8" => "u8", ident if ident == "u16" => "u16", ident if ident == "u32" => "u32", ident if ident == "u64" => "u64", ident if ident == "u128" => "u128", ident if ident == "usize" => "usize", _ => return None, }) }