refactor(firmware): centralize configuration and diagnostics
This commit is contained in:
1 parent
7048ed89d0
commit
387fdaad23
140 files changed
+6891
-4541
No files matched your search
@@ -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",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user