diff --git a/Cargo.lock b/Cargo.lock index cf974c8..3fe4d51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -937,7 +937,7 @@ dependencies = [ [[package]] name = "pagetop-macros" -version = "0.0.2" +version = "0.0.3" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/helpers/pagetop-macros/Cargo.toml b/helpers/pagetop-macros/Cargo.toml index ea6404b..9f914bf 100644 --- a/helpers/pagetop-macros/Cargo.toml +++ b/helpers/pagetop-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pagetop-macros" -version = "0.0.2" +version = "0.0.3" edition = "2021" description = """\ diff --git a/helpers/pagetop-macros/README.md b/helpers/pagetop-macros/README.md index 0679966..e41dd0e 100644 --- a/helpers/pagetop-macros/README.md +++ b/helpers/pagetop-macros/README.md @@ -11,9 +11,12 @@ ## Descripción general 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 -[Chris Wong](https://crates.io/users/lambda-fairy) para no tener que referenciar `maud` en las -dependencias del archivo `Cargo.toml` de cada proyecto `PageTop`. +([0.27.0](https://github.com/lambda-fairy/maud/tree/v0.27.0/maud_macros)) de +[Chris Wong](https://crates.io/users/lambda-fairy) y una versión renombrada de +[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 diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index e0f3bad..eecebed 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -15,16 +15,34 @@ //! y configurables, basadas en HTML, CSS y JavaScript. mod maud; +mod smart_default; use proc_macro::TokenStream; 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] pub fn html(input: TokenStream) -> TokenStream { 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`. /// /// # Ejemplos diff --git a/helpers/pagetop-macros/src/smart_default.rs b/helpers/pagetop-macros/src/smart_default.rs new file mode 100644 index 0000000..87177dc --- /dev/null +++ b/helpers/pagetop-macros/src/smart_default.rs @@ -0,0 +1,4 @@ +pub mod body_impl; + +mod default_attr; +mod util; diff --git a/helpers/pagetop-macros/src/smart_default/body_impl.rs b/helpers/pagetop-macros/src/smart_default/body_impl.rs new file mode 100644 index 0000000..6a76f90 --- /dev/null +++ b/helpers/pagetop-macros/src/smart_default/body_impl.rs @@ -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 { + 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::, 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::, 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(), + )) + } +} diff --git a/helpers/pagetop-macros/src/smart_default/default_attr.rs b/helpers/pagetop-macros/src/smart_default/default_attr.rs new file mode 100644 index 0000000..8487fc0 --- /dev/null +++ b/helpers/pagetop-macros/src/smart_default/default_attr.rs @@ -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, + conversion_strategy: Option, +} + +impl DefaultAttr { + pub fn find_in_attributes(attrs: &[syn::Attribute]) -> Result, 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::(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 { + let ident: syn::Ident = input.parse()?; + if ident != "_code" { + return Err(Error::new(ident.span(), "Expected `_code`")); + } + input.parse::()?; + let code: syn::LitStr = input.parse()?; + let code: TokenStream = code.parse()?; + Ok(ParseCodeHack(code)) + } +} diff --git a/helpers/pagetop-macros/src/smart_default/util.rs b/helpers/pagetop-macros/src/smart_default/util.rs new file mode 100644 index 0000000..0d4b247 --- /dev/null +++ b/helpers/pagetop-macros/src/smart_default/util.rs @@ -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(iter: impl Iterator, pred: F) -> Result, Error> +where + T: Spanned, + F: Fn(&T) -> Result, +{ + 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) +} diff --git a/src/lib.rs b/src/lib.rs index 2f97351..b9b2a4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,7 @@ // RE-EXPORTED ************************************************************************************* -pub use pagetop_macros::{html, main, test}; +pub use pagetop_macros::{html, main, test, AutoDefault}; // API ********************************************************************************************* diff --git a/src/prelude.rs b/src/prelude.rs index eb59c4f..872447a 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -4,6 +4,8 @@ pub use crate::{html, main, test}; +pub use crate::AutoDefault; + // MACROS. // crate::config diff --git a/src/service.rs b/src/service.rs index e6904b8..90b1375 100644 --- a/src/service.rs +++ b/src/service.rs @@ -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::dev::Server;