]>
Commit | Line | Data |
---|---|---|
799c993d WB |
1 | //! Serde support module. |
2 | //! | |
3 | //! The `#![api]` macro needs to be able to cope with some `#[serde(...)]` attributes such as | |
4 | //! `rename` and `rename_all`. | |
5 | ||
6 | use std::convert::TryFrom; | |
7 | ||
b232b580 WB |
8 | use syn::punctuated::Punctuated; |
9 | use syn::Token; | |
799c993d WB |
10 | |
11 | /// Serde name types. | |
e461be1c | 12 | #[allow(clippy::enum_variant_names)] |
799c993d WB |
13 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] |
14 | pub enum RenameAll { | |
15 | LowerCase, | |
16 | UpperCase, | |
17 | PascalCase, | |
18 | CamelCase, | |
19 | SnakeCase, | |
20 | ScreamingSnakeCase, | |
21 | KebabCase, | |
22 | ScreamingKebabCase, | |
23 | } | |
24 | ||
25 | impl TryFrom<&syn::Lit> for RenameAll { | |
26 | type Error = syn::Error; | |
27 | fn try_from(s: &syn::Lit) -> Result<Self, syn::Error> { | |
28 | match s { | |
29 | syn::Lit::Str(s) => Self::try_from(s), | |
30 | _ => bail!(s => "expected rename type as string"), | |
31 | } | |
32 | } | |
33 | } | |
34 | ||
35 | impl TryFrom<&syn::LitStr> for RenameAll { | |
36 | type Error = syn::Error; | |
37 | fn try_from(s: &syn::LitStr) -> Result<Self, syn::Error> { | |
38 | let s = s.value(); | |
39 | if s == "lowercase" { | |
40 | Ok(RenameAll::LowerCase) | |
41 | } else if s == "UPPERCASE" { | |
42 | Ok(RenameAll::UpperCase) | |
43 | } else if s == "PascalCase" { | |
44 | Ok(RenameAll::PascalCase) | |
45 | } else if s == "camelCase" { | |
46 | Ok(RenameAll::CamelCase) | |
47 | } else if s == "snake_case" { | |
48 | Ok(RenameAll::SnakeCase) | |
49 | } else if s == "SCREAMING_SNAKE_CASE" { | |
50 | Ok(RenameAll::ScreamingSnakeCase) | |
51 | } else if s == "kebab-case" { | |
52 | Ok(RenameAll::KebabCase) | |
53 | } else if s == "SCREAMING-KEBAB-CASE" { | |
54 | Ok(RenameAll::ScreamingKebabCase) | |
55 | } else { | |
56 | bail!(&s => "unhandled `rename_all` type: {}", s.to_string()) | |
57 | } | |
58 | } | |
59 | } | |
60 | ||
61 | impl RenameAll { | |
62 | /// Like in serde, we assume that fields are in `snake_case` and enum variants are in | |
63 | /// `PascalCase`, so we only perform the changes required for fields here! | |
64 | pub fn apply_to_field(&self, s: &str) -> String { | |
65 | match self { | |
66 | RenameAll::SnakeCase => s.to_owned(), // this is our source type | |
67 | RenameAll::ScreamingSnakeCase => s.to_uppercase(), // capitalized source type | |
68 | RenameAll::LowerCase => s.to_lowercase(), | |
69 | RenameAll::UpperCase => s.to_uppercase(), | |
70 | RenameAll::PascalCase => { | |
71 | // Strip underscores and capitalize instead: | |
72 | let mut out = String::new(); | |
73 | let mut cap = true; | |
74 | for c in s.chars() { | |
75 | if c == '_' { | |
76 | cap = true; | |
77 | } else if cap { | |
78 | cap = false; | |
79 | out.push(c.to_ascii_uppercase()); | |
80 | } else { | |
81 | out.push(c.to_ascii_lowercase()); | |
82 | } | |
83 | } | |
84 | out | |
85 | } | |
86 | RenameAll::CamelCase => { | |
87 | let s = RenameAll::PascalCase.apply_to_field(s); | |
88 | s[..1].to_ascii_lowercase() + &s[1..] | |
89 | } | |
90 | RenameAll::KebabCase => s.replace('_', "-"), | |
91 | RenameAll::ScreamingKebabCase => s.replace('_', "-").to_ascii_uppercase(), | |
92 | } | |
93 | } | |
94 | ||
95 | /// Like in serde, we assume that fields are in `snake_case` and enum variants are in | |
96 | /// `PascalCase`, so we only perform the changes required for enum variants here! | |
97 | pub fn apply_to_variant(&self, s: &str) -> String { | |
98 | match self { | |
99 | RenameAll::PascalCase => s.to_owned(), // this is our source type | |
100 | RenameAll::CamelCase => s[..1].to_ascii_lowercase() + &s[1..], | |
101 | RenameAll::LowerCase => s.to_lowercase(), | |
102 | RenameAll::UpperCase => s.to_uppercase(), | |
103 | RenameAll::SnakeCase => { | |
104 | // Relatively simple: all lower-case, and new words get split by underscores: | |
105 | let mut out = String::new(); | |
106 | for (i, c) in s.char_indices() { | |
107 | if i > 0 && c.is_uppercase() { | |
108 | out.push('_'); | |
109 | } | |
110 | out.push(c.to_ascii_lowercase()); | |
111 | } | |
112 | out | |
113 | } | |
114 | RenameAll::KebabCase => RenameAll::SnakeCase.apply_to_variant(s).replace('_', "-"), | |
115 | RenameAll::ScreamingSnakeCase => RenameAll::SnakeCase | |
116 | .apply_to_variant(s) | |
117 | .to_ascii_uppercase(), | |
118 | RenameAll::ScreamingKebabCase => RenameAll::KebabCase | |
119 | .apply_to_variant(s) | |
120 | .to_ascii_uppercase(), | |
121 | } | |
122 | } | |
123 | } | |
124 | ||
125 | /// `serde` container attributes we support | |
126 | #[derive(Default)] | |
127 | pub struct ContainerAttrib { | |
dd5cd3f3 | 128 | pub rename_all: Option<RenameAll>, |
799c993d WB |
129 | } |
130 | ||
131 | impl TryFrom<&[syn::Attribute]> for ContainerAttrib { | |
132 | type Error = syn::Error; | |
133 | ||
134 | fn try_from(attributes: &[syn::Attribute]) -> Result<Self, syn::Error> { | |
135 | let mut this: Self = Default::default(); | |
136 | ||
137 | for attrib in attributes { | |
b232b580 WB |
138 | let list = match &attrib.meta { |
139 | syn::Meta::List(list) if list.path.is_ident("serde") => list, | |
140 | _ => continue, | |
141 | }; | |
142 | ||
143 | let args = | |
144 | list.parse_args_with(Punctuated::<syn::Meta, Token![,]>::parse_terminated)?; | |
799c993d | 145 | |
b232b580 WB |
146 | for arg in args { |
147 | if let syn::Meta::NameValue(var) = arg { | |
148 | if !var.path.is_ident("rename_all") { | |
149 | continue; | |
150 | } | |
151 | match &var.value { | |
152 | syn::Expr::Lit(lit) => { | |
153 | let rename_all = RenameAll::try_from(&lit.lit)?; | |
154 | if this.rename_all.is_some() && this.rename_all != Some(rename_all) { | |
155 | error!(var.value => "multiple conflicting 'rename_all' attributes"); | |
156 | } | |
157 | this.rename_all = Some(rename_all); | |
799c993d | 158 | } |
b232b580 | 159 | _ => error!(var.value => "invalid 'rename_all' value type"), |
799c993d WB |
160 | } |
161 | } | |
162 | } | |
163 | } | |
164 | ||
165 | Ok(this) | |
166 | } | |
167 | } | |
168 | ||
89b29415 | 169 | /// `serde` field attributes we support |
799c993d | 170 | #[derive(Default)] |
fa9a50a0 | 171 | pub struct FieldAttrib { |
12674a37 | 172 | pub rename: Option<syn::LitStr>, |
fbc9be47 | 173 | pub flatten: bool, |
799c993d WB |
174 | } |
175 | ||
fa9a50a0 | 176 | impl FieldAttrib { |
8ebcd68a | 177 | pub fn parse_attribute(&mut self, attrib: &syn::Attribute) -> Result<(), syn::Error> { |
b232b580 WB |
178 | let list = match &attrib.meta { |
179 | syn::Meta::List(list) if list.path.is_ident("serde") => list, | |
180 | _ => return Ok(()), | |
181 | }; | |
182 | ||
183 | let args = list.parse_args_with(Punctuated::<syn::Meta, Token![,]>::parse_terminated)?; | |
184 | ||
185 | for arg in args { | |
186 | let path = arg.path(); | |
187 | if path.is_ident("rename") { | |
188 | match &arg.require_name_value()?.value { | |
189 | syn::Expr::Lit(syn::ExprLit { | |
190 | lit: syn::Lit::Str(rename), | |
191 | .. | |
192 | }) => { | |
193 | if self.rename.is_some() && self.rename.as_ref() != Some(rename) { | |
194 | error!(&rename => "multiple conflicting 'rename' attributes"); | |
799c993d | 195 | } |
b232b580 | 196 | self.rename = Some(rename.clone()); |
799c993d | 197 | } |
b232b580 | 198 | value => error!(value => "'rename' value must be a string literal"), |
799c993d | 199 | } |
b232b580 WB |
200 | } else if path.is_ident("flatten") { |
201 | arg.require_path_only()?; | |
202 | self.flatten = true; | |
799c993d WB |
203 | } |
204 | } | |
205 | ||
8ebcd68a WB |
206 | Ok(()) |
207 | } | |
208 | } | |
209 | ||
fa9a50a0 | 210 | impl TryFrom<&[syn::Attribute]> for FieldAttrib { |
8ebcd68a WB |
211 | type Error = syn::Error; |
212 | ||
213 | fn try_from(attributes: &[syn::Attribute]) -> Result<Self, syn::Error> { | |
214 | let mut this: Self = Default::default(); | |
215 | ||
216 | for attrib in attributes { | |
217 | this.parse_attribute(attrib)?; | |
218 | } | |
219 | ||
799c993d WB |
220 | Ok(this) |
221 | } | |
222 | } | |
89b29415 WB |
223 | |
224 | /// `serde` variant attributes we support | |
225 | #[derive(Default)] | |
226 | pub struct VariantAttrib { | |
227 | pub rename: Option<syn::LitStr>, | |
228 | } | |
229 | ||
230 | impl VariantAttrib { | |
231 | pub fn parse_attribute(&mut self, attrib: &syn::Attribute) -> Result<(), syn::Error> { | |
232 | let list = match &attrib.meta { | |
233 | syn::Meta::List(list) if list.path.is_ident("serde") => list, | |
234 | _ => return Ok(()), | |
235 | }; | |
236 | ||
237 | let args = list.parse_args_with(Punctuated::<syn::Meta, Token![,]>::parse_terminated)?; | |
238 | ||
239 | for arg in args { | |
240 | let path = arg.path(); | |
241 | if path.is_ident("rename") { | |
242 | match &arg.require_name_value()?.value { | |
243 | syn::Expr::Lit(syn::ExprLit { | |
244 | lit: syn::Lit::Str(rename), | |
245 | .. | |
246 | }) => { | |
247 | if self.rename.is_some() && self.rename.as_ref() != Some(rename) { | |
248 | error!(&rename => "multiple conflicting 'rename' attributes"); | |
249 | } | |
250 | self.rename = Some(rename.clone()); | |
251 | } | |
252 | value => error!(value => "'rename' value must be a string literal"), | |
253 | } | |
254 | } | |
255 | } | |
256 | ||
257 | Ok(()) | |
258 | } | |
259 | } | |
260 | ||
261 | impl TryFrom<&[syn::Attribute]> for VariantAttrib { | |
262 | type Error = syn::Error; | |
263 | ||
264 | fn try_from(attributes: &[syn::Attribute]) -> Result<Self, syn::Error> { | |
265 | let mut this: Self = Default::default(); | |
266 | ||
267 | for attrib in attributes { | |
268 | this.parse_attribute(attrib)?; | |
269 | } | |
270 | ||
271 | Ok(this) | |
272 | } | |
273 | } |