1pub mod config;
5
6pub use config::OutputConfig;
7
8use convert_case::{Case, Casing};
9use globetrotter_model as model;
10use globetrotter_model::ext::iter::TryUnzipExt;
11use quote::{format_ident, quote};
12
13#[must_use]
15pub fn preamble() -> String {
16 indoc::formatdoc!(
17 r"
18 //
19 // AUTOGENERATED. DO NOT EDIT.
20 // generated by globetrotter v{version}.
21 //
22 ",
23 version = std::env!("CARGO_PKG_VERSION"),
24 )
25}
26
27fn argument_to_rust_field_name(name: &str) -> String {
28 let field_name = name.replace(' ', "").replace(['-', '.'], "_");
29 field_name.to_case(Case::Snake)
30}
31
32fn key_to_rust_enum_variant(key: &str) -> String {
33 let variant_name = key.replace(' ', "").replace(['-', '.'], "_");
34 variant_name.to_case(Case::UpperCamel)
35}
36
37trait IntoTokenStream {
38 fn into_token_stream(self) -> (proc_macro2::TokenStream, bool);
39}
40
41impl IntoTokenStream for model::ArgumentType {
42 fn into_token_stream(self) -> (proc_macro2::TokenStream, bool) {
43 match self {
44 Self::Number => {
45 let tokens = quote! {i64};
46 (tokens, false)
47 }
48 Self::String | Self::Iso8601DateTimeString => {
50 let tokens = quote! {&'a str};
51 (tokens, true)
52 }
53 Self::Any => {
54 let tokens = quote! {serde_json::Value};
55 (tokens, false)
56 }
57 }
58 }
59}
60
61#[derive(thiserror::Error, Debug)]
64pub struct DuplicateIdentifierError {
65 identifier: String,
66 keys: Vec<String>,
67}
68
69impl std::fmt::Display for DuplicateIdentifierError {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 write!(
72 f,
73 "duplicate identifier `{}` (used by {})",
74 self.identifier,
75 self.keys.join(", ")
76 )
77 }
78}
79
80#[derive(thiserror::Error, Debug)]
83pub struct DuplicateFieldError {
84 field: String,
85 enum_variant: String,
86 arguments: Vec<String>,
87 key: String,
88}
89
90impl std::fmt::Display for DuplicateFieldError {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 write!(
93 f,
94 "{}: duplicate field `{}` used by arguments {} of variant `{}`",
95 self.key,
96 self.field,
97 self.arguments
98 .iter()
99 .map(|arg| format!("{arg:?}"))
100 .collect::<Vec<_>>()
101 .join(", "),
102 self.enum_variant,
103 )
104 }
105}
106
107#[derive(thiserror::Error, Debug)]
109pub enum Error {
110 #[error(transparent)]
112 DuplicateIdentifier(#[from] DuplicateIdentifierError),
113 #[error(transparent)]
115 DuplicateField(#[from] DuplicateFieldError),
116 #[error("{0}")]
118 Syn(String),
119}
120
121pub fn generate_translation_enum(translations: &model::Translations) -> Result<String, Error> {
132 use itertools::Itertools;
133
134 let enum_variant_names: Vec<_> = translations
137 .0
138 .iter()
139 .map(|(key, translation)| (key_to_rust_enum_variant(key.as_ref()), key, translation))
140 .collect();
141
142 let duplicates: Vec<_> = enum_variant_names
144 .iter()
145 .duplicates_by(|(safe_key, _, _)| safe_key)
146 .collect();
147
148 if let Some(first) = duplicates.first() {
149 let identifier = first.0.clone();
150 let keys = duplicates
151 .into_iter()
152 .map(|(_, key, _)| key.to_string())
153 .collect();
154 return Err(DuplicateIdentifierError { identifier, keys }.into());
155 }
156
157 let enum_variants = enum_variant_names
158 .iter()
159 .map(|(safe_key, key, translation)| {
160 let fields: Vec<_> = translation
161 .arguments
162 .iter()
163 .map(|(name, typ)| (argument_to_rust_field_name(name), name, typ))
164 .collect();
165
166 let duplicates: Vec<_> = fields
168 .iter()
169 .duplicates_by(|(safe_name, _, _)| safe_name)
170 .collect();
171
172 if let Some(first) = duplicates.first() {
173 let field = first.0.clone();
174 let arguments = duplicates
175 .into_iter()
176 .map(|(_, key, _)| (*key).clone())
177 .collect();
178 return Err(Error::from(DuplicateFieldError {
179 field,
180 arguments,
181 enum_variant: safe_key.clone(),
182 key: key.to_string(),
183 }));
184 }
185
186 let fields = fields.into_iter().map(|(safe_name, name, typ)| {
187 let field_ident = format_ident!("{safe_name}");
188 let (typ, uses_lifetime) = typ.into_token_stream();
189 let tokens = quote! {
190 #[serde(rename = #name)]
191 #field_ident: #typ,
192 };
193 (tokens, uses_lifetime)
194 });
195
196 let (fields, uses_lifetime): (Vec<_>, Vec<_>) = fields.unzip();
197 let uses_lifetime = uses_lifetime.iter().any(|v| *v);
198
199 let variant_name_ident = format_ident!("{safe_key}");
200 let tokens = quote! {
201 #variant_name_ident {
202 #(#fields)*
203 },
204 };
205 Ok((tokens, uses_lifetime))
206 });
207
208 let (enum_variants, uses_lifetime): (Vec<_>, Vec<_>) = enum_variants.try_unzip()?;
209 let uses_lifetime = uses_lifetime.iter().any(|v| *v);
210
211 let enum_variant_keys: Vec<_> = enum_variant_names
212 .iter()
213 .map(|(safe_key, key, _)| {
214 let variant_name_ident = format_ident!("{safe_key}");
215 let key = key.as_ref();
216 quote! {
217 Self::#variant_name_ident { .. } => #key,
218 }
219 })
220 .collect();
221
222 let generics: syn::Generics = if uses_lifetime {
224 syn::parse_quote!(<'a>)
225 } else {
226 syn::Generics::default()
227 };
228 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
229
230 let out = quote! {
231 #[derive(
232 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, ::serde::Serialize, ::serde::Deserialize,
233 )]
234 #[serde(untagged)]
235 pub enum Translation #generics {
236 #(#enum_variants)*
237 }
238
239 impl #impl_generics Translation #ty_generics #where_clause {
240 pub fn key(&self) -> &'static str {
241 match self {
242 #(#enum_variant_keys)*
243 }
244 }
245 }
246 };
247 let code = pretty_print(&out).map_err(|err| Error::Syn(err.to_string()))?;
248 let code = format!("{}\n{}", preamble(), code);
249 Ok(code)
250}
251
252fn pretty_print<T>(input: T) -> Result<String, syn::Error>
253where
254 T: quote::ToTokens,
255{
256 let file: syn::File = syn::parse2(quote! { #input })?;
257 Ok(prettyplease::unparse(&file))
258}
259
260#[cfg(test)]
261mod tests {
262 use color_eyre::eyre;
263 use globetrotter_model::{self as model, diagnostics::Spanned};
264 use similar_asserts::assert_eq as sim_assert_eq;
265
266 static INIT: std::sync::Once = std::sync::Once::new();
267
268 pub fn init() {
272 INIT.call_once(|| {
273 color_eyre::install().ok();
274 });
275 }
276
277 #[test]
278 fn generate_enum_with_lifetime() -> eyre::Result<()> {
279 crate::tests::init();
280
281 let translations = [
282 (
283 Spanned::dummy("test.one".to_string()),
284 model::Translation {
285 language: [(
286 model::Language::En,
287 Spanned::dummy("test.one in en".to_string()),
288 )]
289 .into_iter()
290 .collect(),
291 arguments: [].into_iter().collect(),
292 file_id: 0,
293 },
294 ),
295 (
296 Spanned::dummy("test.two".to_string()),
297 model::Translation {
298 language: [(
299 model::Language::En,
300 Spanned::dummy("test.two in en".to_string()),
301 )]
302 .into_iter()
303 .collect(),
304 arguments: [
305 ("arg-one".to_string(), model::ArgumentType::String),
306 ("ArgTwo".to_string(), model::ArgumentType::Number),
307 ("Arg_Three".to_string(), model::ArgumentType::Any),
308 ]
309 .into_iter()
310 .collect(),
311 file_id: 0,
312 },
313 ),
314 ];
315 let translations = model::Translations(translations.into_iter().collect());
316 let have = super::generate_translation_enum(&translations)?;
317 println!("{have}");
318
319 let want = indoc::indoc! {r#"
320 #[derive(
321 Debug,
322 Clone,
323 PartialEq,
324 Eq,
325 PartialOrd,
326 Ord,
327 ::serde::Serialize,
328 ::serde::Deserialize,
329 )]
330 #[serde(untagged)]
331 pub enum Translation<'a> {
332 TestOne {},
333 TestTwo {
334 #[serde(rename = "arg-one")]
335 arg_one: &'a str,
336 #[serde(rename = "ArgTwo")]
337 arg_two: i64,
338 #[serde(rename = "Arg_Three")]
339 arg_three: serde_json::Value,
340 },
341 }
342 impl<'a> Translation<'a> {
343 pub fn key(&self) -> &'static str {
344 match self {
345 Self::TestOne { .. } => "test.one",
346 Self::TestTwo { .. } => "test.two",
347 }
348 }
349 }
350 "# };
351 let want = format!("{}\n{}", super::preamble(), want);
352 sim_assert_eq!(have: have, want: want);
353 Ok(())
354 }
355
356 #[test]
357 fn generate_enum_without_lifetime() -> eyre::Result<()> {
358 crate::tests::init();
359
360 let translations = [
361 (
362 Spanned::dummy("test.one".to_string()),
363 model::Translation {
364 language: [(
365 model::Language::En,
366 Spanned::dummy("test.one in en".to_string()),
367 )]
368 .into_iter()
369 .collect(),
370 arguments: [].into_iter().collect(),
371 file_id: 0,
372 },
373 ),
374 (
375 Spanned::dummy("test.two".to_string()),
376 model::Translation {
377 language: [(
378 model::Language::En,
379 Spanned::dummy("test.two in en".to_string()),
380 )]
381 .into_iter()
382 .collect(),
383 arguments: [("ArgTwo".to_string(), model::ArgumentType::Number)]
384 .into_iter()
385 .collect(),
386 file_id: 0,
387 },
388 ),
389 ];
390 let translations = model::Translations(translations.into_iter().collect());
391 let have = super::generate_translation_enum(&translations)?;
392 println!("{have}");
393
394 let want = indoc::indoc! {r#"
395 #[derive(
396 Debug,
397 Clone,
398 PartialEq,
399 Eq,
400 PartialOrd,
401 Ord,
402 ::serde::Serialize,
403 ::serde::Deserialize,
404 )]
405 #[serde(untagged)]
406 pub enum Translation {
407 TestOne {},
408 TestTwo { #[serde(rename = "ArgTwo")] arg_two: i64 },
409 }
410 impl Translation {
411 pub fn key(&self) -> &'static str {
412 match self {
413 Self::TestOne { .. } => "test.one",
414 Self::TestTwo { .. } => "test.two",
415 }
416 }
417 }
418 "# };
419 let want = format!("{}\n{}", super::preamble(), want);
420 sim_assert_eq!(have: have, want: want);
421 Ok(())
422 }
423}