ferroui_static_view_locator_macros/
lib.rs1use proc_macro::TokenStream;
14use proc_macro2::{Span, TokenStream as TokenStream2};
15use quote::quote;
16use syn::parse::{ParseStream, Parser};
17use syn::punctuated::Punctuated;
18use syn::spanned::Spanned;
19use syn::{bracketed, Attribute, Item, LitBool, LitStr, Token, Type};
20
21const RESOLUTION_MODES: &[&str] =
23 &["Exact", "ExactThenBaseTypes", "ExactThenInterfaces", "ExactThenBaseTypesThenInterfaces", "ExactThenInterfacesThenBaseTypes"];
24
25const BOOLEAN_OPTIONS: &[&str] =
27 &["generate_view_factory_methods", "generate_runtime_type_fallback_methods", "generate_view_locator", "generate_data_template"];
28
29#[proc_macro_attribute]
57pub fn static_view_locator(attribute: TokenStream, item: TokenStream) -> TokenStream {
58 expand_locator(attribute.into(), item.into()).unwrap_or_else(syn::Error::into_compile_error).into()
59}
60
61#[proc_macro_attribute]
75pub fn static_view_mapping(attribute: TokenStream, item: TokenStream) -> TokenStream {
76 let checked = parse_mapping.parse2(attribute.into());
77 match checked {
78 Ok(_) => item,
79 Err(error) => {
80 let mut tokens: TokenStream2 = error.into_compile_error();
81 tokens.extend(TokenStream2::from(item));
82 tokens.into()
83 }
84 }
85}
86
87fn expand_locator(attribute: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
88 let (id, mut named) = parse_options.parse2(attribute)?;
89 let mut item: Item = syn::parse2(item)?;
90 let Item::Struct(locator) = &mut item else {
91 return Err(syn::Error::new(
92 item.span(),
93 "SVL0005: #[static_view_locator] extends a struct; the generated members are an `impl` block of the struct",
94 ));
95 };
96 let mut kept: Vec<Attribute> = Vec::with_capacity(locator.attrs.len());
97 for attribute in locator.attrs.drain(..) {
98 if is_named(&attribute, "static_view_mapping") {
99 let (view_model, view) = attribute.parse_args_with(parse_mapping)?;
100 named.push(view_model);
101 named.push(view);
102 } else if is_named(&attribute, "static_view_locator") {
103 return Err(syn::Error::new(attribute.span(), "#[static_view_locator] is stated more than once"));
104 } else {
105 kept.push(attribute);
106 }
107 }
108 locator.attrs = kept;
109 let file = format!("{}.rs", id.unwrap_or_else(|| locator.ident.to_string()));
110 let uses = named.iter().filter_map(type_path).map(|path| quote! { use #path as _; });
114 Ok(quote! {
115 #item
116 #[allow(unused_imports)]
117 const _: () = {
118 #(#uses)*
119 };
120 include!(concat!(env!("OUT_DIR"), "/static_view_locator/", #file));
121 })
122}
123
124fn type_path(type_: &Type) -> Option<syn::Path> {
127 let Type::Path(type_path) = type_ else { return None };
128 if type_path.qself.is_some() {
129 return None;
130 }
131 let mut path = type_path.path.clone();
132 for segment in &mut path.segments {
133 segment.arguments = syn::PathArguments::None;
134 }
135 let primitive = path.segments.len() == 1 && path.segments[0].ident.to_string().starts_with(|character: char| character.is_ascii_lowercase());
136 (!primitive).then_some(path)
137}
138
139fn is_named(attribute: &Attribute, name: &str) -> bool {
141 attribute.path().segments.last().is_some_and(|segment| segment.ident == name)
142}
143
144fn parse_mapping(input: ParseStream) -> syn::Result<(Type, Type)> {
146 let types = Punctuated::<Type, Token![,]>::parse_terminated(input)?;
147 if types.len() != 2 {
148 return Err(syn::Error::new(
149 if types.is_empty() { Span::call_site() } else { types.span() },
150 "#[static_view_mapping] takes the view model type and the view type: #[static_view_mapping(SettingsViewModel, SettingsView)]",
151 ));
152 }
153 let mut types = types.into_iter();
154 Ok((types.next().expect("two types"), types.next().expect("two types")))
155}
156
157fn parse_options(input: ParseStream) -> syn::Result<(Option<String>, Vec<Type>)> {
160 let mut id = None;
161 let mut named = Vec::new();
162 let mut seen: Vec<String> = Vec::new();
163 while !input.is_empty() {
164 let name: syn::Ident = input.parse()?;
165 let key = name.to_string();
166 if seen.contains(&key) {
167 return Err(syn::Error::new(name.span(), format!("the option `{key}` is stated more than once")));
168 }
169 input.parse::<Token![=]>()?;
170 match key.as_str() {
171 "id" => {
172 let value: LitStr = input.parse()?;
173 let text = value.value();
174 if text.is_empty() || !text.chars().all(|character| character.is_ascii_alphanumeric() || character == '_') {
175 return Err(syn::Error::new(value.span(), "the id of a locator is a name of letters, digits and underscores"));
176 }
177 id = Some(text);
178 }
179 key if BOOLEAN_OPTIONS.contains(&key) => {
180 input.parse::<LitBool>()?;
181 }
182 "generated_adapter_resolution_mode" => {
183 let path: syn::Path = input.parse()?;
184 let mode = path.segments.last().map(|segment| segment.ident.to_string()).unwrap_or_default();
185 if !RESOLUTION_MODES.contains(&mode.as_str()) {
186 return Err(syn::Error::new(path.span(), format!("the resolution mode is one of {}", RESOLUTION_MODES.join(", "))));
187 }
188 }
189 "view_model_mapping_contracts" | "data_template_match_types" => {
190 let content;
191 bracketed!(content in input);
192 named.extend(Punctuated::<Type, Token![,]>::parse_terminated(&content)?);
193 }
194 _ => {
195 return Err(syn::Error::new(
196 name.span(),
197 format!(
198 "unknown option `{key}`; the options are id, {}, generated_adapter_resolution_mode, view_model_mapping_contracts and data_template_match_types",
199 BOOLEAN_OPTIONS.join(", ")
200 ),
201 ))
202 }
203 }
204 seen.push(key);
205 if input.is_empty() {
206 break;
207 }
208 input.parse::<Token![,]>()?;
209 }
210 Ok((id, named))
211}