Añade AutoDefault para derivar Default avanzado

This commit is contained in:
Manuel Cillero 2025-07-07 21:23:06 +02:00
parent bceb43e6d0
commit 3bb2355b4f
11 changed files with 303 additions and 8 deletions

2
Cargo.lock generated
View file

@ -937,7 +937,7 @@ dependencies = [
[[package]] [[package]]
name = "pagetop-macros" name = "pagetop-macros"
version = "0.0.2" version = "0.0.3"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "pagetop-macros" name = "pagetop-macros"
version = "0.0.2" version = "0.0.3"
edition = "2021" edition = "2021"
description = """\ description = """\

View file

@ -11,9 +11,12 @@
## Descripción general ## Descripción general
Entre sus macros se incluye una adaptación de [maud-macros](https://crates.io/crates/maud_macros) Entre sus macros se incluye una adaptación de [maud-macros](https://crates.io/crates/maud_macros)
([0.25.0](https://github.com/lambda-fairy/maud/tree/v0.25.0/maud_macros)) de ([0.27.0](https://github.com/lambda-fairy/maud/tree/v0.27.0/maud_macros)) de
[Chris Wong](https://crates.io/users/lambda-fairy) para no tener que referenciar `maud` en las [Chris Wong](https://crates.io/users/lambda-fairy) y una versión renombrada de
dependencias del archivo `Cargo.toml` de cada proyecto `PageTop`. [SmartDefault](https://crates.io/crates/smart_default) (0.7.1) de
[Jane Doe](https://crates.io/users/jane-doe), llamada `AutoDefault`. Estas macros eliminan la
necesidad de referenciar `maud` o `smart_default` en las dependencias del archivo `Cargo.toml` de
cada proyecto `PageTop`.
## Sobre PageTop ## Sobre PageTop

View file

@ -15,16 +15,34 @@
//! y configurables, basadas en HTML, CSS y JavaScript. //! y configurables, basadas en HTML, CSS y JavaScript.
mod maud; mod maud;
mod smart_default;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
use syn::{parse_macro_input, DeriveInput};
/// Macro para escribir plantillas HTML ([Maud](https://docs.rs/maud)). /// Macro para escribir plantillas HTML (basada en [Maud](https://docs.rs/maud)).
#[proc_macro] #[proc_macro]
pub fn html(input: TokenStream) -> TokenStream { pub fn html(input: TokenStream) -> TokenStream {
maud::expand(input.into()).into() maud::expand(input.into()).into()
} }
/// Deriva [`Default`] con atributos personalizados (basada en
/// [SmartDefault](https://docs.rs/smart-default)).
///
/// Al derivar una estructura con *AutoDefault* se genera automáticamente la implementación de
/// [`Default`]. Aunque, a diferencia de un simple `#[derive(Default)]`, el atributo
/// `#[derive(AutoDefault)]` permite usar anotaciones en los campos como `#[default = "..."]`,
/// funcionando incluso en estructuras con campos que no implementan [`Default`] o en *enums*.
#[proc_macro_derive(AutoDefault, attributes(default))]
pub fn derive_auto_default(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match smart_default::body_impl::impl_my_derive(&input) {
Ok(output) => output.into(),
Err(error) => error.to_compile_error().into(),
}
}
/// Define una función `main` asíncrona como punto de entrada de `PageTop`. /// Define una función `main` asíncrona como punto de entrada de `PageTop`.
/// ///
/// # Ejemplos /// # Ejemplos

View file

@ -0,0 +1,4 @@
pub mod body_impl;
mod default_attr;
mod util;

View file

@ -0,0 +1,158 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::Error;
use syn::spanned::Spanned;
use syn::DeriveInput;
use crate::smart_default::default_attr::{ConversionStrategy, DefaultAttr};
use crate::smart_default::util::find_only;
pub fn impl_my_derive(input: &DeriveInput) -> Result<TokenStream, Error> {
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let (default_expr, doc) = match input.data {
syn::Data::Struct(ref body) => {
let (body_assignment, _doc) = default_body_tt(&body.fields)?;
(
quote! {
#name #body_assignment
},
format!("Returns a `{}` default.", name),
)
}
syn::Data::Enum(ref body) => {
let default_variant = find_only(body.variants.iter(), |variant| {
if let Some(meta) = DefaultAttr::find_in_attributes(&variant.attrs)? {
if meta.code.is_none() {
Ok(true)
} else {
Err(Error::new(
meta.code.span(),
"Attribute #[default] on variants should have no value",
))
}
} else {
Ok(false)
}
})?
.ok_or_else(|| Error::new(input.span(), "No default variant"))?;
let default_variant_name = &default_variant.ident;
let (body_assignment, _doc) = default_body_tt(&default_variant.fields)?;
(
quote! {
#name :: #default_variant_name #body_assignment
},
format!("Returns a `{}::{}` default.", name, default_variant_name),
)
}
syn::Data::Union(_) => {
panic!()
}
};
Ok(quote! {
#[automatically_derived]
impl #impl_generics Default for #name #ty_generics #where_clause {
#[doc = #doc]
fn default() -> Self {
#default_expr
}
}
})
}
/// Return a token-tree for the default "body" - the part after the name that contains the values.
/// That is, the `{ ... }` part for structs, the `(...)` part for tuples, and nothing for units.
fn default_body_tt(body: &syn::Fields) -> Result<(TokenStream, String), Error> {
let mut doc = String::new();
use std::fmt::Write;
let body_tt = match body {
syn::Fields::Named(ref fields) => {
doc.push_str(" {");
let result = {
let field_assignments = fields
.named
.iter()
.map(|field| {
let field_name = field.ident.as_ref();
let (default_value, default_doc) = field_default_expr_and_doc(field)?;
write!(
&mut doc,
"\n {}: {},",
field_name.expect("field value in struct is empty"),
default_doc
)
.unwrap();
// let default_value = default_value.into_token_stream();
Ok(quote! { #field_name : #default_value })
})
.collect::<Result<Vec<_>, Error>>()?;
quote! {
{
#( #field_assignments ),*
}
}
};
if doc.ends_with(',') {
doc.pop();
doc.push('\n');
};
doc.push('}');
result
}
syn::Fields::Unnamed(ref fields) => {
doc.push('(');
let result = {
let field_assignments = fields
.unnamed
.iter()
.map(|field| {
let (default_value, default_doc) = field_default_expr_and_doc(field)?;
write!(&mut doc, "{}, ", default_doc).unwrap();
Ok(default_value)
})
.collect::<Result<Vec<TokenStream>, Error>>()?;
quote! {
(
#( #field_assignments ),*
)
}
};
if doc.ends_with(", ") {
doc.pop();
doc.pop();
};
doc.push(')');
result
}
&syn::Fields::Unit => quote! {},
};
Ok((body_tt, doc))
}
/// Return a default expression for a field based on it's `#[default = "..."]` attribute. Panic
/// if there is more than one, of if there is a `#[default]` attribute without value.
fn field_default_expr_and_doc(field: &syn::Field) -> Result<(TokenStream, String), Error> {
if let Some(default_attr) = DefaultAttr::find_in_attributes(&field.attrs)? {
let conversion_strategy = default_attr.conversion_strategy();
let field_value = default_attr.code.ok_or_else(|| {
Error::new(field.span(), "Expected #[default = ...] or #[default(...)]")
})?;
let field_value = match conversion_strategy {
ConversionStrategy::NoConversion => field_value,
ConversionStrategy::Into => quote!((#field_value).into()),
};
let field_doc = format!("{}", field_value);
Ok((field_value, field_doc))
} else {
Ok((
quote! {
Default::default()
},
"Default::default()".to_owned(),
))
}
}

View file

@ -0,0 +1,89 @@
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{parse::Error, MetaNameValue};
use crate::smart_default::util::find_only;
#[derive(Debug, Clone, Copy)]
pub enum ConversionStrategy {
NoConversion,
Into,
}
pub struct DefaultAttr {
pub code: Option<TokenStream>,
conversion_strategy: Option<ConversionStrategy>,
}
impl DefaultAttr {
pub fn find_in_attributes(attrs: &[syn::Attribute]) -> Result<Option<Self>, Error> {
if let Some(default_attr) =
find_only(attrs.iter(), |attr| Ok(attr.path().is_ident("default")))?
{
match &default_attr.meta {
syn::Meta::Path(_) => Ok(Some(Self {
code: None,
conversion_strategy: None,
})),
syn::Meta::List(meta) => {
// If the meta contains exactly (_code = "...") take the string literal as the
// expression
if let Ok(ParseCodeHack(code_hack)) = syn::parse(meta.tokens.clone().into()) {
Ok(Some(Self {
code: Some(code_hack),
conversion_strategy: Some(ConversionStrategy::NoConversion),
}))
} else {
Ok(Some(Self {
code: Some(meta.tokens.clone()),
conversion_strategy: None,
}))
}
}
syn::Meta::NameValue(MetaNameValue { value, .. }) => Ok(Some(Self {
code: Some(value.into_token_stream()),
conversion_strategy: None,
})),
}
} else {
Ok(None)
}
}
pub fn conversion_strategy(&self) -> ConversionStrategy {
if let Some(conversion_strategy) = self.conversion_strategy {
// Conversion strategy already set
return conversion_strategy;
}
let code = if let Some(code) = &self.code {
code
} else {
// #[default] - so no conversion (`Default::default()` already has the correct type)
return ConversionStrategy::NoConversion;
};
match syn::parse::<syn::Lit>(code.clone().into()) {
Ok(syn::Lit::Str(_)) | Ok(syn::Lit::ByteStr(_)) => {
// A string literal - so we need a conversion in case we need to make it a `String`
return ConversionStrategy::Into;
}
_ => {}
}
// Not handled by one of the rules, so we don't convert it to avoid causing trouble
ConversionStrategy::NoConversion
}
}
struct ParseCodeHack(TokenStream);
impl syn::parse::Parse for ParseCodeHack {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let ident: syn::Ident = input.parse()?;
if ident != "_code" {
return Err(Error::new(ident.span(), "Expected `_code`"));
}
input.parse::<syn::token::Eq>()?;
let code: syn::LitStr = input.parse()?;
let code: TokenStream = code.parse()?;
Ok(ParseCodeHack(code))
}
}

View file

@ -0,0 +1,21 @@
use syn::parse::Error;
use syn::spanned::Spanned;
/// Return the value that fulfills the predicate if there is one in the slice. Panic if there is
/// more than one.
pub fn find_only<T, F>(iter: impl Iterator<Item = T>, pred: F) -> Result<Option<T>, Error>
where
T: Spanned,
F: Fn(&T) -> Result<bool, Error>,
{
let mut result = None;
for item in iter {
if pred(&item)? {
if result.is_some() {
return Err(Error::new(item.span(), "Multiple defaults"));
}
result = Some(item);
}
}
Ok(result)
}

View file

@ -32,7 +32,7 @@
// RE-EXPORTED ************************************************************************************* // RE-EXPORTED *************************************************************************************
pub use pagetop_macros::{html, main, test}; pub use pagetop_macros::{html, main, test, AutoDefault};
// API ********************************************************************************************* // API *********************************************************************************************

View file

@ -4,6 +4,8 @@
pub use crate::{html, main, test}; pub use crate::{html, main, test};
pub use crate::AutoDefault;
// MACROS. // MACROS.
// crate::config // crate::config

View file

@ -1,4 +1,4 @@
//! Gestión del servidor y servicios web ([Actix Web](https://docs.rs/actix-web)). //! Gestión del servidor y servicios web (con [Actix Web](https://docs.rs/actix-web)).
pub use actix_web::body::BoxBody; pub use actix_web::body::BoxBody;
pub use actix_web::dev::Server; pub use actix_web::dev::Server;