1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3#![warn(clippy::all)]
4
5mod config_api;
6mod config_form;
7mod config_store;
8mod utils;
9
10use proc_macro::TokenStream;
11use syn::parse_macro_input;
12
13#[proc_macro_derive(
20 WifiCaddyConfig,
21 attributes(config_store, config_form, config_server, config_notify, config_ui)
22)]
23pub fn derive_wifi_caddy_config(input: TokenStream) -> TokenStream {
24 let input = parse_macro_input!(input as syn::DeriveInput);
25 let store = config_store::derive_config_store_impl(&input);
28 let form = config_form::derive_config_form_impl(&input);
30 let group = config_api::derive_config_api_impl(&input);
32 proc_macro::TokenStream::from(quote::quote! {
33 #store
34 #form
35 #group
36 })
37}
38
39#[cfg(test)]
40mod tests {
41 use syn::parse_str;
42
43 #[test]
47 fn config_form_password_recognized_after_fieldset_and_help() {
48 let input: syn::DeriveInput = parse_str(
49 r#"
50 struct S {
51 #[config_form(fieldset = "WiFi", input_type = "password", help = "Secret")]
52 wifi_pass: String,
53 }
54 "#,
55 )
56 .unwrap();
57 let syn::Data::Struct(data) = &input.data else {
58 panic!("expected struct");
59 };
60 let field = data.fields.iter().next().unwrap();
61 let attr = field
62 .attrs
63 .iter()
64 .find(|a| a.path().is_ident("config_form"))
65 .unwrap();
66 let mut input_type = String::from("text");
67 let _ = attr.parse_nested_meta(|meta| {
68 if meta.path.is_ident("input_type") {
69 if let Ok(lit) = meta.value().and_then(|v| v.parse::<syn::LitStr>()) {
70 input_type = lit.value();
71 }
72 } else {
73 let _ = meta.value().and_then(|v| v.parse::<syn::Expr>());
75 }
76 Ok(())
77 });
78 assert_eq!(
79 input_type, "password",
80 "input_type must be recognized so GET /config-group/main redacts the field"
81 );
82 }
83}