use syn::{ Attribute, Data, DeriveInput, Expr, Field, Fields, Ident, Lit, Path, Result, Type, UnOp, spanned::Spanned, }; const MAX_DISPLAY_KEY_BYTES: usize = 48; const MAX_UNIT_BYTES: usize = 16; 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) checks: Vec, pub(crate) enum_backed: 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(), "Config can only be derived for structs", )); }; let Fields::Named(fields) = data.fields else { return Err(syn::Error::new( input.ident.span(), "Config requires named struct fields", )); }; if fields.named.is_empty() { return Err(syn::Error::new( input.ident.span(), "Config requires at least one field", )); } 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.id.is_some()) { ModelKind::Scalars } else if models.iter().all(|field| field.id.is_none()) { ModelKind::Sections } else { let field = models .iter() .find(|field| field.id.is_none()) .expect("mixed configuration model has one invalid field"); return Err(syn::Error::new( field.ident.span(), "Config records must contain either scalar fields or child configuration records exclusively", )); }; if kind == ModelKind::Scalars { let mut display_keys = Vec::with_capacity(models.len()); for field in &models { let Some(display_key) = &field.display_key else { return Err(syn::Error::new( field.ident.span(), "configuration fields require config(key = \"...\")", )); }; validate_metadata_text( display_key, MAX_DISPLAY_KEY_BYTES, "configuration display key", false, )?; if display_keys .iter() .any(|previous: &&syn::LitStr| previous.value() == display_key.value()) { return Err(syn::Error::new( display_key.span(), "duplicate configuration display key", )); } display_keys.push(display_key); let Some(unit) = &field.unit else { return Err(syn::Error::new( field.ident.span(), "configuration fields require config(unit = \"...\")", )); }; validate_metadata_text(unit, MAX_UNIT_BYTES, "configuration unit", true)?; let primitive = scalar_type_name(&field.ty).is_some(); if field.enum_backed && primitive { return Err(syn::Error::new( field.ty.span(), "config(enum) requires a ConfigEnum-derived enum type", )); } if !field.enum_backed && !primitive { return Err(syn::Error::new( field.ty.span(), "non-primitive configuration fields require config(enum)", )); } validate_static_constraints(field)?; } } Ok(Self { ident: input.ident, generics: input.generics, crate_path, post_validate, kind, fields: models, }) } } fn validate_metadata_text( literal: &syn::LitStr, maximum_bytes: usize, name: &str, allow_empty: bool, ) -> Result<()> { let value = literal.value(); if !allow_empty && value.is_empty() { return Err(syn::Error::new( literal.span(), format!("{name} must not be empty"), )); } if value.contains('\0') { return Err(syn::Error::new( literal.span(), format!("{name} cannot contain NUL bytes"), )); } if value.len() > maximum_bytes { return Err(syn::Error::new( literal.span(), format!("{name} exceeds the {maximum_bytes}-byte wire limit"), )); } Ok(()) } 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 finite = false; let mut nonzero = false; let mut range_minimum = false; let mut range_maximum = false; let mut enum_backed = false; for attribute in field .attrs .iter() .filter(|attribute| attribute.path().is_ident("config")) { attribute.parse_nested_meta(|meta| { 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("enum") { if enum_backed { return Err(meta.error("duplicate enum configuration marker")); } enum_backed = true; 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, enum, finite, nonzero, and range", )) })?; } if id.is_none() && (display_key.is_some() || unit.is_some() || enum_backed || !checks.is_empty()) { return Err(syn::Error::new( ident.span(), "scalar configuration fields require config(id = N)", )); } Ok(FieldModel { ident, ty: field.ty, id, display_key, unit, checks, enum_backed, }) } 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_static_constraints(field: &FieldModel) -> Result<()> { let Some(ty) = scalar_type_name(&field.ty) else { if field.checks.is_empty() { return Ok(()); } return Err(syn::Error::new( field.ty.span(), "validation rules require a supported primitive scalar type", )); }; if ty == "bool" { return Ok(()); } 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", )); } 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 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)) } "u8" => { let value = unsigned_literal(expression)?; u8::try_from(value).map_err(|_| { syn::Error::new( expression.span(), "configuration literal is outside u8 range", ) })?; Ok(ScalarLiteral::Unsigned(value)) } "u16" => { let value = unsigned_literal(expression)?; u16::try_from(value).map_err(|_| { syn::Error::new( expression.span(), "configuration literal is outside u16 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, u8, u16, 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, }) }