refactor(firmware): centralize configuration and diagnostics

This commit is contained in:
Onur Özkan committed 2026-08-22 18:37:18 +03:00
1 parent 7048ed89d0
commit 387fdaad23
140 files changed
+6891 -4541

No files matched your search

@@ -0,0 +1,186 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
Attribute, Data, DeriveInput, Expr, Fields, Ident, Lit, LitStr, Result, Token,
punctuated::Punctuated, spanned::Spanned,
};
const MAX_CONFIG_ENUM_LABEL_BYTES: usize = 32;
pub(crate) fn expand(input: DeriveInput) -> Result<TokenStream> {
if !input.generics.params.is_empty() {
return Err(syn::Error::new(
input.generics.span(),
"ConfigEnum does not support generic enums",
));
}
require_u8_representation(&input.attrs, input.ident.span())?;
if input
.attrs
.iter()
.any(|attribute| attribute.path().is_ident("config_enum"))
{
return Err(syn::Error::new(
input.ident.span(),
"config_enum options belong on enum variants",
));
}
let Data::Enum(data) = input.data else {
return Err(syn::Error::new(
input.ident.span(),
"ConfigEnum can only be derived for enums",
));
};
if data.variants.is_empty() {
return Err(syn::Error::new(
input.ident.span(),
"ConfigEnum requires at least one variant",
));
}
if data.variants.len() > usize::from(u8::MAX) {
return Err(syn::Error::new(
input.ident.span(),
"ConfigEnum supports at most 255 variants",
));
}
let mut variants = Vec::with_capacity(data.variants.len());
let mut values = Vec::with_capacity(data.variants.len());
let mut labels = Vec::with_capacity(data.variants.len());
for variant in data.variants {
if !matches!(variant.fields, Fields::Unit) {
return Err(syn::Error::new(
variant.fields.span(),
"ConfigEnum variants cannot contain fields",
));
}
let Some((_, discriminant)) = variant.discriminant else {
return Err(syn::Error::new(
variant.ident.span(),
"ConfigEnum variants require explicit u8 discriminants",
));
};
let value = discriminant_value(&discriminant)?;
if values.contains(&value) {
return Err(syn::Error::new(
discriminant.span(),
"duplicate ConfigEnum discriminant",
));
}
let label = variant_label(&variant.attrs, &variant.ident)?;
if labels
.iter()
.any(|previous: &LitStr| previous.value() == label.value())
{
return Err(syn::Error::new(
label.span(),
"duplicate ConfigEnum display label",
));
}
variants.push(variant.ident);
values.push(value);
labels.push(label);
}
let ident = input.ident;
Ok(quote! {
impl #ident {
/// Canonical stable values and display labels for generic
/// configuration discovery.
pub const CONFIG_ENUM_OPTIONS: &'static [(u8, &'static str)] = &[
#((#values, #labels)),*
];
/// Returns this variant's stable compact representation.
pub const fn wire_value(self) -> u8 {
match self {
#(Self::#variants => #values),*
}
}
/// Decodes one stable compact representation.
pub const fn try_from_wire(value: u8) -> Option<Self> {
match value {
#(#values => Some(Self::#variants),)*
_ => None,
}
}
}
})
}
fn require_u8_representation(attributes: &[Attribute], span: proc_macro2::Span) -> Result<()> {
let representations = attributes
.iter()
.filter(|attribute| attribute.path().is_ident("repr"))
.collect::<Vec<_>>();
if representations.len() != 1 {
return Err(syn::Error::new(
span,
"ConfigEnum requires exactly #[repr(u8)]",
));
}
let arguments =
representations[0].parse_args_with(Punctuated::<Ident, Token![,]>::parse_terminated)?;
if arguments.len() != 1 || !arguments[0].eq("u8") {
return Err(syn::Error::new(
representations[0].span(),
"ConfigEnum requires exactly #[repr(u8)]",
));
}
Ok(())
}
fn discriminant_value(expression: &Expr) -> Result<u8> {
let Expr::Lit(expression) = expression else {
return Err(syn::Error::new(
expression.span(),
"ConfigEnum discriminants must be u8 integer literals",
));
};
let Lit::Int(literal) = &expression.lit else {
return Err(syn::Error::new(
expression.span(),
"ConfigEnum discriminants must be u8 integer literals",
));
};
literal.base10_parse::<u8>().map_err(|_| {
syn::Error::new(
literal.span(),
"ConfigEnum discriminant is outside the u8 domain",
)
})
}
fn variant_label(attributes: &[Attribute], ident: &Ident) -> Result<LitStr> {
let mut label = None;
for attribute in attributes
.iter()
.filter(|attribute| attribute.path().is_ident("config_enum"))
{
attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("label") {
if label.is_some() {
return Err(meta.error("duplicate ConfigEnum display label"));
}
label = Some(meta.value()?.parse::<LitStr>()?);
return Ok(());
}
Err(meta.error("the only variant config_enum option is label = \"...\""))
})?;
}
let label = label.unwrap_or_else(|| LitStr::new(&ident.to_string(), ident.span()));
if label.value().is_empty() || label.value().contains('\0') {
return Err(syn::Error::new(
label.span(),
"ConfigEnum display labels must be non-empty text without NUL bytes",
));
}
if label.value().len() > MAX_CONFIG_ENUM_LABEL_BYTES {
return Err(syn::Error::new(
label.span(),
"ConfigEnum display label exceeds the 32-byte wire limit",
));
}
Ok(label)
}
+150 -138
View File
@@ -15,7 +15,7 @@ pub(crate) fn expand(model: Model) -> Result<TokenStream> {
} = 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),
ModelKind::Sections => expand_children(ident, generics, crate_path, post_validate, fields),
}
}
@@ -29,7 +29,6 @@ fn expand_scalars(
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 {
@@ -37,38 +36,62 @@ fn expand_scalars(
descriptor,
checks: field_checks,
} = expand_field(&crate_path, field)?;
if let Some(descriptor) = descriptor {
descriptors.push(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 id = field.id.expect("scalar configuration fields have IDs");
let ty = &field.ty;
let field_ident = &field.ident;
defaults.push(quote! { #field_ident: #default });
let (kind, read, decode, out_of_domain_rule) = if !field.enum_backed {
(
quote! { <#ty as #crate_path::ConfigScalar>::KIND },
quote! { #crate_path::ConfigScalar::into_config_value(self.#field_ident) },
quote! { <#ty as #crate_path::ConfigScalar>::try_from_config_value(value) },
quote! { #crate_path::ValidationRule::AboveMaximum },
)
} else {
(
quote! { #crate_path::ValueKind::U32 },
quote! {
#crate_path::ConfigValue::U32(u32::from(self.#field_ident.wire_value()))
},
quote! {
match value {
#crate_path::ConfigValue::U32(value) => u8::try_from(value)
.ok()
.and_then(<#ty>::try_from_wire)
.ok_or(#crate_path::ScalarValueError::OutOfDomain),
value => Err(#crate_path::ScalarValueError::WrongKind(value.kind())),
}
},
quote! { #crate_path::ValidationRule::InvalidEnumValue },
)
};
reads.push(quote! {
#id => Some(#value(self.#field_ident)),
#id => Some(#read),
});
writes.push(quote! {
#id => {
let next = match value {
#value(next) => next,
actual => {
let next = match #decode {
Ok(next) => next,
Err(#crate_path::ScalarValueError::WrongKind(actual)) => {
return Err(#crate_path::FieldWriteError::WrongType {
field: #crate_path::FieldId::new(#id),
expected: #kind,
actual: actual.kind(),
actual,
});
}
Err(#crate_path::ScalarValueError::OutOfDomain) => {
return Err(#crate_path::FieldWriteError::Violation(
#crate_path::ConfigViolation::field(
#crate_path::FieldId::new(#id),
#out_of_domain_rule,
),
));
}
};
let previous = self.#field_ident;
self.#field_ident = next;
if let Err(violation) =
<Self as #crate_path::ConfigSection>::validate_local_fields(self)
{
if let Err(violation) = self.__validate_local_configuration() {
self.#field_ident = previous;
return Err(#crate_path::FieldWriteError::Violation(violation));
}
@@ -78,30 +101,32 @@ fn expand_scalars(
}
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
impl #impl_generics #ident #type_generics #where_clause {
/// Field metadata generated from the same declarations as validation.
pub const DESCRIPTORS: &'static [#crate_path::FieldDescriptor] = &[
#(#descriptors),*
];
fn __validate_local_configuration(
&self,
) -> Result<(), #crate_path::ConfigViolation> {
if !#crate_path::validation::__valid_enum_options(Self::DESCRIPTORS) {
return Err(#crate_path::ConfigViolation::global(
#crate_path::ValidationRule::Inconsistent,
));
}
#(#checks)*
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 {
impl #impl_generics #crate_path::Config for #ident #type_generics #where_clause {
const FIELD_COUNT: usize = Self::DESCRIPTORS.len();
const SCHEMA_ID: [u8; 16] =
#crate_path::validation::__schema_id(Self::DESCRIPTORS);
fn default_section() -> Result<Self, #crate_path::ConfigViolation> {
let section = Self {
#(#defaults),*
};
<Self as #crate_path::ValidateFields>::validate_fields(&section)?;
Ok(section)
fn descriptor(index: usize) -> Option<&'static #crate_path::FieldDescriptor> {
Self::DESCRIPTORS.get(index)
}
fn field_value(&self, id: #crate_path::FieldId) -> Option<#crate_path::ConfigValue> {
@@ -122,15 +147,16 @@ fn expand_scalars(
}
}
fn validate_local_fields(&self) -> Result<(), #crate_path::ConfigViolation> {
#(#checks)*
fn validate(&self) -> Result<(), #crate_path::ConfigViolation> {
self.__validate_local_configuration()?;
#post_validate
Ok(())
}
}
})
}
fn expand_sections(
fn expand_children(
ident: syn::Ident,
generics: syn::Generics,
crate_path: syn::Path,
@@ -138,71 +164,55 @@ fn expand_sections(
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 mut validations = Vec::new();
let mut reads = Vec::new();
let mut writes = Vec::new();
let mut counts = Vec::new();
let mut schema_ids = Vec::new();
let mut descriptor_lookups = Vec::new();
for field in &fields {
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(
validations.push(quote! {
<#ty as #crate_path::Config>::validate(&self.#field_ident)?;
});
reads.push(quote! {
if let Some(value) = <#ty as #crate_path::Config>::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(
});
writes.push(quote! {
if <#ty as #crate_path::Config>::field_value(&self.#field_ident, id).is_some() {
return <#ty as #crate_path::Config>::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(())
});
counts.push(quote! { <#ty as #crate_path::Config>::FIELD_COUNT });
schema_ids.push(quote! { <#ty as #crate_path::Config>::SCHEMA_ID });
descriptor_lookups.push(quote! {
if index < <#ty as #crate_path::Config>::FIELD_COUNT {
return <#ty as #crate_path::Config>::descriptor(index);
}
}
index -= <#ty as #crate_path::Config>::FIELD_COUNT;
});
}
let post_validate = post_validate.map(|hook| quote! { #hook(self)?; });
impl #impl_generics #ident #type_generics #where_clause {
/// Number of fields aggregated from derived nested sections.
pub const FIELD_COUNT: usize = 0 #(+ #counts)*;
}
Ok(quote! {
impl #impl_generics #crate_path::Config for #ident #type_generics #where_clause {
const FIELD_COUNT: usize = 0 #(+ #counts)*;
const SCHEMA_ID: [u8; 16] =
#crate_path::validation::__combine_schema_ids(&[#(#schema_ids),*]);
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 descriptor(mut index: usize) -> Option<&'static #crate_path::FieldDescriptor> {
#(#descriptor_lookups)*
None
}
fn field_value(&self, id: #crate_path::FieldId) -> Option<#crate_path::ConfigValue> {
@@ -218,23 +228,39 @@ fn expand_sections(
#(#writes)*
Err(#crate_path::FieldWriteError::UnknownField(id))
}
fn validate(&self) -> Result<(), #crate_path::ConfigViolation> {
if !#crate_path::validation::__unique_field_ids::<Self>() {
return Err(#crate_path::ConfigViolation::global(
#crate_path::ValidationRule::Inconsistent,
));
}
if !#crate_path::validation::__unique_display_keys::<Self>() {
return Err(#crate_path::ConfigViolation::global(
#crate_path::ValidationRule::Inconsistent,
));
}
#(#validations)*
#post_validate
Ok(())
}
}
impl #impl_generics #ident #type_generics #where_clause {
/// Number of fields contributed by the complete generated tree.
pub const FIELD_COUNT: usize = <Self as #crate_path::Config>::FIELD_COUNT;
}
})
}
struct FieldMetadata {
descriptor: Option<TokenStream>,
descriptor: 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 id = field.id.expect("scalar configuration fields have IDs");
let minimum = field.checks.iter().find_map(|check| match check {
Check::Minimum(value) => Some(value),
_ => None,
@@ -248,23 +274,33 @@ fn expand_field(crate_path: &syn::Path, field: &crate::parse::FieldModel) -> Res
}
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 ty = &field.ty;
let (kind, enum_options) = if !field.enum_backed {
(
quote! { <#ty as #crate_path::ConfigScalar>::KIND },
quote! { &[] },
)
} else {
(
quote! { #crate_path::ValueKind::U32 },
quote! { <#ty>::CONFIG_ENUM_OPTIONS },
)
};
let value = config_value_constructor_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! {
descriptor: quote! {
#crate_path::FieldDescriptor {
id: #crate_path::FieldId::new(#id),
display_key: #key,
kind: #kind,
minimum: #minimum,
maximum: #maximum,
default: #value(#default),
enum_options: #enum_options,
unit: #unit,
apply_policy: #crate_path::ApplyPolicy::RestartRequired,
}
}),
},
checks,
})
}
@@ -276,10 +312,7 @@ fn option_value_tokens(value: TokenStream, expression: Option<&syn::Expr>) -> To
}
}
fn config_value_tokens(
crate_path: &syn::Path,
ty: &syn::Type,
) -> Result<(TokenStream, TokenStream)> {
fn config_value_constructor_tokens(crate_path: &syn::Path, ty: &syn::Type) -> Result<TokenStream> {
let syn::Type::Path(path) = ty else {
return Err(syn::Error::new_spanned(
ty,
@@ -292,39 +325,18 @@ fn config_value_tokens(
"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 },
),
let value = match segment.ident.to_string().as_str() {
"bool" => quote! { #crate_path::ConfigValue::Bool },
"i32" => quote! { #crate_path::ConfigValue::I32 },
"u8" | "u16" | "u32" => quote! { #crate_path::ConfigValue::U32 },
"u64" => quote! { #crate_path::ConfigValue::U64 },
"f32" => quote! { #crate_path::ConfigValue::F32 },
"f64" => quote! { #crate_path::ConfigValue::F64 },
_ => {
return Err(syn::Error::new_spanned(
ty,
"configuration metadata supports bool, i32, u32, u64, f32, and f64",
));
return Ok(quote! { #crate_path::ConfigValue::U32 });
}
};
Ok((kind, value))
Ok(value)
}
fn expand_check(crate_path: &syn::Path, field: &syn::Ident, id: u16, check: Check) -> TokenStream {
+23 -4
View File
@@ -1,17 +1,36 @@
//! Procedural validation derive for Kaptan configuration records.
//! Procedural derive for recursively composable Kaptan configuration records.
mod config_enum;
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 {
/// Generates validation, metadata, routing, and schema identity.
///
/// Primitive fields retain their native scalar type. Enum-backed fields must
/// opt in with `#[config(enum)]` and derive [`ConfigEnum`].
#[proc_macro_derive(Config, attributes(config))]
pub fn derive_config(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(),
}
}
/// Generates the canonical table and conversions for a field configured as a
/// compact enum.
///
/// The enum must use `#[repr(u8)]`, have explicit integer discriminants, and
/// contain only fieldless variants. Variant identifiers are the default GCS
/// labels; `#[config_enum(label = "...")]` overrides one label.
#[proc_macro_derive(ConfigEnum, attributes(config_enum))]
pub fn derive_config_enum(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput);
match config_enum::expand(input) {
Ok(tokens) => tokens.into(),
Err(error) => error.into_compile_error().into(),
}
}
+110 -157
View File
@@ -3,6 +3,9 @@ use syn::{
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,
@@ -24,9 +27,8 @@ pub(crate) struct FieldModel {
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,
pub(crate) enum_backed: bool,
}
#[derive(Clone)]
@@ -43,19 +45,19 @@ impl Model {
let Data::Struct(data) = input.data else {
return Err(syn::Error::new(
input.ident.span(),
"ConfigValidate can only be derived for structs",
"Config 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",
"Config 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",
"Config requires at least one field",
));
}
let mut models = Vec::with_capacity(fields.named.len());
@@ -73,38 +75,63 @@ impl Model {
}
models.push(model);
}
let kind = if models.iter().all(|field| field.section) {
ModelKind::Sections
} else if models.iter().all(|field| field.id.is_some()) {
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.section || field.id.is_none())
.find(|field| 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",
"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 {
if field.display_key.is_none() {
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",
));
}
if field.unit.is_none() {
display_keys.push(display_key);
let Some(unit) = &field.unit else {
return Err(syn::Error::new(
field.ident.span(),
"configuration fields require config(unit = \"...\")",
));
}
if field.default.is_none() {
};
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.ident.span(),
"configuration fields require config(default = ...)",
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)?;
@@ -121,6 +148,34 @@ impl Model {
}
}
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<Path>)> {
let mut crate_path = syn::parse_quote!(::kaptan_config);
let mut has_crate_path = false;
@@ -160,40 +215,17 @@ fn parse_field(field: Field) -> Result<FieldModel> {
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;
let mut enum_backed = 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"));
@@ -220,13 +252,11 @@ fn parse_field(field: Field) -> Result<FieldModel> {
unit = Some(meta.value()?.parse()?);
return Ok(());
}
if meta.path.is_ident("default") {
if default.is_some() {
return Err(meta.error("duplicate configuration default"));
if meta.path.is_ident("enum") {
if enum_backed {
return Err(meta.error("duplicate enum configuration marker"));
}
let expression = meta.value()?.parse()?;
validate_default_literal(&expression, &field.ty)?;
default = Some(expression);
enum_backed = true;
return Ok(());
}
if meta.path.is_ident("finite") {
@@ -273,26 +303,16 @@ fn parse_field(field: Field) -> Result<FieldModel> {
return Ok(());
}
Err(meta.error(
"supported field config rules are id, key, unit, default, finite, nonzero, and range",
"supported field config rules are id, key, unit, enum, 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())
if id.is_none()
&& (display_key.is_some() || unit.is_some() || enum_backed || !checks.is_empty())
{
return Err(syn::Error::new(
ident.span(),
"config(section) cannot be combined with scalar id, metadata, or validation rules",
"scalar configuration fields require config(id = N)",
));
}
Ok(FieldModel {
@@ -301,9 +321,8 @@ fn parse_field(field: Field) -> Result<FieldModel> {
id,
display_key,
unit,
default,
checks,
section,
enum_backed,
})
}
@@ -353,33 +372,19 @@ fn validate_numeric_literal(expression: &Expr) -> Result<()> {
}
}
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(
let Some(ty) = scalar_type_name(&field.ty) else {
if field.checks.is_empty() {
return Ok(());
}
return Err(syn::Error::new(
field.ty.span(),
"configuration metadata requires a supported scalar type",
)
})?;
"validation rules require a supported primitive 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,
@@ -406,70 +411,6 @@ fn validate_static_constraints(field: &FieldModel) -> Result<()> {
"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(())
}
@@ -487,14 +428,6 @@ impl ScalarLiteral {
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> {
@@ -519,6 +452,26 @@ fn scalar_literal(expression: &Expr, ty: &str) -> Result<ScalarLiteral> {
})?;
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(|_| {
@@ -535,7 +488,7 @@ fn scalar_literal(expression: &Expr, ty: &str) -> Result<ScalarLiteral> {
"f64" => float_literal(expression).map(ScalarLiteral::Float),
_ => Err(syn::Error::new(
expression.span(),
"configuration metadata supports bool, i32, u32, u64, f32, and f64",
"configuration metadata supports bool, i32, u8, u16, u32, u64, f32, and f64",
)),
}
}
@@ -1,27 +1,27 @@
use kaptan_config::{ConfigAggregate, ConfigSection, ConfigViolation, ValidateFields};
use kaptan_config_derive::ConfigValidate;
use kaptan_config::{Config, ConfigViolation};
#[derive(ConfigValidate)]
#[derive(Config)]
struct Child {
#[config(id = 1, key = "child.value", unit = "count", default = 1, range(min = 0, max = 2))]
#[config(id = 1, key = "child.value", unit = "count", range(min = 0, max = 2))]
value: u32,
}
#[derive(ConfigValidate)]
#[derive(Config)]
#[config(post = Self::validate_complete)]
struct Root {
#[config(section)]
child: Child,
}
impl Root {
fn validate_complete(&self) -> Result<(), ConfigViolation> {
self.child.validate_fields()
self.child.validate()
}
}
fn main() {
let root = Root::default_aggregate().unwrap();
let root = Root {
child: Child { value: 1 },
};
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,25 @@
use kaptan_config_derive::{Config, ConfigEnum};
#[derive(Clone, Copy, ConfigEnum)]
#[repr(u8)]
enum Mode {
Safe = 1,
#[config_enum(label = "Full authority")]
FullAuthority = 7,
}
#[derive(Config)]
struct EnumConfig {
#[config(enum, id = 1, key = "mode", unit = "mode")]
mode: Mode,
}
fn main() {
assert_eq!(Mode::Safe.wire_value(), 1);
assert!(matches!(Mode::try_from_wire(7), Some(Mode::FullAuthority)));
assert_eq!(Mode::CONFIG_ENUM_OPTIONS[1], (7, "Full authority"));
assert_eq!(
EnumConfig::DESCRIPTORS[0].enum_options,
Mode::CONFIG_ENUM_OPTIONS
);
}
@@ -1,8 +1,8 @@
use kaptan_config_derive::ConfigValidate;
use kaptan_config_derive::Config;
#[derive(ConfigValidate)]
#[derive(Config)]
struct RootBooleanConfig {
#[config(id = 1, key = "enabled", unit = "bool", default = true)]
#[config(id = 1, key = "enabled", unit = "bool")]
enabled: bool,
}
@@ -1,37 +1,22 @@
use kaptan_config::{
ConfigAggregate, ConfigSection, ConfigValue, ConfigViolation, FieldId, FieldWriteError,
ValidateFields, ValidationRule,
Config, ConfigEnum, ConfigValue, ConfigViolation, FieldId, FieldWriteError, ValidationRule,
};
use kaptan_config_derive::ConfigValidate;
#[derive(ConfigValidate)]
#[derive(Config)]
struct LocalSection {
#[config(
id = 1,
key = "local.first",
unit = "count",
default = 2,
range(min = 1, max = 9)
)]
#[config(id = 1, key = "local.first", unit = "count", range(min = 1, max = 9))]
first: u32,
#[config(
id = 2,
key = "local.second",
unit = "count",
default = 4,
range(min = 1, max = 9)
)]
#[config(id = 2, key = "local.second", unit = "count", range(min = 1, max = 9))]
second: u32,
}
#[derive(ConfigValidate)]
#[derive(Config)]
#[config(post = Self::validate_order)]
struct OrderedSection {
#[config(
id = 3,
key = "ordered.minimum",
unit = "count",
default = 1,
range(min = 0, max = 9)
)]
minimum: u32,
@@ -39,7 +24,6 @@ struct OrderedSection {
id = 4,
key = "ordered.maximum",
unit = "count",
default = 2,
range(min = 0, max = 9)
)]
maximum: u32,
@@ -58,17 +42,54 @@ impl OrderedSection {
}
}
#[derive(ConfigValidate)]
#[derive(Config)]
struct Aggregate {
#[config(section)]
local: LocalSection,
#[config(section)]
ordered: OrderedSection,
}
#[derive(Config)]
struct BoardOwnedSection {
#[config(
id = 10,
key = "board.endpoint",
unit = "id",
range(min = 1, max = 255)
)]
endpoint: u32,
}
#[derive(Config)]
struct BoardOwnedAggregate {
endpoint: BoardOwnedSection,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ConfigEnum)]
#[repr(u8)]
enum TestMode {
Quiet = 1,
Loud = 3,
}
#[derive(Config)]
struct CompactSection {
#[config(
id = 20,
key = "compact.endpoint",
unit = "id",
range(min = 1, max = 255)
)]
endpoint: u8,
#[config(enum, id = 21, key = "compact.mode", unit = "mode")]
mode: TestMode,
}
#[test]
fn generated_section_owns_every_declared_scalar_path() {
let mut section = LocalSection::default_section().unwrap();
let mut section = LocalSection {
first: 2,
second: 4,
};
assert_eq!(LocalSection::FIELD_COUNT, 2);
assert_eq!(
section.field_value(FieldId::new(2)),
@@ -94,9 +115,52 @@ fn generated_section_owns_every_declared_scalar_path() {
);
}
#[test]
fn narrow_scalars_and_compact_enums_keep_native_types_and_generic_metadata() {
let mut section = CompactSection {
endpoint: 7,
mode: TestMode::Quiet,
};
assert_eq!(
section.field_value(FieldId::new(20)),
Some(ConfigValue::U32(7))
);
assert_eq!(
CompactSection::DESCRIPTORS[1].enum_options,
TestMode::CONFIG_ENUM_OPTIONS
);
assert_eq!(TestMode::Quiet.wire_value(), 1);
assert_eq!(TestMode::try_from_wire(3), Some(TestMode::Loud));
assert_eq!(TestMode::try_from_wire(2), None);
section
.set_field(FieldId::new(21), ConfigValue::U32(3))
.unwrap();
assert_eq!(section.mode, TestMode::Loud);
assert!(matches!(
section.set_field(FieldId::new(21), ConfigValue::U32(2)),
Err(FieldWriteError::Violation(issue))
if issue.rule() == ValidationRule::InvalidEnumValue
));
assert!(matches!(
section.set_field(FieldId::new(20), ConfigValue::U32(256)),
Err(FieldWriteError::Violation(issue))
if issue.rule() == ValidationRule::AboveMaximum
));
}
#[test]
fn aggregate_routes_writes_and_defers_declared_post_validation() {
let mut aggregate = Aggregate::default_aggregate().unwrap();
let mut aggregate = Aggregate {
local: LocalSection {
first: 2,
second: 4,
},
ordered: OrderedSection {
minimum: 1,
maximum: 2,
},
};
assert_eq!(Aggregate::FIELD_COUNT, 4);
assert_eq!(
aggregate.field_value(FieldId::new(2)),
@@ -106,7 +170,7 @@ fn aggregate_routes_writes_and_defers_declared_post_validation() {
.set_field(FieldId::new(3), ConfigValue::U32(3))
.unwrap();
assert!(matches!(
aggregate.validate_fields(),
aggregate.validate(),
Err(issue)
if issue.primary() == Some(FieldId::new(3))
&& issue.related_field() == Some(FieldId::new(4))
@@ -115,5 +179,101 @@ fn aggregate_routes_writes_and_defers_declared_post_validation() {
aggregate
.set_field(FieldId::new(4), ConfigValue::U32(3))
.unwrap();
aggregate.validate_fields().unwrap();
aggregate.validate().unwrap();
}
#[test]
fn aggregate_derives_its_flattened_descriptor_order() {
let descriptors: Vec<_> = (0..Aggregate::FIELD_COUNT)
.map(|index| Aggregate::descriptor(index).unwrap())
.collect();
assert_eq!(
descriptors
.iter()
.map(|descriptor| descriptor.id.get())
.collect::<Vec<_>>(),
[1, 2, 3, 4]
);
assert!(Aggregate::descriptor(Aggregate::FIELD_COUNT).is_none());
}
#[test]
fn factory_values_are_owned_outside_the_generated_schema() {
let mut aggregate = BoardOwnedAggregate {
endpoint: BoardOwnedSection { endpoint: 42 },
};
assert_eq!(BoardOwnedAggregate::FIELD_COUNT, 1);
assert_eq!(
aggregate.field_value(FieldId::new(10)),
Some(ConfigValue::U32(42))
);
aggregate
.set_field(FieldId::new(10), ConfigValue::U32(43))
.unwrap();
aggregate.validate().unwrap();
}
#[derive(Config)]
struct DuplicateChildren {
first: LocalSection,
second: LocalSection,
}
#[derive(Config)]
struct TransparentRoot {
local: LocalSection,
}
#[derive(Config)]
struct FirstKeyOwner {
#[config(id = 30, key = "shared.key", unit = "count")]
value: u32,
}
#[derive(Config)]
struct SecondKeyOwner {
#[config(id = 31, key = "shared.key", unit = "count")]
value: u32,
}
#[derive(Config)]
struct DuplicateKeysAcrossChildren {
first: FirstKeyOwner,
second: SecondKeyOwner,
}
#[test]
fn schema_fingerprint_is_generated_and_one_child_wrappers_are_transparent() {
assert_eq!(TransparentRoot::SCHEMA_ID, LocalSection::SCHEMA_ID);
assert_ne!(Aggregate::SCHEMA_ID, LocalSection::SCHEMA_ID);
}
#[test]
fn recursive_tree_rejects_duplicate_field_ids() {
let duplicate = DuplicateChildren {
first: LocalSection {
first: 2,
second: 4,
},
second: LocalSection {
first: 2,
second: 4,
},
};
assert!(matches!(
duplicate.validate(),
Err(issue) if issue.rule() == ValidationRule::Inconsistent
));
}
#[test]
fn recursive_tree_rejects_duplicate_display_keys() {
let duplicate = DuplicateKeysAcrossChildren {
first: FirstKeyOwner { value: 1 },
second: SecondKeyOwner { value: 2 },
};
assert!(matches!(
duplicate.validate(),
Err(issue) if issue.rule() == ValidationRule::Inconsistent
));
}
@@ -0,0 +1,10 @@
use kaptan_config_derive::ConfigEnum;
#[derive(ConfigEnum)]
#[repr(u8)]
enum ImplicitDiscriminant {
First = 1,
Second,
}
fn main() {}
@@ -0,0 +1,5 @@
error: ConfigEnum variants require explicit u8 discriminants
--> tests/ui/config_enum_implicit_discriminant.rs:7:5
|
7 | Second,
| ^^^^^^
@@ -0,0 +1,10 @@
use kaptan_config_derive::ConfigEnum;
#[derive(ConfigEnum)]
#[repr(u8)]
enum LabelWithNul {
#[config_enum(label = "invalid\0label")]
Value = 1,
}
fn main() {}
@@ -0,0 +1,5 @@
error: ConfigEnum display labels must be non-empty text without NUL bytes
--> tests/ui/config_enum_label_nul.rs:6:27
|
6 | #[config_enum(label = "invalid\0label")]
| ^^^^^^^^^^^^^^^^
@@ -0,0 +1,10 @@
use kaptan_config_derive::ConfigEnum;
#[derive(ConfigEnum)]
#[repr(u8)]
enum LabelTooManyBytes {
#[config_enum(label = "şşşşşşşşşşşşşşşşş")]
Value = 1,
}
fn main() {}
@@ -0,0 +1,5 @@
error: ConfigEnum display label exceeds the 32-byte wire limit
--> tests/ui/config_enum_label_too_many_bytes.rs:6:27
|
6 | #[config_enum(label = "şşşşşşşşşşşşşşşşş")]
| ^^^^^^^^^^^^^^^^^^^
@@ -0,0 +1,8 @@
use kaptan_config_derive::ConfigEnum;
#[derive(ConfigEnum)]
enum MissingRepresentation {
First = 1,
}
fn main() {}
@@ -0,0 +1,5 @@
error: ConfigEnum requires exactly #[repr(u8)]
--> tests/ui/config_enum_missing_repr.rs:4:6
|
4 | enum MissingRepresentation {
| ^^^^^^^^^^^^^^^^^^^^^
@@ -0,0 +1,9 @@
use kaptan_config_derive::ConfigEnum;
#[derive(ConfigEnum)]
#[repr(u8)]
enum Payload {
Value(u8) = 1,
}
fn main() {}
@@ -0,0 +1,5 @@
error: ConfigEnum variants cannot contain fields
--> tests/ui/config_enum_payload.rs:6:10
|
6 | Value(u8) = 1,
| ^^^^
@@ -1,9 +0,0 @@
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() {}
@@ -1,5 +0,0 @@
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))]
| ^
@@ -1,9 +0,0 @@
use kaptan_config_derive::ConfigValidate;
#[derive(ConfigValidate)]
struct NonFiniteDefault {
#[config(id = 1, key = "value", unit = "ratio", default = 1e100, finite)]
value: f32,
}
fn main() {}
@@ -1,5 +0,0 @@
error: configuration default violates finite
--> tests/ui/default_violates_finite.rs:5:63
|
5 | #[config(id = 1, key = "value", unit = "ratio", default = 1e100, finite)]
| ^^^^^
@@ -1,9 +0,0 @@
use kaptan_config_derive::ConfigValidate;
#[derive(ConfigValidate)]
struct ZeroDefault {
#[config(id = 1, key = "value", unit = "count", default = 0, nonzero)]
value: u32,
}
fn main() {}
@@ -1,5 +0,0 @@
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,13 @@
use kaptan_config_derive::Config;
#[derive(Config)]
struct DisplayKeyTooLong {
#[config(
id = 1,
key = "configuration.display.key.with.more.than.forty.eight.bytes",
unit = "count"
)]
value: u32,
}
fn main() {}
@@ -0,0 +1,5 @@
error: configuration display key exceeds the 48-byte wire limit
--> tests/ui/display_key_too_long.rs:7:15
|
7 | key = "configuration.display.key.with.more.than.forty.eight.bytes",
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -0,0 +1,11 @@
use kaptan_config_derive::Config;
#[derive(Config)]
struct DuplicateDisplayKey {
#[config(id = 1, key = "duplicate.key", unit = "count")]
first: u32,
#[config(id = 2, key = "duplicate.key", unit = "count")]
second: u32,
}
fn main() {}
@@ -0,0 +1,5 @@
error: duplicate configuration display key
--> tests/ui/duplicate_display_key.rs:7:28
|
7 | #[config(id = 2, key = "duplicate.key", unit = "count")]
| ^^^^^^^^^^^^^^^
@@ -1,6 +1,6 @@
use kaptan_config_derive::ConfigValidate;
use kaptan_config_derive::Config;
#[derive(ConfigValidate)]
#[derive(Config)]
struct DuplicateId {
#[config(id = 7, nonzero)]
first: u32,
@@ -1,6 +1,6 @@
use kaptan_config_derive::ConfigValidate;
use kaptan_config_derive::Config;
#[derive(ConfigValidate)]
#[derive(Config)]
struct DuplicateRange {
#[config(id = 7, range(min = 1), range(min = 2))]
value: u32,
@@ -1,6 +1,6 @@
use kaptan_config_derive::ConfigValidate;
use kaptan_config_derive::Config;
#[derive(ConfigValidate)]
#[derive(Config)]
struct Empty {}
fn main() {}
@@ -1,4 +1,4 @@
error: ConfigValidate requires at least one configured field or section
error: Config requires at least one field
--> tests/ui/empty.rs:4:8
|
4 | struct Empty {}
@@ -0,0 +1,9 @@
use kaptan_config_derive::Config;
#[derive(Config)]
struct PrimitiveEnumMarker {
#[config(enum, id = 1, key = "mode", unit = "mode")]
mode: u8,
}
fn main() {}
@@ -0,0 +1,5 @@
error: config(enum) requires a ConfigEnum-derived enum type
--> tests/ui/enum_marker_on_primitive.rs:6:11
|
6 | mode: u8,
| ^^
@@ -0,0 +1,15 @@
use kaptan_config_derive::{Config, ConfigEnum};
#[derive(Clone, Copy, ConfigEnum)]
#[repr(u8)]
enum Mode {
Safe = 1,
}
#[derive(Config)]
struct MissingEnumMarker {
#[config(id = 1, key = "mode", unit = "mode")]
mode: Mode,
}
fn main() {}
@@ -0,0 +1,5 @@
error: non-primitive configuration fields require config(enum)
--> tests/ui/missing_enum_marker.rs:12:11
|
12 | mode: Mode,
| ^^^^
@@ -1,6 +1,6 @@
use kaptan_config_derive::ConfigValidate;
use kaptan_config_derive::Config;
#[derive(ConfigValidate)]
#[derive(Config)]
struct MissingId {
#[config(nonzero)]
value: u32,
@@ -1,4 +1,4 @@
error: validated configuration fields require config(id = N)
error: scalar configuration fields require config(id = N)
--> tests/ui/missing_id.rs:6:5
|
6 | value: u32,
@@ -1,8 +1,8 @@
use kaptan_config_derive::ConfigValidate;
use kaptan_config_derive::Config;
#[derive(ConfigValidate)]
#[derive(Config)]
struct MissingMetadata {
#[config(id = 7, unit = "count", default = 1, range(min = 0, max = 10))]
#[config(id = 7, unit = "count", range(min = 0, max = 10))]
value: u32,
}
@@ -1,10 +1,9 @@
use kaptan_config_derive::ConfigValidate;
use kaptan_config_derive::Config;
#[derive(ConfigValidate)]
#[derive(Config)]
struct MixedSectionAndScalar {
#[config(section)]
child: u32,
#[config(id = 1, key = "scalar", unit = "count", default = 1, range(min = 0, max = 2))]
#[config(id = 1, key = "scalar", unit = "count", range(min = 0, max = 2))]
scalar: u32,
}
@@ -1,5 +1,5 @@
error: ConfigValidate records must contain either scalar config fields or config(section) fields exclusively
--> tests/ui/mixed_section_scalar.rs:6:5
error: Config records must contain either scalar fields or child configuration records exclusively
--> tests/ui/mixed_section_scalar.rs:5:5
|
6 | child: u32,
5 | child: u32,
| ^^^^^
@@ -1,8 +1,8 @@
use kaptan_config_derive::ConfigValidate;
use kaptan_config_derive::Config;
#[derive(ConfigValidate)]
#[derive(Config)]
struct InvalidRange {
#[config(id = 1, key = "value", unit = "count", default = 2, range(min = 3, max = 1))]
#[config(id = 1, key = "value", unit = "count", range(min = 3, max = 1))]
value: u32,
}
@@ -0,0 +1,9 @@
use kaptan_config_derive::Config;
#[derive(Config)]
struct UnitTooLong {
#[config(id = 1, key = "value", unit = "seventeen_bytes___")]
value: u32,
}
fn main() {}
@@ -0,0 +1,5 @@
error: configuration unit exceeds the 16-byte wire limit
--> tests/ui/unit_too_long.rs:5:44
|
5 | #[config(id = 1, key = "value", unit = "seventeen_bytes___")]
| ^^^^^^^^^^^^^^^^^^^^
@@ -0,0 +1,9 @@
use kaptan_config_derive::Config;
#[derive(Config)]
struct UnsupportedDefault {
#[config(id = 1, key = "value", unit = "count", default = 1)]
value: u32,
}
fn main() {}
@@ -0,0 +1,5 @@
error: supported field config rules are id, key, unit, enum, finite, nonzero, and range
--> tests/ui/unsupported_default.rs:5:53
|
5 | #[config(id = 1, key = "value", unit = "count", default = 1)]
| ^^^^^^^
@@ -0,0 +1,10 @@
use kaptan_config_derive::Config;
#[derive(Config)]
#[config(external_defaults)]
struct UnsupportedExternalDefaults {
#[config(id = 1, key = "value", unit = "count")]
value: u32,
}
fn main() {}
@@ -0,0 +1,5 @@
error: struct config options are crate = path and post = path
--> tests/ui/unsupported_external_defaults.rs:4:10
|
4 | #[config(external_defaults)]
| ^^^^^^^^^^^^^^^^^