use proc_macro::TokenStream;
use darling::ast::Data;
use darling::{FromDeriveInput, FromField};
use quote::{quote, ToTokens};
use scraper::Selector;
use syn::spanned::Spanned;
use syn::{parse_macro_input, Expr};
#[proc_macro_derive(FromHtml, attributes(h2s))]
pub fn derive(input: TokenStream) -> TokenStream {
match FromHtmlStructReceiver::from_derive_input(&parse_macro_input!(input)) {
Ok(struct_receiver) => quote!(#struct_receiver).into(),
Err(e) => TokenStream::from(e.write_errors()),
}
}
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(h2s), supports(struct_any))]
struct FromHtmlStructReceiver {
ident: syn::Ident,
data: Data<(), H2sFieldReceiver>,
}
#[derive(Debug, FromField)]
#[darling(attributes(h2s))]
struct H2sFieldReceiver {
ident: Option<syn::Ident>,
ty: syn::Type,
select: Option<String>,
extractor: Option<Expr>,
attr: Option<String>,
}
impl ToTokens for FromHtmlStructReceiver {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self {
ref ident,
ref data,
} = *self;
let token_stream = match data.as_ref() {
Data::Struct(fields) => {
let field_and_values = fields
.into_iter()
.enumerate()
.map(|(i, r)| r.build_field_and_value(i));
quote! {
impl ::h2s::FromHtml for #ident {
type Error = ::h2s::FieldError;
fn from_html<E>(input: E) -> Result<Self, Self::Error>
where
E: ::h2s::html::HtmlElement
{
Ok(Self{
#(#field_and_values),*
})
}
}
}
}
Data::Enum(_) => {
syn::Error::new(ident.span(), "FromHtml doesn't support enum".to_string())
.to_compile_error()
}
};
tokens.extend(token_stream);
}
}
impl H2sFieldReceiver {
fn build_field_and_value(&self, index: usize) -> proc_macro2::TokenStream {
let (ident, field_name_str) = match &self.ident {
Some(id) => (quote!(#id), id.to_string()),
None => {
let i = syn::Index::from(index);
(quote!(#i), index.to_string())
}
};
let value = self.build_value(&field_name_str);
quote!(#ident: #value)
}
fn build_value(&self, field_name: &String) -> proc_macro2::TokenStream {
let selector = match &self.select {
Some(selector) => {
if Selector::parse(selector).is_err() {
return syn::Error::new(
self.ident
.as_ref()
.and_then(|id| id.span().join(self.ty.span()))
.unwrap_or_else(|| self.ty.span()),
format!("invalid css selector: `{selector}`"),
)
.to_compile_error();
}
quote!(::h2s::element_selector::Select{ selector: #selector.to_string() })
}
None => quote!(::h2s::element_selector::Root),
};
let extraction_method = if let Some(attr) = self.attr.as_ref() {
quote!(::h2s::macro_utils::extraction_method(::h2s::extraction_method::ExtractAttribute{ name: #attr .to_string() }))
} else if let Some(a) = self.extractor.as_ref() {
quote!(::h2s::macro_utils::extraction_method(#a))
} else {
quote!(::h2s::macro_utils::default_extraction_method::<E, _>())
};
quote!({
let field_name = #field_name.to_string();
let selector = #selector;
let extraction_method = #extraction_method;
::h2s::macro_utils::process_field(&input, selector, extraction_method)
.map_err(|error| ::h2s::FieldError {
field_name,
error: Box::new(error),
})?
})
}
}