refactor: move firmware workspace under firmware
This commit is contained in:
1 parent
153b0bf262
commit
f03fc119a4
222 files changed
+56
-13
No files matched your search
@@ -0,0 +1,365 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::Result;
|
||||
|
||||
use crate::parse::{Check, Model, ModelKind};
|
||||
|
||||
pub(crate) fn expand(model: Model) -> Result<TokenStream> {
|
||||
let Model {
|
||||
ident,
|
||||
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();
|
||||
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)?;
|
||||
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> {
|
||||
<Self as #crate_path::ConfigSection>::validate_local_fields(self)?;
|
||||
#post_validate
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl #impl_generics #ident #type_generics #where_clause {
|
||||
/// Field metadata generated from the same attributes as validation.
|
||||
pub const DESCRIPTORS: &'static [#crate_path::FieldDescriptor] = &[
|
||||
#(#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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
struct FieldMetadata {
|
||||
descriptor: Option<TokenStream>,
|
||||
checks: Vec<TokenStream>,
|
||||
}
|
||||
|
||||
fn expand_field(crate_path: &syn::Path, field: &crate::parse::FieldModel) -> Result<FieldMetadata> {
|
||||
let mut checks = Vec::new();
|
||||
let Some(id) = field.id else {
|
||||
return Ok(FieldMetadata {
|
||||
descriptor: None,
|
||||
checks,
|
||||
});
|
||||
};
|
||||
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,
|
||||
});
|
||||
for check in &field.checks {
|
||||
checks.push(expand_check(crate_path, &field.ident, id, check.clone()));
|
||||
}
|
||||
let key = field.display_key.as_ref().expect("id fields have a key");
|
||||
let unit = field.unit.as_ref().expect("id fields have a unit");
|
||||
let default = field.default.as_ref().expect("id fields have a default");
|
||||
let (kind, value) = config_value_tokens(crate_path, &field.ty)?;
|
||||
let minimum = option_value_tokens(value.clone(), minimum);
|
||||
let maximum = option_value_tokens(value.clone(), maximum);
|
||||
Ok(FieldMetadata {
|
||||
descriptor: Some(quote! {
|
||||
#crate_path::FieldDescriptor {
|
||||
id: #crate_path::FieldId::new(#id),
|
||||
display_key: #key,
|
||||
kind: #kind,
|
||||
minimum: #minimum,
|
||||
maximum: #maximum,
|
||||
default: #value(#default),
|
||||
unit: #unit,
|
||||
apply_policy: #crate_path::ApplyPolicy::RestartRequired,
|
||||
}
|
||||
}),
|
||||
checks,
|
||||
})
|
||||
}
|
||||
|
||||
fn option_value_tokens(value: TokenStream, expression: Option<&syn::Expr>) -> TokenStream {
|
||||
match expression {
|
||||
Some(expression) => quote! { Some(#value(#expression)) },
|
||||
None => quote! { None },
|
||||
}
|
||||
}
|
||||
|
||||
fn config_value_tokens(
|
||||
crate_path: &syn::Path,
|
||||
ty: &syn::Type,
|
||||
) -> Result<(TokenStream, TokenStream)> {
|
||||
let syn::Type::Path(path) = ty else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
ty,
|
||||
"configuration metadata requires a scalar type",
|
||||
));
|
||||
};
|
||||
let Some(segment) = path.path.segments.last() else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
ty,
|
||||
"configuration metadata requires a scalar type",
|
||||
));
|
||||
};
|
||||
let (kind, value) = match segment.ident.to_string().as_str() {
|
||||
"bool" => (
|
||||
quote! { #crate_path::ValueKind::Bool },
|
||||
quote! { #crate_path::ConfigValue::Bool },
|
||||
),
|
||||
"i32" => (
|
||||
quote! { #crate_path::ValueKind::I32 },
|
||||
quote! { #crate_path::ConfigValue::I32 },
|
||||
),
|
||||
"u32" => (
|
||||
quote! { #crate_path::ValueKind::U32 },
|
||||
quote! { #crate_path::ConfigValue::U32 },
|
||||
),
|
||||
"u64" => (
|
||||
quote! { #crate_path::ValueKind::U64 },
|
||||
quote! { #crate_path::ConfigValue::U64 },
|
||||
),
|
||||
"f32" => (
|
||||
quote! { #crate_path::ValueKind::F32 },
|
||||
quote! { #crate_path::ConfigValue::F32 },
|
||||
),
|
||||
"f64" => (
|
||||
quote! { #crate_path::ValueKind::F64 },
|
||||
quote! { #crate_path::ConfigValue::F64 },
|
||||
),
|
||||
_ => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
ty,
|
||||
"configuration metadata supports bool, i32, u32, u64, f32, and f64",
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok((kind, value))
|
||||
}
|
||||
|
||||
fn expand_check(crate_path: &syn::Path, field: &syn::Ident, id: u16, check: Check) -> TokenStream {
|
||||
match check {
|
||||
Check::Finite => quote! {
|
||||
if !self.#field.is_finite() {
|
||||
return Err(#crate_path::ConfigViolation::field(
|
||||
#crate_path::FieldId::new(#id),
|
||||
#crate_path::ValidationRule::NonFinite,
|
||||
));
|
||||
}
|
||||
},
|
||||
Check::Nonzero => quote! {
|
||||
if self.#field == 0 {
|
||||
return Err(#crate_path::ConfigViolation::field(
|
||||
#crate_path::FieldId::new(#id),
|
||||
#crate_path::ValidationRule::Zero,
|
||||
));
|
||||
}
|
||||
},
|
||||
Check::Minimum(minimum) => quote! {
|
||||
if self.#field < (#minimum) {
|
||||
return Err(#crate_path::ConfigViolation::field(
|
||||
#crate_path::FieldId::new(#id),
|
||||
#crate_path::ValidationRule::BelowMinimum,
|
||||
));
|
||||
}
|
||||
},
|
||||
Check::Maximum(maximum) => quote! {
|
||||
if self.#field > (#maximum) {
|
||||
return Err(#crate_path::ConfigViolation::field(
|
||||
#crate_path::FieldId::new(#id),
|
||||
#crate_path::ValidationRule::AboveMaximum,
|
||||
));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//! Procedural validation derive for Kaptan configuration records.
|
||||
|
||||
mod expand;
|
||||
mod parse;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use syn::parse_macro_input;
|
||||
|
||||
/// Implements `kaptan_config::ValidateFields` from `#[config(...)]` helpers.
|
||||
#[proc_macro_derive(ConfigValidate, attributes(config))]
|
||||
pub fn derive_config_validate(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as syn::DeriveInput);
|
||||
match parse::Model::parse(input).and_then(expand::expand) {
|
||||
Ok(tokens) => tokens.into(),
|
||||
Err(error) => error.into_compile_error().into(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
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<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,
|
||||
pub(crate) id: Option<u16>,
|
||||
pub(crate) display_key: Option<syn::LitStr>,
|
||||
pub(crate) unit: Option<syn::LitStr>,
|
||||
pub(crate) default: Option<Expr>,
|
||||
pub(crate) checks: Vec<Check>,
|
||||
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<Self> {
|
||||
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<Path>)> {
|
||||
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<FieldModel> {
|
||||
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::<u16>()?;
|
||||
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<Expr>, Option<Expr>)> {
|
||||
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<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(())
|
||||
} 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,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user