Files
kaptan/firmware/libs/kaptan-config-derive/src/parse.rs
T

662 lines
22 KiB
Rust

use syn::{
Attribute, Data, DeriveInput, Expr, Field, Fields, Ident, Lit, Path, Result, Type, UnOp,
spanned::Spanned,
};
const MAX_DISPLAY_KEY_BYTES: usize = 48;
const MAX_UNIT_BYTES: usize = 16;
pub(crate) struct Model {
pub(crate) ident: Ident,
pub(crate) generics: syn::Generics,
pub(crate) crate_path: Path,
pub(crate) post_validate: Option<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) checks: Vec<Check>,
pub(crate) enum_backed: bool,
}
#[derive(Clone)]
pub(crate) enum Check {
Finite,
Nonzero,
Minimum(Expr),
Maximum(Expr),
}
impl Model {
pub(crate) fn parse(input: DeriveInput) -> Result<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(),
"Config can only be derived for structs",
));
};
let Fields::Named(fields) = data.fields else {
return Err(syn::Error::new(
input.ident.span(),
"Config requires named struct fields",
));
};
if fields.named.is_empty() {
return Err(syn::Error::new(
input.ident.span(),
"Config requires at least one field",
));
}
let mut models = Vec::with_capacity(fields.named.len());
let mut ids = Vec::new();
for field in fields.named {
let model = parse_field(field)?;
if let Some(id) = model.id {
if ids.contains(&id) {
return Err(syn::Error::new(
model.ident.span(),
"duplicate configuration field id",
));
}
ids.push(id);
}
models.push(model);
}
let kind = if models.iter().all(|field| field.id.is_some()) {
ModelKind::Scalars
} else if models.iter().all(|field| field.id.is_none()) {
ModelKind::Sections
} else {
let field = models
.iter()
.find(|field| field.id.is_none())
.expect("mixed configuration model has one invalid field");
return Err(syn::Error::new(
field.ident.span(),
"Config records must contain either scalar fields or child configuration records exclusively",
));
};
if kind == ModelKind::Scalars {
let mut display_keys = Vec::with_capacity(models.len());
for field in &models {
let Some(display_key) = &field.display_key else {
return Err(syn::Error::new(
field.ident.span(),
"configuration fields require config(key = \"...\")",
));
};
validate_metadata_text(
display_key,
MAX_DISPLAY_KEY_BYTES,
"configuration display key",
false,
)?;
if display_keys
.iter()
.any(|previous: &&syn::LitStr| previous.value() == display_key.value())
{
return Err(syn::Error::new(
display_key.span(),
"duplicate configuration display key",
));
}
display_keys.push(display_key);
let Some(unit) = &field.unit else {
return Err(syn::Error::new(
field.ident.span(),
"configuration fields require config(unit = \"...\")",
));
};
validate_metadata_text(unit, MAX_UNIT_BYTES, "configuration unit", true)?;
let primitive = scalar_type_name(&field.ty).is_some();
if field.enum_backed && primitive {
return Err(syn::Error::new(
field.ty.span(),
"config(enum) requires a ConfigEnum-derived enum type",
));
}
if !field.enum_backed && !primitive {
return Err(syn::Error::new(
field.ty.span(),
"non-primitive configuration fields require config(enum)",
));
}
validate_static_constraints(field)?;
}
}
Ok(Self {
ident: input.ident,
generics: input.generics,
crate_path,
post_validate,
kind,
fields: models,
})
}
}
fn validate_metadata_text(
literal: &syn::LitStr,
maximum_bytes: usize,
name: &str,
allow_empty: bool,
) -> Result<()> {
let value = literal.value();
if !allow_empty && value.is_empty() {
return Err(syn::Error::new(
literal.span(),
format!("{name} must not be empty"),
));
}
if value.contains('\0') {
return Err(syn::Error::new(
literal.span(),
format!("{name} cannot contain NUL bytes"),
));
}
if value.len() > maximum_bytes {
return Err(syn::Error::new(
literal.span(),
format!("{name} exceeds the {maximum_bytes}-byte wire limit"),
));
}
Ok(())
}
fn parse_container_attributes(attributes: &[Attribute]) -> Result<(Path, Option<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 finite = false;
let mut nonzero = false;
let mut range_minimum = false;
let mut range_maximum = false;
let mut enum_backed = false;
for attribute in field
.attrs
.iter()
.filter(|attribute| attribute.path().is_ident("config"))
{
attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("id") {
if id.is_some() {
return Err(meta.error("duplicate configuration field id"));
}
let literal: syn::LitInt = meta.value()?.parse()?;
let parsed = literal.base10_parse::<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("enum") {
if enum_backed {
return Err(meta.error("duplicate enum configuration marker"));
}
enum_backed = true;
return Ok(());
}
if meta.path.is_ident("finite") {
if finite {
return Err(meta.error("duplicate finite validation rule"));
}
require_float(&field.ty, &meta.path)?;
checks.push(Check::Finite);
finite = true;
return Ok(());
}
if meta.path.is_ident("nonzero") {
if nonzero {
return Err(meta.error("duplicate nonzero validation rule"));
}
require_numeric(&field.ty, &meta.path)?;
checks.push(Check::Nonzero);
nonzero = true;
return Ok(());
}
if meta.path.is_ident("range") {
require_numeric(&field.ty, &meta.path)?;
let (minimum, maximum) = parse_range(meta)?;
if minimum.is_none() && maximum.is_none() {
return Err(syn::Error::new(
field.span(),
"range requires min and/or max",
));
}
if let Some(minimum) = minimum {
if range_minimum {
return Err(syn::Error::new(field.span(), "duplicate range minimum"));
}
checks.push(Check::Minimum(minimum));
range_minimum = true;
}
if let Some(maximum) = maximum {
if range_maximum {
return Err(syn::Error::new(field.span(), "duplicate range maximum"));
}
checks.push(Check::Maximum(maximum));
range_maximum = true;
}
return Ok(());
}
Err(meta.error(
"supported field config rules are id, key, unit, enum, finite, nonzero, and range",
))
})?;
}
if id.is_none()
&& (display_key.is_some() || unit.is_some() || enum_backed || !checks.is_empty())
{
return Err(syn::Error::new(
ident.span(),
"scalar configuration fields require config(id = N)",
));
}
Ok(FieldModel {
ident,
ty: field.ty,
id,
display_key,
unit,
checks,
enum_backed,
})
}
fn parse_range(meta: syn::meta::ParseNestedMeta<'_>) -> Result<(Option<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_static_constraints(field: &FieldModel) -> Result<()> {
let Some(ty) = scalar_type_name(&field.ty) else {
if field.checks.is_empty() {
return Ok(());
}
return Err(syn::Error::new(
field.ty.span(),
"validation rules require a supported primitive scalar type",
));
};
if ty == "bool" {
return Ok(());
}
let minimum = field.checks.iter().find_map(|check| match check {
Check::Minimum(value) => Some(value),
_ => None,
});
let maximum = field.checks.iter().find_map(|check| match check {
Check::Maximum(value) => Some(value),
_ => None,
});
let minimum = minimum.map(|value| scalar_literal(value, ty)).transpose()?;
let maximum = maximum.map(|value| scalar_literal(value, ty)).transpose()?;
if minimum.is_some_and(|value| !value.is_finite())
|| maximum.is_some_and(|value| !value.is_finite())
{
return Err(syn::Error::new(
field.ident.span(),
"configuration range bounds must be finite",
));
}
if let (Some(minimum), Some(maximum)) = (minimum, maximum)
&& compare_scalar(minimum, maximum).is_gt()
{
return Err(syn::Error::new(
field.ident.span(),
"configuration range minimum exceeds maximum",
));
}
Ok(())
}
#[derive(Clone, Copy)]
enum ScalarLiteral {
Signed(i128),
Unsigned(u128),
Float(f64),
}
impl ScalarLiteral {
fn is_finite(self) -> bool {
match self {
Self::Signed(_) | Self::Unsigned(_) => true,
Self::Float(value) => value.is_finite(),
}
}
}
fn scalar_literal(expression: &Expr, ty: &str) -> Result<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))
}
"u8" => {
let value = unsigned_literal(expression)?;
u8::try_from(value).map_err(|_| {
syn::Error::new(
expression.span(),
"configuration literal is outside u8 range",
)
})?;
Ok(ScalarLiteral::Unsigned(value))
}
"u16" => {
let value = unsigned_literal(expression)?;
u16::try_from(value).map_err(|_| {
syn::Error::new(
expression.span(),
"configuration literal is outside u16 range",
)
})?;
Ok(ScalarLiteral::Unsigned(value))
}
"u64" => {
let value = unsigned_literal(expression)?;
u64::try_from(value).map_err(|_| {
syn::Error::new(
expression.span(),
"configuration literal is outside u64 range",
)
})?;
Ok(ScalarLiteral::Unsigned(value))
}
"f32" => Ok(ScalarLiteral::Float(f64::from(
float_literal(expression)? as f32
))),
"f64" => float_literal(expression).map(ScalarLiteral::Float),
_ => Err(syn::Error::new(
expression.span(),
"configuration metadata supports bool, i32, u8, u16, u32, u64, f32, and f64",
)),
}
}
fn signed_literal(expression: &Expr) -> Result<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,
})
}