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