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(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("`{}` value is not configured. Use default or set it in the .properties file", #key)))
47 }
48 },
49 };
50
51 let mut 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 if let syn::Type::Path(tpath) = field_type {
61 if tpath.path.segments.last().is_some_and(|segment| segment.ident == "Vec") {
62 init = quote! {
63 #field_name : {
64 let raw_value_str = #raw_value_str;
65 raw_value_str.split(',')
66 .map(|s| s.trim())
67 .filter(|s| !s.is_empty())
68 .map(|s| s.parse::<_>().map_err(|e|
69 std::io::Error::new(std::io::ErrorKind::InvalidData,
70 format!("Error Parsing `{}` with value `{}` {}", #key, s, e))
71 ))
72 .collect::<std::io::Result<Vec<_>>>()?
73 }
74 }
75 }
76 }
77
78 init_arr.push(init);
79 }
80
81 Ok(init_arr)
82}
83
84fn generate_prop_fns(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
85 let fields = extract_named_fields(input)?;
86 let init_arr = generate_initalizers(fields)?;
87
88 let new_impl = quote! {
89 pub fn from_file(path : &str) -> std::io::Result<Self> {
90 use std::collections::HashMap;
91 use std::fs;
92 use std::io::{self, ErrorKind}; use std::path::Path; use std::{fs::File, io::Read};
95
96 let mut content = String::new();
97
98 let mut file = File::open(path).map_err(|e| std::io::Error::new(e.kind(), format!("Error opening file {}", path)))?;
99 file.read_to_string(&mut content) .map_err(|e| std::io::Error::new(e.kind(), format!("Error Reading File : {}", path)))?;
100
101 let mut propmap = std::collections::HashMap::<String, String>::new();
102 for (line_num, line) in content.lines().enumerate() {
103 let line = line.trim();
104
105 if line.is_empty() || line.starts_with('#') || line.starts_with('!') {
106 continue;
107 }
108
109 match line.split_once('=') {
111 Some((key, value)) => propmap.insert(key.trim().to_string(), value.trim().to_string()),
112 None => return Err(io::Error::new( ErrorKind::InvalidData, format!("Malformed line {} in '{}' (missing '='): {}", line_num + 1, path, line) )),
113 };
114 }
115
116 Ok(Self { #( #init_arr ),* })
117 }
118
119 pub fn from_hash_map(propmap : &std::collections::HashMap<&str, &str>) -> std::io::Result<Self> {
120 let propmap : std::collections::HashMap<String, String> = propmap.iter().map(|(k, v)| (k.trim().to_string(), v.trim().to_string())).collect();
121 Ok(Self { #( #init_arr ),* })
122 }
123
124 pub fn default() -> std::io::Result<Self> {
125 use std::collections::HashMap;
126 let mut propmap = HashMap::<String, String>::new();
127 Ok(Self { #( #init_arr ),* })
128 }
129 };
130
131 Ok(new_impl)
132}
133
134fn parse_key_default(field: &syn::Field) -> syn::Result<(LitStr, Option<LitStr>)> {
135 let prop_attr = field.attrs.iter().find(|attr| attr.path().is_ident("prop")).ok_or_else(|| {
136 syn::Error::new_spanned(
137 field.ident.as_ref().unwrap(),
138 format!("Field '{}' is missing the #[prop(...)] attribute", field.ident.as_ref().map(|i| i.to_string()).unwrap_or_else(|| "<?>".into())),
139 )
140 })?;
141
142 let mut key: Option<LitStr> = None;
143 let mut default: Option<LitStr> = None;
144
145 prop_attr.parse_nested_meta(|meta| {
147 if meta.path.is_ident("key") {
148 if key.is_some() {
149 return Err(meta.error("duplicate 'key' parameter"));
150 }
151 key = Some(meta.value()?.parse()?); } else if meta.path.is_ident("default") {
153 if default.is_some() {
154 return Err(meta.error("duplicate 'default' parameter"));
155 }
156 default = Some(meta.value()?.parse()?);
157 } else {
158 return Err(meta.error(format!("unrecognized parameter '{}' in #[prop] attribute", meta.path.get_ident().map(|i| i.to_string()).unwrap_or_else(|| "<?>".into()))));
159 }
160 Ok(())
161 })?;
162
163 let key_str = key.ok_or_else(|| syn::Error::new_spanned(prop_attr, "Missing 'key' parameter in #[prop] attribute"))?;
165
166 Ok((key_str, default))
167}