mod args;
mod config_doc;
mod decode_vpl;
use crate::{args::*, config_doc::*, decode_vpl::*};
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::{ToTokens, quote};
use syn::{Fields, parse_macro_input, spanned::Spanned};
#[proc_macro_derive(VPLDecode)]
pub fn decode_vpl(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput);
let expanded = match input.data.clone() {
syn::Data::Struct(data_struct) => decode_struct(input, data_struct),
_ => panic!("VPLDecode can only be derived for structs, but: {:?}", input.data),
};
TokenStream::from(expanded)
}
#[proc_macro_derive(ConfigDoc, attributes(config, config_demo))]
pub fn derive_config_doc(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput);
let name = &input.ident;
let data = match &input.data {
syn::Data::Struct(ds) => ds,
_ => {
return syn::Error::new(
input.span(),
"ConfigDoc can only be derived for structs with named fields",
)
.to_compile_error()
.into();
}
};
let fields = match &data.fields {
Fields::Named(named) => &named.named,
_ => {
return syn::Error::new(
data.struct_token.span(),
"ConfigDoc requires a struct with named fields",
)
.to_compile_error()
.into();
}
};
struct Row {
ident: syn::Ident,
key: String,
ty: syn::Type,
doc: String,
is_vec: bool,
inner_ty_vec: Option<syn::Type>, is_nested_struct: bool,
demo_value: Option<String>,
}
let mut rows = Vec::<Row>::new();
for f in fields {
let ident = f.ident.clone().expect("named field");
let key = serde_rename(&f.attrs).unwrap_or_else(|| ident.to_string());
let ty = f.ty.clone();
let doc = collect_doc(&f.attrs);
let is_option = is_option(&f.ty);
let mut is_vec = false;
let mut is_map = false;
let mut inner_ty_vec = None;
if let Some(id) = path_ident(&f.ty) {
let id_s = id.to_string();
if id_s == "Vec" {
is_vec = true;
if let Some(mut inners) = angle_inner(&f.ty)
&& let Some(inner) = inners.pop()
{
inner_ty_vec = Some(inner.clone());
}
} else if id_s == "HashMap" {
is_map = true;
}
}
let is_url_path = is_url_path(&f.ty);
let mut demo_value = None;
for attr in &f.attrs {
if attr.path().is_ident("config_demo") {
if demo_value.is_none()
&& let Ok(lit) = attr.parse_args::<syn::LitStr>()
{
demo_value = Some(lit.value());
continue;
}
}
}
let is_nested_struct =
!is_option && !is_vec && !is_map && path_ident(&f.ty).is_some() && !is_primitive_like(&f.ty) && !is_url_path;
rows.push(Row {
ident,
key,
ty,
doc,
is_vec,
inner_ty_vec,
is_nested_struct,
demo_value,
});
}
let field_yaml_blocks: Vec<_> = rows
.iter()
.map(|r| {
use proc_macro2::TokenStream as TokenStream2;
let _ident = &r.ident;
let key = &r.key;
let ty = &r.ty;
let doc = &r.doc;
let doc_lit = syn::LitStr::new(doc, Span::call_site());
let demo_value = r.demo_value.as_ref();
let demo_lit = demo_value.map(|d| syn::LitStr::new(d, Span::call_site()));
let key_lit = syn::LitStr::new(key, Span::call_site());
let mut output: TokenStream2 = quote! {
__s.push_str(&__sp(__indent));
__s.push('\n');
for line in #doc_lit.lines() {
__s.push_str(&__sp(__indent));
__s.push_str("# ");
__s.push_str(line);
__s.push('\n');
}
__s.push_str(&__sp(__indent));
__s.push_str(#key_lit);
__s.push_str(": ");
};
if let Some(demo_lit) = &demo_lit {
output = quote! {
#output
__s.push_str(#demo_lit);
};
} else if r.is_nested_struct {
output = quote! {
#output
__s.push_str("\n");
__s.push_str(&<#ty>::demo_yaml_with_indent(__indent + 2));
};
} else if r.is_vec
&& let Some(inner) = &r.inner_ty_vec
{
output = quote! {
#output
__s.push_str("\n");
let __inner = <#inner>::demo_yaml_with_indent(0);
let mut __first_line_printed = false;
for __line in __inner.lines() {
if !__first_line_printed {
if __line.trim().is_empty() { continue; }
__s.push_str(&__sp(__indent + 2));
__s.push_str("- ");
__first_line_printed = true;
} else {
__s.push_str(&__sp(__indent + 4));
}
__s.push_str(__line);
__s.push('\n');
}
};
}
quote! {
#output
if !__s.ends_with('\n') {
__s.push('\n');
}
}
})
.collect();
let expanded = quote! {
impl #name {
pub(crate) fn demo_yaml_with_indent(__indent: usize) -> String {
let mut __s = String::new();
let __sp = |n: usize| -> String { " ".repeat(n) };
#( {
#field_yaml_blocks
} )*
__s
}
}
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn context(args: TokenStream, input: TokenStream) -> TokenStream {
let Args(move_token, format_args) = parse_macro_input!(args);
let mut input = parse_macro_input!(input as syn::ItemFn);
let body = &input.block;
let return_type = &input.sig.output;
let err = Ident::new("err", Span::mixed_site());
let new_body = if input.sig.asyncness.is_some() {
let return_type = match return_type {
syn::ReturnType::Default => {
return syn::Error::new_spanned(input, "function should return Result")
.to_compile_error()
.into();
}
syn::ReturnType::Type(_, return_type) => return_type,
};
let result = Ident::new("result", Span::mixed_site());
quote! {{
use ::anyhow::Context as _;
let #result: #return_type = (async #move_token { #body }).await;
#result.map_err(|#err| #err.context(format!(#format_args)).into())
}}
} else {
{
let is_pin_return = matches!(
&return_type,
syn::ReturnType::Type(_, ty)
if matches!(ty.as_ref(),
syn::Type::Path(tp) if tp.path.segments.last().map(|s| s.ident == "Pin").unwrap_or(false)
)
);
if is_pin_return {
quote! {{
use ::anyhow::Context as _;
let __fut = (|| #return_type { #body })();
::core::pin::Pin::from(Box::new(async move {
let __res = __fut.await;
__res.map_err(|#err| #err.context(format!(#format_args)).into())
}))
}}
} else {
let force_fn_once = Ident::new("force_fn_once", Span::mixed_site());
quote! {{
use ::anyhow::Context as _;
let #force_fn_once = ::core::iter::empty::<()>();
(#move_token || #return_type {
::core::mem::drop(#force_fn_once);
#body
})().map_err(|#err| #err.context(format!(#format_args)).into())
}}
}
}
};
input.block.stmts = vec![syn::Stmt::Expr(syn::Expr::Verbatim(new_body), None)];
input.into_token_stream().into()
}