✨ Añade AutoDefault para derivar Default avanzado
This commit is contained in:
parent
bceb43e6d0
commit
3bb2355b4f
11 changed files with 303 additions and 8 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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 = """\
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
4
helpers/pagetop-macros/src/smart_default.rs
Normal file
4
helpers/pagetop-macros/src/smart_default.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod body_impl;
|
||||||
|
|
||||||
|
mod default_attr;
|
||||||
|
mod util;
|
158
helpers/pagetop-macros/src/smart_default/body_impl.rs
Normal file
158
helpers/pagetop-macros/src/smart_default/body_impl.rs
Normal 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(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
89
helpers/pagetop-macros/src/smart_default/default_attr.rs
Normal file
89
helpers/pagetop-macros/src/smart_default/default_attr.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
21
helpers/pagetop-macros/src/smart_default/util.rs
Normal file
21
helpers/pagetop-macros/src/smart_default/util.rs
Normal 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)
|
||||||
|
}
|
|
@ -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 *********************************************************************************************
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue