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