1use proc_macro2::TokenStream;
2use quote::{ToTokens, quote};
3use syn::{Data, DeriveInput, LitStr, Token, parse_macro_input};
4
5/// Derive macro for the `SettingsUi` marker trait.
6///
7/// This macro automatically implements the `SettingsUi` trait for the annotated type.
8/// The `SettingsUi` trait is a marker trait used to indicate that a type can be
9/// displayed in the settings UI.
10///
11/// # Example
12///
13/// ```
14/// use settings::SettingsUi;
15/// use settings_ui_macros::SettingsUi;
16///
17/// #[derive(SettingsUi)]
18/// #[settings_ui(group = "Standard")]
19/// struct MySettings {
20/// enabled: bool,
21/// count: usize,
22/// }
23/// ```
24#[proc_macro_derive(SettingsUi, attributes(settings_ui))]
25pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
26 let input = parse_macro_input!(input as DeriveInput);
27 let name = &input.ident;
28
29 // Handle generic parameters if present
30 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
31
32 let mut group_name = Option::<String>::None;
33 let mut path_name = Option::<String>::None;
34
35 for attr in &input.attrs {
36 if attr.path().is_ident("settings_ui") {
37 attr.parse_nested_meta(|meta| {
38 if meta.path.is_ident("group") {
39 if group_name.is_some() {
40 return Err(meta.error("Only one 'group' path can be specified"));
41 }
42 meta.input.parse::<Token![=]>()?;
43 let lit: LitStr = meta.input.parse()?;
44 group_name = Some(lit.value());
45 } else if meta.path.is_ident("path") {
46 // todo(settings_ui) try get KEY from Settings if possible, and once we do,
47 // if can get key from settings, throw error if path also passed
48 if path_name.is_some() {
49 return Err(meta.error("Only one 'path' can be specified"));
50 }
51 meta.input.parse::<Token![=]>()?;
52 let lit: LitStr = meta.input.parse()?;
53 path_name = Some(lit.value());
54 }
55 Ok(())
56 })
57 .unwrap_or_else(|e| panic!("in #[settings_ui] attribute: {}", e));
58 }
59 }
60
61 if path_name.is_none() && group_name.is_some() {
62 // todo(settings_ui) derive path from settings
63 panic!("path is required when group is specified");
64 }
65
66 let ui_render_fn_body = generate_ui_item_body(group_name.as_ref(), path_name.as_ref(), &input);
67
68 let settings_ui_item_fn_body = path_name
69 .as_ref()
70 .map(|path_name| map_ui_item_to_render(path_name, quote! { Self }))
71 .unwrap_or(quote! {
72 settings::SettingsUiEntry {
73 item: settings::SettingsUiEntryVariant::None
74 }
75 });
76
77 let expanded = quote! {
78 impl #impl_generics settings::SettingsUi for #name #ty_generics #where_clause {
79 fn settings_ui_item() -> settings::SettingsUiItem {
80 #ui_render_fn_body
81 }
82
83 fn settings_ui_entry() -> settings::SettingsUiEntry {
84 #settings_ui_item_fn_body
85 }
86 }
87 };
88
89 proc_macro::TokenStream::from(expanded)
90}
91
92fn map_ui_item_to_render(path: &str, ty: TokenStream) -> TokenStream {
93 quote! {
94 settings::SettingsUiEntry {
95 item: match #ty::settings_ui_item() {
96 settings::SettingsUiItem::Group{title, items} => settings::SettingsUiEntryVariant::Group {
97 title,
98 path: #path,
99 items,
100 },
101 settings::SettingsUiItem::Single(item) => settings::SettingsUiEntryVariant::Item {
102 path: #path,
103 item,
104 },
105 settings::SettingsUiItem::None => settings::SettingsUiEntryVariant::None,
106 }
107 }
108 }
109}
110
111fn generate_ui_item_body(
112 group_name: Option<&String>,
113 path_name: Option<&String>,
114 input: &syn::DeriveInput,
115) -> TokenStream {
116 match (group_name, path_name, &input.data) {
117 (_, _, Data::Union(_)) => unimplemented!("Derive SettingsUi for Unions"),
118 (None, None, Data::Struct(_)) => quote! {
119 settings::SettingsUiItem::None
120 },
121 (Some(_), None, Data::Struct(_)) => quote! {
122 settings::SettingsUiItem::None
123 },
124 (None, Some(_), Data::Struct(_)) => quote! {
125 settings::SettingsUiItem::None
126 },
127 (Some(group_name), _, Data::Struct(data_struct)) => {
128 let fields = data_struct
129 .fields
130 .iter()
131 .filter(|field| {
132 !field.attrs.iter().any(|attr| {
133 let mut has_skip = false;
134 if attr.path().is_ident("settings_ui") {
135 let _ = attr.parse_nested_meta(|meta| {
136 if meta.path.is_ident("skip") {
137 has_skip = true;
138 }
139 Ok(())
140 });
141 }
142
143 has_skip
144 })
145 })
146 .map(|field| {
147 (
148 field.ident.clone().expect("tuple fields").to_string(),
149 field.ty.to_token_stream(),
150 )
151 })
152 .map(|(name, ty)| map_ui_item_to_render(&name, ty));
153
154 quote! {
155 settings::SettingsUiItem::Group{ title: #group_name, items: vec![#(#fields),*] }
156 }
157 }
158 (None, _, Data::Enum(data_enum)) => {
159 let mut lowercase = false;
160 for attr in &input.attrs {
161 if attr.path().is_ident("serde") {
162 attr.parse_nested_meta(|meta| {
163 if meta.path.is_ident("rename_all") {
164 meta.input.parse::<Token![=]>()?;
165 let lit = meta.input.parse::<LitStr>()?.value();
166 // todo(settings_ui) snake case
167 lowercase = lit == "lowercase" || lit == "snake_case";
168 }
169 Ok(())
170 })
171 .ok();
172 }
173 }
174 let length = data_enum.variants.len();
175
176 let variants = data_enum.variants.iter().map(|variant| {
177 let string = variant.ident.clone().to_string();
178
179 if lowercase {
180 string.to_lowercase()
181 } else {
182 string
183 }
184 });
185
186 if length > 6 {
187 quote! {
188 settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::DropDown(&[#(#variants),*]))
189 }
190 } else {
191 quote! {
192 settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::ToggleGroup(&[#(#variants),*]))
193 }
194 }
195 }
196 // todo(settings_ui) discriminated unions
197 (_, _, Data::Enum(_)) => quote! {
198 settings::SettingsUiItem::None
199 },
200 }
201}