Skip to main content

wifi_caddy_proc/
lib.rs

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/// Derive macro for WiFi caddy config structs.
14///
15/// Generates storage (load/store, keys, accessors), form HTML/JS for the config UI,
16/// and the group API for the HTTP handler. Use `#[config_server(...)]`, `#[config_notify]`,
17/// and `#[config_ui(...)]` on the struct for options. All generated code references only
18/// `wifi_caddy::*` (no platform-specific dependencies).
19#[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    // Three codegen passes, all emitted into the same module:
26    // 1. store: ConfigKey enum, load/store, getters/setters
27    let store = config_store::derive_config_store_impl(&input);
28    // 2. form: HTML/JS segments for config UI
29    let form = config_form::derive_config_form_impl(&input);
30    // 3. group: ConfigApi, ConfigChange, notify channel, esp-wifi-caddy helpers
31    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    /// Asserts that `input_type = "password"` is recognized when other name-value pairs
44    /// (page, fieldset, help) appear before it in `#[config_form(...)]`. Without consuming
45    /// unrecognized meta values, the parse stream does not advance and input_type stays "text".
46    #[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                // Consume (page, fieldset, help, etc.) so stream advances to input_type
74                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}