struct_path/
lib.rs

1//! A helper macros implementation to build a string that represents struct fields path at compile time.
2//!
3//! Library provides a tiny macro implementation to reference Rust struct fields at compile time to represent its string format.
4//! This is needed to work with JSON paths, and some others protocols when we still want to rely on the compiler to avoid inconsistent changes.
5//!
6//! Features:
7//! - Fast and no macro parsing without huge deps;
8//! - Macro produces the code to verify if the specified path really exists;
9//! - Multiple fields/arrays support
10//! - Optional camelCase and PascalCase conversion support;
11//! - Optional delimiter parameter;
12//! Example:
13//!
14//! ```rust,no_run
15//! use struct_path::*;
16//!
17//! fn example() {
18//!
19//! pub struct TestStructParent {
20//!     pub value_str: String,
21//!     pub value_num: u64,
22//!     pub value_child: TestStructChild,
23//! }
24//!
25//! pub struct TestStructChild {
26//!     pub child_value_str: String,
27//!     pub child_value_num: u64,
28//! }
29//!
30//!// returns "value_str"
31//!let s1: &str = path!(TestStructParent::value_str);
32//!
33//!// returns "value_child.child_value_str"
34//!let s2: &str = path!(TestStructParent::value_child.child_value_str) ;
35//!
36//!// returns also "value_child.child_value_str"
37//!let s3: &str = path!(TestStructParent::value_child,TestStructChild::child_value_str);
38//!
39//!// options, returns "valueChild/childValueStr"
40//!let s4: &str = path!(TestStructParent::value_child.child_value_str; delim="/", case="camel") ;
41//!
42//!// returns ["value_str", "value_num"]
43//!let arr: [&str; 2] = paths!(TestStructParent::{ value_str, value_num });
44//!
45//! }
46//!
47//! ```
48//!
49
50use convert_case::{Case, Casing};
51use proc_macro::{TokenStream, TokenTree};
52use std::collections::HashMap;
53
54#[proc_macro]
55pub fn paths(struct_path_stream: TokenStream) -> TokenStream {
56    let mut current_struct_name: Option<String> = None;
57    let mut current_struct_fields: Vec<String> = Vec::with_capacity(16);
58
59    let mut opened_struct = false;
60    let mut colons_counter = 0;
61    let mut options_opened = false;
62
63    let mut current_field_path: Option<String> = None;
64
65    let mut current_option_name: Option<String> = None;
66    let mut expect_option_value: bool = false;
67
68    let mut options: HashMap<String, String> = HashMap::new();
69    let mut found_structs: Vec<(String, Vec<String>)> = Vec::new();
70
71    for token_tree in struct_path_stream.into_iter() {
72        match token_tree {
73            TokenTree::Ident(id) if current_struct_name.is_none() => {
74                current_struct_name = Some(id.to_string());
75            }
76            TokenTree::Punct(punct)
77                if current_struct_name.is_some()
78                    && !opened_struct
79                    && punct == ':'
80                    && colons_counter < 2 =>
81            {
82                colons_counter += 1;
83                if colons_counter > 1 {
84                    opened_struct = true;
85                }
86            }
87            TokenTree::Ident(id) if opened_struct => {
88                colons_counter = 0;
89                if let Some(ref mut field_path) = &mut current_field_path {
90                    field_path.push_str(id.to_string().as_str())
91                } else {
92                    current_field_path = Some(id.to_string());
93                }
94            }
95            TokenTree::Punct(punct)
96                if current_struct_name.is_some()
97                    && opened_struct
98                    && punct == ':'
99                    && colons_counter < 2 =>
100            {
101                colons_counter += 1;
102                opened_struct = false;
103                if let Some(ref mut field_path) = current_field_path.take() {
104                    if let Some(ref mut struct_name) = &mut current_struct_name {
105                        struct_name.push_str("::");
106                        struct_name.push_str(field_path);
107                    }
108                }
109            }
110            TokenTree::Punct(punct) if opened_struct && (punct == '.' || punct == '~') => {
111                if let Some(ref mut field_path) = &mut current_field_path {
112                    field_path.push(punct.as_char());
113                } else {
114                    panic!(
115                        "Unexpected punctuation input for struct path group parameters: {:?}",
116                        punct
117                    )
118                }
119            }
120            TokenTree::Group(group) if opened_struct && current_field_path.is_none() => {
121                parse_multiple_fields(group.stream(), &mut current_struct_fields)
122            }
123            TokenTree::Punct(punct) if !options_opened && opened_struct && punct == ',' => {
124                opened_struct = false;
125                colons_counter = 0;
126                if let Some(struct_name) = current_struct_name.take() {
127                    if let Some(field_path) = current_field_path.take() {
128                        current_struct_fields.push(field_path);
129                    }
130                    if !current_struct_fields.is_empty() {
131                        found_structs
132                            .push((struct_name, current_struct_fields.drain(..).collect()));
133                    } else {
134                        panic!("Unexpected comma with empty fields for {}!", struct_name);
135                    }
136                } else {
137                    panic!("Unexpected comma with empty definitions!");
138                }
139            }
140            TokenTree::Punct(punct) if punct == ';' && opened_struct && !options_opened => {
141                options_opened = true;
142                opened_struct = false;
143            }
144            TokenTree::Ident(id) if options_opened && !expect_option_value => {
145                current_option_name = Some(id.to_string())
146            }
147            TokenTree::Ident(id) if options_opened && expect_option_value => {
148                expect_option_value = false;
149                match current_option_name.take() {
150                    Some(option_name) => {
151                        options.insert(option_name, id.to_string());
152                    }
153                    _ => {
154                        panic!("Wrong options format")
155                    }
156                }
157            }
158            TokenTree::Literal(lit) if options_opened && expect_option_value => {
159                expect_option_value = false;
160                match current_option_name.take() {
161                    Some(option_name) => {
162                        let lit_str = lit.to_string();
163                        options.insert(
164                            option_name,
165                            lit_str.as_str()[1..lit_str.len() - 1].to_string(),
166                        );
167                    }
168                    _ => {
169                        panic!("Wrong options format")
170                    }
171                }
172            }
173            TokenTree::Punct(punct) if options_opened && punct == '=' => {
174                expect_option_value = true;
175            }
176            TokenTree::Punct(punct) if options_opened && punct == ',' => {
177                expect_option_value = false;
178            }
179            others => {
180                panic!("Unexpected input for struct path parameters: {:?}", others)
181            }
182        }
183    }
184
185    if let Some(field_path) = current_field_path.take() {
186        current_struct_fields.push(field_path);
187    }
188
189    if let Some(struct_name) = current_struct_name.take() {
190        if let Some(field_path) = current_field_path.take() {
191            current_struct_fields.push(field_path);
192        }
193        if !current_struct_fields.is_empty() {
194            found_structs.push((struct_name, current_struct_fields.drain(..).collect()));
195        } else {
196            panic!("Unexpected comma with empty fields for {}!", struct_name);
197        }
198    } else {
199        panic!("Unexpected comma with empty definitions!");
200    }
201
202    let all_check_functions = generate_checks_code_for(&found_structs);
203
204    let mut all_final_fields: Vec<String> = Vec::with_capacity(16);
205
206    for (_, struct_fields) in &found_structs {
207        for field_path in struct_fields {
208            let mut final_field_path = field_path.clone().replace('~', ".");
209            if !options.is_empty() {
210                final_field_path = apply_options(&options, final_field_path);
211            }
212            all_final_fields.push(format!("\"{}\"", final_field_path))
213        }
214    }
215
216    if !all_final_fields.is_empty() {
217        format!(
218            "{{{}\n[{}]}}",
219            all_check_functions,
220            all_final_fields.join(",")
221        )
222        .parse()
223        .unwrap()
224    } else {
225        panic!("Empty struct fields")
226    }
227}
228
229#[inline]
230fn parse_multiple_fields(group_stream: TokenStream, found_struct_fields: &mut Vec<String>) {
231    let mut current_field_path: Option<String> = None;
232
233    for token_tree in group_stream.into_iter() {
234        match token_tree {
235            TokenTree::Ident(id) => {
236                if let Some(ref mut field_path) = &mut current_field_path {
237                    field_path.push_str(id.to_string().as_str())
238                } else {
239                    current_field_path = Some(id.to_string());
240                }
241            }
242            TokenTree::Punct(punct) if punct == ',' => {
243                if let Some(field_path) = current_field_path.take() {
244                    found_struct_fields.push(field_path);
245                    current_field_path = None;
246                } else {
247                    panic!(
248                        "Unexpected punctuation input for struct path group parameters: {:?}",
249                        punct
250                    )
251                }
252            }
253            TokenTree::Punct(punct) if punct == '.' => {
254                if let Some(ref mut field_path) = &mut current_field_path {
255                    field_path.push('.');
256                } else {
257                    panic!(
258                        "Unexpected punctuation input for struct path group parameters: {:?}",
259                        punct
260                    )
261                }
262            }
263            others => {
264                panic!(
265                    "Unexpected input for struct path group parameters: {:?}",
266                    others
267                )
268            }
269        }
270    }
271
272    if let Some(field_path) = current_field_path.take() {
273        found_struct_fields.push(field_path);
274    }
275}
276
277#[proc_macro]
278pub fn path(struct_path_stream: TokenStream) -> TokenStream {
279    let mut current_struct_name: Option<String> = None;
280
281    let mut opened_struct = false;
282    let mut colons_counter = 0;
283    let mut options_opened = false;
284
285    let mut current_field_path: Option<String> = None;
286    let mut current_full_field_path: Option<String> = None;
287
288    let mut current_option_name: Option<String> = None;
289    let mut expect_option_value: bool = false;
290
291    let mut options: HashMap<String, String> = HashMap::new();
292    let mut found_structs: Vec<(String, Vec<String>)> = Vec::new();
293
294    for token_tree in struct_path_stream.into_iter() {
295        match token_tree {
296            TokenTree::Ident(id) if current_struct_name.is_none() => {
297                current_struct_name = Some(id.to_string());
298            }
299            TokenTree::Punct(punct)
300                if current_struct_name.is_some()
301                    && !opened_struct
302                    && punct == ':'
303                    && colons_counter < 2 =>
304            {
305                colons_counter += 1;
306                if colons_counter > 1 {
307                    opened_struct = true;
308                }
309            }
310            TokenTree::Ident(id) if opened_struct => {
311                colons_counter = 0;
312                if let Some(ref mut field_path) = &mut current_field_path {
313                    field_path.push_str(id.to_string().as_str())
314                } else {
315                    current_field_path = Some(id.to_string());
316                }
317            }
318            TokenTree::Punct(punct)
319                if current_struct_name.is_some()
320                    && opened_struct
321                    && punct == ':'
322                    && colons_counter < 2 =>
323            {
324                colons_counter += 1;
325                opened_struct = false;
326                if let Some(ref mut field_path) = current_field_path.take() {
327                    if let Some(ref mut struct_name) = &mut current_struct_name {
328                        struct_name.push_str("::");
329                        struct_name.push_str(field_path);
330                    }
331                }
332            }
333            TokenTree::Punct(punct) if opened_struct && (punct == '.' || punct == '~') => {
334                if let Some(ref mut field_path) = &mut current_field_path {
335                    field_path.push(punct.as_char());
336                } else {
337                    panic!(
338                        "Unexpected punctuation input for struct path group parameters: {:?}",
339                        punct
340                    )
341                }
342            }
343            TokenTree::Punct(punct) if !options_opened && opened_struct && punct == ',' => {
344                opened_struct = false;
345                colons_counter = 0;
346                if let Some(struct_name) = current_struct_name.take() {
347                    if let Some(field_path) = current_field_path.take() {
348                        found_structs.push((struct_name, vec![field_path.clone()]));
349
350                        if let Some(full_field_path) = &mut current_full_field_path {
351                            full_field_path.push('.');
352                            full_field_path.push_str(field_path.as_str());
353                        } else {
354                            current_full_field_path = Some(field_path)
355                        }
356                    } else {
357                        panic!("Unexpected comma with empty fields for {}!", struct_name);
358                    }
359                } else {
360                    panic!("Unexpected comma with empty definitions!");
361                }
362            }
363            TokenTree::Punct(punct) if punct == ';' && opened_struct && !options_opened => {
364                options_opened = true;
365                opened_struct = false;
366            }
367            TokenTree::Ident(id) if options_opened && !expect_option_value => {
368                current_option_name = Some(id.to_string())
369            }
370            TokenTree::Ident(id) if options_opened && expect_option_value => {
371                expect_option_value = false;
372                match current_option_name.take() {
373                    Some(option_name) => {
374                        options.insert(option_name, id.to_string());
375                    }
376                    _ => {
377                        panic!("Wrong options format")
378                    }
379                }
380            }
381            TokenTree::Literal(lit) if options_opened && expect_option_value => {
382                expect_option_value = false;
383                match current_option_name.take() {
384                    Some(option_name) => {
385                        let lit_str = lit.to_string();
386                        options.insert(
387                            option_name,
388                            lit_str.as_str()[1..lit_str.len() - 1].to_string(),
389                        );
390                    }
391                    _ => {
392                        panic!("Wrong options format")
393                    }
394                }
395            }
396            TokenTree::Punct(punct) if options_opened && punct == '=' => {
397                expect_option_value = true;
398            }
399            TokenTree::Punct(punct) if options_opened && punct == ',' => {
400                expect_option_value = false;
401            }
402            others => {
403                panic!("Unexpected input for struct path parameters: {:?}", others)
404            }
405        }
406    }
407
408    if let Some(struct_name) = current_struct_name.take() {
409        if let Some(field_path) = current_field_path.take() {
410            found_structs.push((struct_name, vec![field_path.clone()]));
411
412            if let Some(full_field_path) = &mut current_full_field_path {
413                full_field_path.push('.');
414                full_field_path.push_str(field_path.as_str());
415            } else {
416                current_full_field_path = Some(field_path)
417            }
418        }
419    }
420
421    if let Some(full_field_path) = current_full_field_path.take() {
422        let all_check_functions = generate_checks_code_for(&found_structs);
423        let final_field_path = apply_options(&options, full_field_path).replace('~', ".");
424        let result_str = format!("{{{}\n\"{}\"}}", all_check_functions, final_field_path);
425        result_str.parse().unwrap()
426    } else {
427        panic!("Unexpected empty path definition!");
428    }
429}
430
431#[inline]
432fn generate_checks_code_for(found_structs: &Vec<(String, Vec<String>)>) -> String {
433    let mut all_check_functions = String::new();
434
435    for (struct_name, struct_fields) in found_structs {
436        let check_functions = struct_fields
437            .iter()
438            .map(|field_path| {
439                let field_path_result = field_path.replace('~', ".iter().next().unwrap().");
440                format!(
441                    r#"
442                {{
443                    #[allow(dead_code, unused_variables)]
444                    #[cold]
445                    fn _check_sp(test_struct: &{}) {{
446                        let _t = &test_struct.{};
447                    }}
448                }}
449            "#,
450                    struct_name, field_path_result
451                )
452            })
453            .collect::<Vec<String>>()
454            .join("\n");
455
456        all_check_functions.push_str(&check_functions);
457    }
458    all_check_functions
459}
460
461#[inline]
462fn apply_options(options: &HashMap<String, String>, field_path: String) -> String {
463    let delim = options
464        .get("delim")
465        .as_ref()
466        .map(|s| s.as_str())
467        .unwrap_or_else(|| ".");
468    let case = options.get("case");
469    field_path
470        .split('.')
471        .map(|field_name| {
472            if let Some(case_value) = case {
473                match case_value.as_str() {
474                    "camel" => field_name.from_case(Case::Snake).to_case(Case::Camel),
475                    "pascal" => field_name.from_case(Case::Snake).to_case(Case::Pascal),
476                    another => panic!("Unknown case is specified: {}", another),
477                }
478            } else {
479                field_name.to_string()
480            }
481        })
482        .collect::<Vec<String>>()
483        .join(delim)
484}