props_util/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{DeriveInput, Error, Field, LitStr, parse_macro_input, punctuated::Punctuated, token::Comma};
6
7#[proc_macro_derive(Properties, attributes(prop))]
8pub fn parse_prop_derive(input: TokenStream) -> TokenStream {
9    let input = parse_macro_input!(input as DeriveInput);
10    let struct_name = &input.ident;
11
12    match generate_prop_fns(&input) {
13        Ok(prop_impl) => quote! {
14            impl #struct_name { #prop_impl }
15        }
16        .into(),
17        Err(e) => e.to_compile_error().into(),
18    }
19}
20
21fn extract_named_fields(input: &DeriveInput) -> syn::Result<Punctuated<Field, Comma>> {
22    let fields = match &input.data {
23        syn::Data::Struct(data_struct) => match &data_struct.fields {
24            syn::Fields::Named(fields_named) => &fields_named.named,
25            _ => return Err(Error::new_spanned(&input.ident, "Only named structs are allowd")),
26        },
27        _ => return Err(Error::new_spanned(&input.ident, "Only structs can be used on Properties")),
28    };
29
30    Ok(fields.to_owned())
31}
32
33fn generate_result_quote(field_type: &syn::Type, field_name: &proc_macro2::Ident, raw_value_str: proc_macro2::TokenStream, key: LitStr, is_option: bool) -> proc_macro2::TokenStream {
34    match field_type {
35        syn::Type::Path(tpath) if tpath.path.segments.last().is_some_and(|segment| segment.ident == "Vec") => match is_option {
36            false => quote! {
37                #field_name : match #raw_value_str {
38                    Some(val) => Self::parse_vec::<_>(val).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Error Parsing `{}` with value `{}` {}", #key, val, e)))?,
39                    None => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("`{}` value is not configured. Use default or set it in the .properties file", #key)))
40                }
41            },
42            true => quote! {
43                #field_name : match #raw_value_str {
44                    Some(val) => Some(Self::parse_vec::<_>(val).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Error Parsing `{}` with value `{}` {}", #key, val, e)))?),
45                    None => None
46                }
47            },
48        },
49        _ => match is_option {
50            false => quote! {
51                #field_name : match #raw_value_str {
52                    Some(val) => Self::parse(val).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Error Parsing `{}` with value `{}` {}", #key, val, e)))?,
53                    None => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("`{}` value is not configured. Use default or set it in the .properties file", #key)))
54                }
55            },
56            true => quote! {
57                #field_name : match #raw_value_str {
58                    Some(val) => Some(Self::parse(val).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Error Parsing `{}` with value `{}` {}", #key, val, e)))?),
59                    None => None
60                }
61            },
62        },
63    }
64}
65
66fn generate_initalizers(fields: Punctuated<Field, Comma>) -> syn::Result<Vec<proc_macro2::TokenStream>> {
67    let mut init_arr: Vec<proc_macro2::TokenStream> = Vec::new();
68
69    for field in fields {
70        let (key, default) = parse_key_default(&field).map_err(|_| Error::new_spanned(field.clone(), "Expecting `key` and `default` values"))?;
71        let field_name = field.ident.as_ref().to_owned().unwrap();
72        let field_type = &field.ty;
73
74        let raw_value_str = match default {
75            Some(default) => quote! { Some(propmap.get(#key).map(String::as_str).unwrap_or(#default)) },
76            None => quote! { propmap.get(#key).map(String::as_str) },
77        };
78
79        let init = match field_type {
80            syn::Type::Path(tpath) if tpath.path.segments.last().is_some_and(|segment| segment.ident == "Option") => match tpath.path.segments.last().unwrap().to_owned().arguments {
81                syn::PathArguments::AngleBracketed(arguments) if arguments.args.first().is_some() => match arguments.args.first().unwrap() {
82                    syn::GenericArgument::Type(ftype) => generate_result_quote(ftype, field_name, raw_value_str, key, true),
83                    _ => panic!("Option not configured {field_name} properly"),
84                },
85                _ => panic!("Option not configured {field_name} properly"),
86            },
87            _ => generate_result_quote(field_type, field_name, raw_value_str, key, false),
88        };
89
90        init_arr.push(init);
91    }
92
93    Ok(init_arr)
94}
95
96fn generate_prop_fns(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
97    let fields = extract_named_fields(input)?;
98    let init_arr = generate_initalizers(fields)?;
99
100    let new_impl = quote! {
101
102        fn parse_vec<T: std::str::FromStr>(string: &str) -> anyhow::Result<Vec<T>> {
103            Ok(string
104                .split(',')
105                .map(|s| s.trim())
106                .filter(|s| !s.is_empty())
107                .map(|s| s.parse::<T>().map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Error Parsing with value `{s}`"))))
108                .collect::<std::io::Result<Vec<T>>>()?)
109        }
110
111        fn parse<T : std::str::FromStr>(string : &str) -> anyhow::Result<T> {
112            Ok(string.parse::<T>().map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Error Parsing with value `{string}`")))?)
113        }
114
115        pub fn from_file(path : &str) -> std::io::Result<Self> {
116            use std::collections::HashMap;
117            use std::fs;
118            use std::io::{self, ErrorKind}; // Explicitly import ErrorKind
119            use std::path::Path; // Required for AsRef<Path> trait bound
120            use std::{fs::File, io::Read};
121
122            let mut content = String::new();
123
124            let mut file = File::open(path).map_err(|e| std::io::Error::new(e.kind(), format!("Error opening file {}", path)))?;
125            file.read_to_string(&mut content) .map_err(|e| std::io::Error::new(e.kind(), format!("Error Reading File : {}", path)))?;
126
127            let mut propmap = std::collections::HashMap::<String, String>::new();
128            for (line_num, line) in content.lines().enumerate() {
129                let line = line.trim();
130
131                if line.is_empty() || line.starts_with('#') || line.starts_with('!') {
132                    continue;
133                }
134
135                // Find the first '=', handling potential whitespace
136                match line.split_once('=') {
137                    Some((key, value)) => propmap.insert(key.trim().to_string(), value.trim().to_string()),
138                    None => return Err(io::Error::new( ErrorKind::InvalidData, format!("Malformed line {} in '{}' (missing '='): {}", line_num + 1, path, line) )),
139                };
140            }
141
142            Ok(Self { #( #init_arr ),* })
143        }
144
145        pub fn from_hash_map(propmap : &std::collections::HashMap<&str, &str>) -> std::io::Result<Self> {
146            let propmap : std::collections::HashMap<String, String> = propmap.iter().map(|(k, v)| (k.trim().to_string(), v.trim().to_string())).collect();
147            Ok(Self { #( #init_arr ),* })
148        }
149
150        pub fn default() -> std::io::Result<Self> {
151            use std::collections::HashMap;
152            let mut propmap = HashMap::<String, String>::new();
153            Ok(Self { #( #init_arr ),* })
154        }
155    };
156
157    Ok(new_impl)
158}
159
160fn parse_key_default(field: &syn::Field) -> syn::Result<(LitStr, Option<LitStr>)> {
161    let prop_attr = field.attrs.iter().find(|attr| attr.path().is_ident("prop")).ok_or_else(|| {
162        syn::Error::new_spanned(
163            field.ident.as_ref().unwrap(),
164            format!("Field '{}' is missing the #[prop(...)] attribute", field.ident.as_ref().map(|i| i.to_string()).unwrap_or_else(|| "<?>".into())),
165        )
166    })?;
167
168    let mut key: Option<LitStr> = None;
169    let mut default: Option<LitStr> = None;
170
171    // Use parse_nested_meta for more robust parsing of attribute arguments
172    prop_attr.parse_nested_meta(|meta| {
173        if meta.path.is_ident("key") {
174            if key.is_some() {
175                return Err(meta.error("duplicate 'key' parameter"));
176            }
177            key = Some(meta.value()?.parse()?); // value()? gets the = LitStr part
178        } else if meta.path.is_ident("default") {
179            if default.is_some() {
180                return Err(meta.error("duplicate 'default' parameter"));
181            }
182            default = Some(meta.value()?.parse()?);
183        } else {
184            return Err(meta.error(format!("unrecognized parameter '{}' in #[prop] attribute", meta.path.get_ident().map(|i| i.to_string()).unwrap_or_else(|| "<?>".into()))));
185        }
186        Ok(())
187    })?;
188
189    // Check if key is found
190    let key_str = key.ok_or_else(|| syn::Error::new_spanned(prop_attr, "Missing 'key' parameter in #[prop] attribute"))?;
191
192    Ok((key_str, default))
193}