Add typed Decodal derive support

This commit is contained in:
2026-06-26 00:39:33 +09:00
parent 87fef44c68
commit b8404df047
21 changed files with 729 additions and 11 deletions
+212
View File
@@ -0,0 +1,212 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
Data, DeriveInput, Expr, Fields, LitStr, Result, Type, parse_macro_input, spanned::Spanned,
};
#[proc_macro_derive(Decodal, attributes(decodal))]
pub fn derive_decodal(input: TokenStream) -> TokenStream {
match expand_decodal(parse_macro_input!(input as DeriveInput)) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
let name = input.ident;
let fields = match input.data {
Data::Struct(data) => match data.fields {
Fields::Named(fields) => fields.named,
_ => {
return Err(syn::Error::new(
data.struct_token.span(),
"Decodal derive supports structs with named fields only",
));
}
},
_ => {
return Err(syn::Error::new(
name.span(),
"Decodal derive supports structs only",
));
}
};
let mut schema_fields = Vec::new();
let mut decode_fields = Vec::new();
for field in fields {
let ident = field.ident.expect("named field");
let ty = field.ty;
let attrs = FieldAttrs::from_attrs(&field.attrs, &ident.to_string())?;
let path = attrs.rename.clone();
let schema_value = schema_expr(&ty, &attrs)?;
let decode_value = decode_expr(&ty, &path, &attrs);
schema_fields.push(quote! {
(#path, { #schema_value })
});
decode_fields.push(quote! {
#ident: #decode_value
});
}
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
Ok(quote! {
impl #impl_generics ::decodal::DecodalSchema for #name #ty_generics #where_clause {
fn decodal_schema() -> ::decodal::HostValue {
::decodal::HostValue::object_from_paths([
#(#schema_fields),*
])
}
}
impl #impl_generics ::decodal::DecodalDecode for #name #ty_generics #where_clause {
fn decodal_decode(data: &::decodal::Data) -> ::decodal::DecodeResult<Self> {
Ok(Self {
#(#decode_fields),*
})
}
}
})
}
#[derive(Default)]
struct FieldAttrs {
rename: String,
default: Option<DefaultAttr>,
constraints: Vec<ConstraintAttr>,
}
impl FieldAttrs {
fn from_attrs(attrs: &[syn::Attribute], fallback_name: &str) -> Result<Self> {
let mut output = Self {
rename: fallback_name.to_string(),
..Self::default()
};
for attr in attrs {
if !attr.path().is_ident("decodal") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
let value: LitStr = meta.value()?.parse()?;
output.rename = value.value();
return Ok(());
}
if meta.path.is_ident("default") {
if meta.input.peek(syn::Token![=]) {
let expr: Expr = meta.value()?.parse()?;
output.default = Some(DefaultAttr::Expr(expr));
} else {
output.default = Some(DefaultAttr::Default);
}
return Ok(());
}
if let Some(kind) = ConstraintKind::from_path(&meta.path) {
let expr: Expr = meta.value()?.parse()?;
output.constraints.push(ConstraintAttr { kind, expr });
return Ok(());
}
Err(meta.error("unsupported decodal attribute"))
})?;
}
Ok(output)
}
}
enum DefaultAttr {
Default,
Expr(Expr),
}
struct ConstraintAttr {
kind: ConstraintKind,
expr: Expr,
}
#[derive(Clone, Copy)]
enum ConstraintKind {
Gt,
Gte,
Lt,
Lte,
}
impl ConstraintKind {
fn from_path(path: &syn::Path) -> Option<Self> {
if path.is_ident("gt") {
Some(Self::Gt)
} else if path.is_ident("gte") {
Some(Self::Gte)
} else if path.is_ident("lt") {
Some(Self::Lt)
} else if path.is_ident("lte") {
Some(Self::Lte)
} else {
None
}
}
fn method(self) -> syn::Ident {
match self {
Self::Gt => format_ident!("gt"),
Self::Gte => format_ident!("gte"),
Self::Lt => format_ident!("lt"),
Self::Lte => format_ident!("lte"),
}
}
}
fn schema_expr(ty: &Type, attrs: &FieldAttrs) -> Result<TokenStream2> {
let mut tokens = quote! {
let mut schema = <#ty as ::decodal::DecodalSchema>::decodal_schema();
};
for constraint in &attrs.constraints {
let method = constraint.kind.method();
let expr = &constraint.expr;
tokens.extend(quote! {
schema = schema.#method(#expr);
});
}
if let Some(default) = &attrs.default {
let default_expr = match default {
DefaultAttr::Default => quote! { <#ty as ::core::default::Default>::default() },
DefaultAttr::Expr(expr) => quote! { (#expr) },
};
tokens.extend(quote! {
schema = schema
.default(::decodal::IntoHostValue::into_host_value(#default_expr))
.expect("Decodal derive generated a valid default");
});
}
tokens.extend(quote! { schema });
Ok(tokens)
}
fn decode_expr(ty: &Type, path: &str, attrs: &FieldAttrs) -> TokenStream2 {
let missing = if let Some(default) = &attrs.default {
match default {
DefaultAttr::Default => quote! { <#ty as ::core::default::Default>::default() },
DefaultAttr::Expr(expr) => quote! { ::core::convert::Into::into(#expr) },
}
} else {
quote! { return Err(::decodal::DecodeError::new(#path, "missing field")); }
};
quote! {
if let Some(value) = ::decodal::data_at_path(data, #path) {
<#ty as ::decodal::DecodalDecode>::decodal_decode(value)
.map_err(|error| ::decodal::prefix_decode_error(#path, error))?
} else {
#missing
}
}
}