2024-11-23 23:25:14 +00:00
|
|
|
use std::{collections::HashSet, fmt::Write as _, fs::OpenOptions, io::Write as _};
|
2024-09-08 22:17:02 +00:00
|
|
|
|
|
|
|
use proc_macro::TokenStream;
|
2024-10-22 09:09:20 +00:00
|
|
|
use proc_macro2::Span;
|
2024-09-08 22:17:02 +00:00
|
|
|
use quote::ToTokens;
|
2024-10-22 09:09:20 +00:00
|
|
|
use syn::{
|
2024-12-15 00:05:47 -05:00
|
|
|
parse::Parser, punctuated::Punctuated, spanned::Spanned, Error, Expr, ExprLit, Field, Fields,
|
|
|
|
FieldsNamed, ItemStruct, Lit, Meta, MetaList, MetaNameValue, Type, TypePath,
|
2024-10-22 09:09:20 +00:00
|
|
|
};
|
2024-09-08 22:17:02 +00:00
|
|
|
|
2024-11-23 23:25:14 +00:00
|
|
|
use crate::{
|
2025-01-10 06:59:12 +00:00
|
|
|
utils::{get_simple_settings, is_cargo_build, is_cargo_test},
|
2024-11-23 23:25:14 +00:00
|
|
|
Result,
|
|
|
|
};
|
2024-09-08 22:17:02 +00:00
|
|
|
|
2024-10-22 09:09:20 +00:00
|
|
|
const UNDOCUMENTED: &str = "# This item is undocumented. Please contribute documentation for it.";
|
|
|
|
|
2024-09-08 22:17:02 +00:00
|
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
|
|
pub(super) fn example_generator(input: ItemStruct, args: &[Meta]) -> Result<TokenStream> {
|
2025-01-10 06:59:12 +00:00
|
|
|
if is_cargo_build() && !is_cargo_test() {
|
2024-09-08 22:17:02 +00:00
|
|
|
generate_example(&input, args)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(input.to_token_stream().into())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
|
|
#[allow(unused_variables)]
|
2024-10-22 22:16:59 +00:00
|
|
|
fn generate_example(input: &ItemStruct, args: &[Meta]) -> Result<()> {
|
2024-11-23 23:25:14 +00:00
|
|
|
let settings = get_simple_settings(args);
|
2024-10-22 22:16:59 +00:00
|
|
|
|
2024-12-15 00:05:47 -05:00
|
|
|
let filename = settings.get("filename").ok_or_else(|| {
|
|
|
|
Error::new(args[0].span(), "missing required 'filename' attribute argument")
|
|
|
|
})?;
|
2024-10-22 22:16:59 +00:00
|
|
|
|
|
|
|
let undocumented = settings
|
|
|
|
.get("undocumented")
|
|
|
|
.map_or(UNDOCUMENTED, String::as_str);
|
|
|
|
|
|
|
|
let ignore: HashSet<&str> = settings
|
|
|
|
.get("ignore")
|
|
|
|
.map_or("", String::as_str)
|
|
|
|
.split(' ')
|
|
|
|
.collect();
|
|
|
|
|
2024-12-15 00:05:47 -05:00
|
|
|
let section = settings.get("section").ok_or_else(|| {
|
|
|
|
Error::new(args[0].span(), "missing required 'section' attribute argument")
|
|
|
|
})?;
|
2024-10-22 22:16:59 +00:00
|
|
|
|
|
|
|
let mut file = OpenOptions::new()
|
|
|
|
.write(true)
|
|
|
|
.create(section == "global")
|
|
|
|
.truncate(section == "global")
|
|
|
|
.append(section != "global")
|
|
|
|
.open(filename)
|
2024-12-15 00:05:47 -05:00
|
|
|
.map_err(|e| {
|
|
|
|
Error::new(
|
|
|
|
Span::call_site(),
|
|
|
|
format!("Failed to open config file for generation: {e}"),
|
|
|
|
)
|
|
|
|
})?;
|
2024-10-22 09:09:20 +00:00
|
|
|
|
2024-10-22 22:16:59 +00:00
|
|
|
if let Some(header) = settings.get("header") {
|
|
|
|
file.write_all(header.as_bytes())
|
|
|
|
.expect("written to config file");
|
|
|
|
}
|
|
|
|
|
|
|
|
file.write_fmt(format_args!("\n[{section}]\n"))
|
2024-10-22 09:09:20 +00:00
|
|
|
.expect("written to config file");
|
|
|
|
|
2024-12-15 00:05:47 -05:00
|
|
|
if let Fields::Named(FieldsNamed { named, .. }) = &input.fields {
|
2024-09-08 22:17:02 +00:00
|
|
|
for field in named {
|
|
|
|
let Some(ident) = &field.ident else {
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
2024-10-22 22:16:59 +00:00
|
|
|
if ignore.contains(ident.to_string().as_str()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-10-22 09:09:20 +00:00
|
|
|
let Some(type_name) = get_type_name(field) else {
|
2024-09-08 22:17:02 +00:00
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
2024-10-22 09:09:20 +00:00
|
|
|
let doc = get_doc_comment(field)
|
2024-10-22 22:16:59 +00:00
|
|
|
.unwrap_or_else(|| undocumented.into())
|
2024-10-22 09:09:20 +00:00
|
|
|
.trim_end()
|
|
|
|
.to_owned();
|
|
|
|
|
|
|
|
let doc = if doc.ends_with('#') {
|
|
|
|
format!("{doc}\n")
|
|
|
|
} else {
|
|
|
|
format!("{doc}\n#\n")
|
|
|
|
};
|
|
|
|
|
|
|
|
let default = get_doc_default(field)
|
|
|
|
.or_else(|| get_default(field))
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
let default = if !default.is_empty() {
|
|
|
|
format!(" {default}")
|
|
|
|
} else {
|
|
|
|
default
|
2024-09-08 22:17:02 +00:00
|
|
|
};
|
|
|
|
|
2024-10-22 09:09:20 +00:00
|
|
|
file.write_fmt(format_args!("\n{doc}"))
|
|
|
|
.expect("written to config file");
|
|
|
|
|
|
|
|
file.write_fmt(format_args!("#{ident} ={default}\n"))
|
|
|
|
.expect("written to config file");
|
2024-09-08 22:17:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-22 22:16:59 +00:00
|
|
|
if let Some(footer) = settings.get("footer") {
|
|
|
|
file.write_all(footer.as_bytes())
|
|
|
|
.expect("written to config file");
|
|
|
|
}
|
|
|
|
|
2024-09-08 22:17:02 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2024-10-22 09:09:20 +00:00
|
|
|
fn get_default(field: &Field) -> Option<String> {
|
|
|
|
for attr in &field.attrs {
|
2024-12-15 00:05:47 -05:00
|
|
|
let Meta::List(MetaList { path, tokens, .. }) = &attr.meta else {
|
2024-10-22 09:09:20 +00:00
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
2024-10-27 00:30:30 +00:00
|
|
|
if path
|
2024-10-22 09:09:20 +00:00
|
|
|
.segments
|
|
|
|
.iter()
|
|
|
|
.next()
|
2024-11-10 02:29:45 +00:00
|
|
|
.is_none_or(|s| s.ident != "serde")
|
2024-10-22 09:09:20 +00:00
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let Some(arg) = Punctuated::<Meta, syn::Token![,]>::parse_terminated
|
|
|
|
.parse(tokens.clone().into())
|
|
|
|
.ok()?
|
|
|
|
.iter()
|
|
|
|
.next()
|
|
|
|
.cloned()
|
|
|
|
else {
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
|
|
|
match arg {
|
2024-12-15 00:05:47 -05:00
|
|
|
| Meta::NameValue(MetaNameValue {
|
|
|
|
value: Expr::Lit(ExprLit { lit: Lit::Str(str), .. }),
|
2024-10-22 09:09:20 +00:00
|
|
|
..
|
|
|
|
}) => {
|
|
|
|
match str.value().as_str() {
|
2024-12-15 00:05:47 -05:00
|
|
|
| "HashSet::new" | "Vec::new" | "RegexSet::empty" => Some("[]".to_owned()),
|
|
|
|
| "true_fn" => return Some("true".to_owned()),
|
|
|
|
| _ => return None,
|
2024-10-22 09:09:20 +00:00
|
|
|
};
|
|
|
|
},
|
2024-12-15 00:05:47 -05:00
|
|
|
| Meta::Path { .. } => return Some("false".to_owned()),
|
|
|
|
| _ => return None,
|
2024-10-22 09:09:20 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_doc_default(field: &Field) -> Option<String> {
|
|
|
|
for attr in &field.attrs {
|
2024-12-15 00:05:47 -05:00
|
|
|
let Meta::NameValue(MetaNameValue { path, value, .. }) = &attr.meta else {
|
2024-10-22 09:09:20 +00:00
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
2024-11-10 02:29:45 +00:00
|
|
|
if path.segments.iter().next().is_none_or(|s| s.ident != "doc") {
|
2024-10-22 09:09:20 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-12-15 00:05:47 -05:00
|
|
|
let Expr::Lit(ExprLit { lit, .. }) = &value else {
|
2024-10-22 09:09:20 +00:00
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
|
|
|
let Lit::Str(token) = &lit else {
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
|
|
|
let value = token.value();
|
|
|
|
if !value.trim().starts_with("default:") {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
return value
|
|
|
|
.split_once(':')
|
|
|
|
.map(|(_, v)| v)
|
|
|
|
.map(str::trim)
|
|
|
|
.map(ToOwned::to_owned);
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
2024-09-08 22:17:02 +00:00
|
|
|
fn get_doc_comment(field: &Field) -> Option<String> {
|
|
|
|
let mut out = String::new();
|
|
|
|
for attr in &field.attrs {
|
2024-12-15 00:05:47 -05:00
|
|
|
let Meta::NameValue(MetaNameValue { path, value, .. }) = &attr.meta else {
|
2024-09-08 22:17:02 +00:00
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
2024-11-10 02:29:45 +00:00
|
|
|
if path.segments.iter().next().is_none_or(|s| s.ident != "doc") {
|
2024-09-08 22:17:02 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-12-15 00:05:47 -05:00
|
|
|
let Expr::Lit(ExprLit { lit, .. }) = &value else {
|
2024-09-08 22:17:02 +00:00
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
|
|
|
let Lit::Str(token) = &lit else {
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
2024-10-22 09:09:20 +00:00
|
|
|
let value = token.value();
|
|
|
|
if value.trim().starts_with("default:") {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
writeln!(&mut out, "#{value}").expect("wrote to output string buffer");
|
2024-09-08 22:17:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
(!out.is_empty()).then_some(out)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_type_name(field: &Field) -> Option<String> {
|
2024-12-15 00:05:47 -05:00
|
|
|
let Type::Path(TypePath { path, .. }) = &field.ty else {
|
2024-09-08 22:17:02 +00:00
|
|
|
return None;
|
|
|
|
};
|
|
|
|
|
|
|
|
path.segments
|
|
|
|
.iter()
|
|
|
|
.next()
|
|
|
|
.map(|segment| segment.ident.to_string())
|
|
|
|
}
|