tor_netdoc/parse/
macros.rs

1//! Declares macros to help implementing parsers.
2
3// https://github.com/rust-lang/rust-clippy/issues/6860
4#![allow(renamed_and_removed_lints, clippy::unknown_clippy_lints)]
5
6/// Macro for declaring a keyword enumeration to help parse a document.
7///
8/// A keyword enumeration implements the Keyword trait.
9///
10/// These enums are a bit different from those made by `caret`, in a
11/// few ways.  Notably, they are optimized for parsing, they are
12/// required to be compact, and they allow multiple strings to be mapped to
13/// a single index.
14///
15/// ```ignore
16/// decl_keyword! {
17///    Location {
18//         "start" => START,
19///        "middle" | "center" => MID,
20///        "end" => END
21///    }
22/// }
23///
24/// assert_eq!(Location::from_str("start"), Location::START);
25/// assert_eq!(Location::from_str("stfff"), Location::UNRECOGNIZED);
26/// ```
27#[allow(unused_macro_rules)]
28macro_rules! decl_keyword {
29    { $(#[$meta:meta])* $v:vis
30      $name:ident { $( $($anno:ident)? $($s:literal)|+ => $i:ident),* $(,)? } } => {
31        #[derive(Copy,Clone,Eq,PartialEq,Debug,std::hash::Hash)]
32        #[allow(non_camel_case_types)]
33        $(#[$meta])*
34        #[allow(unknown_lints)]
35        // https://github.com/rust-lang/rust-clippy/issues/6860
36        #[allow(renamed_and_removed_lints, clippy::unknown_clippy_lints)]
37        #[allow(clippy::upper_case_acronyms)]
38        $v enum $name {
39            $( $i , )*
40            UNRECOGNIZED,
41            ANN_UNRECOGNIZED
42        }
43        impl $crate::parse::keyword::Keyword for $name {
44            fn idx(self) -> usize { self as usize }
45            fn n_vals() -> usize { ($name::ANN_UNRECOGNIZED as usize) + 1 }
46            fn unrecognized() -> Self { $name::UNRECOGNIZED }
47            fn ann_unrecognized() -> Self { $name::ANN_UNRECOGNIZED }
48            fn from_str(s : &str) -> Self {
49                // Note usage of phf crate to create a perfect hash over
50                // the possible keywords.  It will be even better if someday
51                // the phf crate can find hash functions that are better
52                // than siphash.
53                const KEYWORD: phf::Map<&'static str, $name> = phf::phf_map! {
54                    $( $( $s => $name::$i , )+ )*
55                };
56                match KEYWORD.get(s) {
57                    Some(k) => *k,
58                    None => if s.starts_with('@') {
59                        $name::ANN_UNRECOGNIZED
60                    } else {
61                        $name::UNRECOGNIZED
62                    }
63                }
64            }
65            fn from_idx(i : usize) -> Option<Self> {
66                // Note looking up the value in a vec.  This may or may
67                // not be faster than a case statement would be.
68                static VALS: once_cell::sync::Lazy<Vec<$name>> =
69                    once_cell::sync::Lazy::new(
70                        || vec![ $($name::$i , )*
71                              $name::UNRECOGNIZED,
72                                 $name::ANN_UNRECOGNIZED ]);
73                VALS.get(i).copied()
74            }
75            fn to_str(self) -> &'static str {
76                use $name::*;
77                match self {
78                    $( $i => decl_keyword![@impl join $($s),+], )*
79                    UNRECOGNIZED => "<unrecognized>",
80                    ANN_UNRECOGNIZED => "<unrecognized annotation>"
81                }
82            }
83            fn is_annotation(self) -> bool {
84                use $name::*;
85                match self {
86                    $( $i => decl_keyword![@impl is_anno $($anno)? ], )*
87                    UNRECOGNIZED => false,
88                    ANN_UNRECOGNIZED => true,
89                }
90            }
91        }
92    };
93    [ @impl is_anno annotation ] => ( true );
94    [ @impl is_anno $x:ident ] => ( compile_error!("unrecognized keyword; not annotation") );
95    [ @impl is_anno ] => ( false );
96    [ @impl join $s:literal ] => ( $s );
97    [ @impl join $s:literal , $($ss:literal),+ ] => (
98        concat! { $s, "/", decl_keyword![@impl join $($ss),*] }
99    );
100}
101
102#[cfg(test)]
103pub(crate) mod test {
104    #![allow(clippy::cognitive_complexity)]
105
106    decl_keyword! {
107        pub(crate) Fruit {
108            "apple" => APPLE,
109            "orange" => ORANGE,
110            "lemon" => LEMON,
111            "guava" => GUAVA,
112            "cherry" | "plum" => STONEFRUIT,
113            "banana" => BANANA,
114            annotation "@tasty" => ANN_TASTY,
115        }
116    }
117
118    #[test]
119    fn kwd() {
120        use crate::parse::keyword::Keyword;
121        use Fruit::*;
122        assert_eq!(Fruit::from_str("lemon"), LEMON);
123        assert_eq!(Fruit::from_str("cherry"), STONEFRUIT);
124        assert_eq!(Fruit::from_str("plum"), STONEFRUIT);
125        assert_eq!(Fruit::from_str("pear"), UNRECOGNIZED);
126        assert_eq!(Fruit::from_str("@tasty"), ANN_TASTY);
127        assert_eq!(Fruit::from_str("@tastier"), ANN_UNRECOGNIZED);
128
129        assert_eq!(APPLE.idx(), 0);
130        assert_eq!(ORANGE.idx(), 1);
131        assert_eq!(ANN_UNRECOGNIZED.idx(), 8);
132        assert_eq!(Fruit::n_vals(), 9);
133
134        assert_eq!(Fruit::from_idx(0), Some(APPLE));
135        assert_eq!(Fruit::from_idx(8), Some(ANN_UNRECOGNIZED));
136        assert_eq!(Fruit::from_idx(9), None);
137
138        assert_eq!(Fruit::idx_to_str(3), "guava");
139        assert_eq!(Fruit::idx_to_str(999), "<out of range>");
140
141        assert_eq!(APPLE.to_str(), "apple");
142        assert_eq!(GUAVA.to_str(), "guava");
143        assert_eq!(ANN_TASTY.to_str(), "@tasty");
144        assert_eq!(STONEFRUIT.to_str(), "cherry/plum");
145        assert_eq!(UNRECOGNIZED.to_str(), "<unrecognized>");
146        assert_eq!(ANN_UNRECOGNIZED.to_str(), "<unrecognized annotation>");
147
148        assert!(!GUAVA.is_annotation());
149        assert!(!STONEFRUIT.is_annotation());
150        assert!(!UNRECOGNIZED.is_annotation());
151        assert!(ANN_TASTY.is_annotation());
152        assert!(ANN_UNRECOGNIZED.is_annotation());
153    }
154}