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_initalizers(fields: Punctuated<Field, Comma>) -> syn::Result<Vec<proc_macro2::TokenStream>> {
34    let mut init_arr: Vec<proc_macro2::TokenStream> = Vec::new();
35
36    for field in fields {
37        let (key, default) = parse_key_default(&field).map_err(|_| Error::new_spanned(field.clone(), "Expecting `key` and `default` values"))?;
38        let field_name = field.ident.as_ref().to_owned().unwrap();
39        let field_type = &field.ty;
40
41        let raw_value_str = match default {
42            Some(default) => quote! { propmap.get(#key).map(String::as_str).unwrap_or(#default) },
43            None => quote! {
44                match propmap.get(#key).map(String::as_str) {
45                    Some(val) => val,
46                    None => return Err(Error::new_spanned(field, format!("`{}` value is not configured. Use default or set it in the .properties file", #key))),
47                }
48            },
49        };
50
51        let init = quote! {
52             #field_name : {
53                let raw_value_str = #raw_value_str;
54                raw_value_str.parse::<#field_type>().map_err(|e|
55                    std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Error Parsing `{}` with value `{}` {}", #key, raw_value_str, e))
56                )?
57            }
58        };
59
60        init_arr.push(init);
61    }
62
63    Ok(init_arr)
64}
65
66fn generate_prop_fns(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
67    let fields = extract_named_fields(input)?;
68    let init_arr = generate_initalizers(fields)?;
69
70    let new_impl = quote! {
71        pub fn from_file(path : &str) -> std::io::Result<Self> {
72            use std::collections::HashMap;
73            use std::fs;
74            use std::io::{self, ErrorKind}; // Explicitly import ErrorKind
75            use std::path::Path; // Required for AsRef<Path> trait bound
76
77            let mut content = String::new();
78
79            let mut file = File::open(path).map_err(|e| std::io::Error::new(e.kind(), format!("Error opening file {}", path)))?;
80            file.read_to_string(&mut content) .map_err(|e| std::io::Error::new(e.kind(), format!("Error Reading File : {}", path)))?;
81
82            let mut propmap = HashMap::<String, String>::new();
83            for (line_num, line) in content.lines().enumerate() {
84                let line = line.trim();
85
86                if line.is_empty() || line.starts_with('#') || line.starts_with('!') {
87                    continue;
88                }
89
90                // Find the first '=', handling potential whitespace
91                match line.split_once('=') {
92                    Some((key, value)) => propmap.insert(key.trim().to_string(), value.trim().to_string()),
93                    None => return Err(io::Error::new( ErrorKind::InvalidData, format!("Malformed line {} in '{}' (missing '='): {}", line_num + 1, path, line) )),
94                };
95            }
96
97            Ok(Self { #( #init_arr ),* })
98        }
99
100        pub fn default() -> std::io::Result<Self> {
101            use std::collections::HashMap;
102            let mut propmap = HashMap::<String, String>::new();
103            Ok(Self { #( #init_arr ),* })
104        }
105    };
106
107    Ok(proc_macro2::TokenStream::from(new_impl))
108}
109
110fn parse_key_default(field: &syn::Field) -> syn::Result<(LitStr, Option<LitStr>)> {
111    let prop_attr = field.attrs.iter().find(|attr| attr.path().is_ident("prop")).ok_or_else(|| {
112        syn::Error::new_spanned(
113            field.ident.as_ref().unwrap(),
114            format!("Field '{}' is missing the #[prop(...)] attribute", field.ident.as_ref().map(|i| i.to_string()).unwrap_or_else(|| "<?>".into())),
115        )
116    })?;
117
118    let mut key: Option<LitStr> = None;
119    let mut default: Option<LitStr> = None;
120
121    // Use parse_nested_meta for more robust parsing of attribute arguments
122    prop_attr.parse_nested_meta(|meta| {
123        if meta.path.is_ident("key") {
124            if key.is_some() {
125                return Err(meta.error("duplicate 'key' parameter"));
126            }
127            key = Some(meta.value()?.parse()?); // value()? gets the = LitStr part
128        } else if meta.path.is_ident("default") {
129            if default.is_some() {
130                return Err(meta.error("duplicate 'default' parameter"));
131            }
132            default = Some(meta.value()?.parse()?);
133        } else {
134            return Err(meta.error(format!("unrecognized parameter '{}' in #[prop] attribute", meta.path.get_ident().map(|i| i.to_string()).unwrap_or_else(|| "<?>".into()))));
135        }
136        Ok(())
137    })?;
138
139    // Check if both key and default were found
140    let key_str = key.ok_or_else(|| syn::Error::new_spanned(prop_attr, "Missing 'key' parameter in #[prop] attribute"))?;
141
142    Ok((key_str, default))
143}