mod args;
mod config_doc;
mod decode_vpl;
use crate::{
args::Args,
config_doc::{angle_inner, collect_doc, is_option, is_primitive_like, is_url_path, path_ident, serde_rename},
decode_vpl::decode_struct,
};
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) => match decode_struct(input, data_struct) {
Ok(tokens) => tokens,
Err(err) => err.to_compile_error(),
},
_ => syn::Error::new_spanned(&input, "VPLDecode can only be derived for structs").to_compile_error(),
};
TokenStream::from(expanded)
}
struct ConfigDocRow {
key: String,
ty: syn::Type,
doc: String,
is_vec: bool,
inner_ty_vec: Option<syn::Type>,
is_nested_struct: bool,
demo_value: Option<String>,
}
fn extract_config_doc_row(f: &syn::Field) -> ConfigDocRow {
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_opt = 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 = is_url_path(&f.ty);
let mut demo_value = None;
for attr in &f.attrs {
if attr.path().is_ident("config_demo")
&& demo_value.is_none()
&& let Ok(lit) = attr.parse_args::<syn::LitStr>()
{
demo_value = Some(lit.value());
}
}
let is_nested_struct =
!is_opt && !is_vec && !is_map && path_ident(&f.ty).is_some() && !is_primitive_like(&f.ty) && !is_url;
ConfigDocRow {
key,
ty,
doc,
is_vec,
inner_ty_vec,
is_nested_struct,
demo_value,
}
}
fn build_field_yaml_block(r: &ConfigDocRow) -> proc_macro2::TokenStream {
let key = &r.key;
let ty = &r.ty;
let doc = &r.doc;
let doc_lit = syn::LitStr::new(doc, Span::call_site());
let demo_lit = r.demo_value.as_ref().map(|d| syn::LitStr::new(d, Span::call_site()));
let key_lit = syn::LitStr::new(key, Span::call_site());
let mut output = 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');
}
}
}
#[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 syn::Data::Struct(data) = &input.data else {
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();
}
};
let rows: Vec<_> = fields.iter().map(extract_config_doc_row).collect();
let field_yaml_blocks: Vec<_> = rows.iter().map(build_field_yaml_block).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().is_some_and(|s| s.ident == "Pin")
)
);
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()
}