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

+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",
)),
}
}