1use std::str::FromStr;
2
3use convert_case::Casing;
4use paste::paste;
5use proc_macro2::{TokenStream, TokenTree};
6use quote::quote;
7use syn::spanned::Spanned;
8
9struct RecordParams(syn::Type, syn::Ident, Option<syn::LitStr>);
10impl syn::parse::Parse for RecordParams {
11 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
12 let value_type = input.parse()?;
13 let _ = input.parse::<syn::Token![,]>();
14 let ident = input.parse()?;
15 let _ = input.parse::<syn::Token![,]>();
16 let header = input.parse().ok();
17 Ok(RecordParams(value_type, ident, header))
18 }
19}
20
21const GENERIC_ERROR_MSG: &str = "Expected to be used with an enum";
22
23#[proc_macro_attribute]
24pub fn record(
25 params: proc_macro::TokenStream,
26 ast: proc_macro::TokenStream,
27) -> proc_macro::TokenStream {
28 let RecordParams(value_type, ident, header) = syn::parse2(params.into()).expect("Invalid params");
29
30 let ast_tokens: TokenStream = ast.clone().into();
31
32 let mut ast_iter = ast_tokens.clone().into_iter();
33
34 let enum_props_group = ast_iter
35 .find_map(|next| {
36 if let TokenTree::Group(group) = next {
37 Some(group)
38 } else {
39 None
40 }
41 })
42 .expect(GENERIC_ERROR_MSG);
43
44 let struct_props: Vec<TokenStream> = enum_props_group
45 .stream()
46 .into_iter()
47 .filter_map(|token| {
48 if let TokenTree::Ident(ident) = token {
49 let ident_str = ident.to_string();
50 let snake = ident_str.to_case(convert_case::Case::Snake);
51 let snake_ident = syn::Ident::new(&snake, ident_str.span());
52
53 Some(quote! {
54 #snake_ident: #value_type ,
55 })
56 } else {
57 None
58 }
59 })
60 .collect();
61
62 let header_token = header
63 .map(|token| TokenStream::from_str(&token.value()).expect(GENERIC_ERROR_MSG))
64 .unwrap_or(TokenStream::new());
65
66 let output = quote! {
67 #ast_tokens
68 #header_token
69 struct #ident {
70 #(#struct_props)*
71 }
72 };
73
74 output.into()
75}