1pub mod config;
5
6pub use config::OutputConfig;
7
8use convert_case::{Case, Casing};
9use globetrotter_model as model;
10use quote::{format_ident, quote};
11
12#[must_use]
14pub fn preamble() -> String {
15 indoc::formatdoc!(
16 r"
17 //
18 // AUTOGENERATED. DO NOT EDIT.
19 // generated by globetrotter v{version}.
20 //
21 ",
22 version = std::env!("CARGO_PKG_VERSION"),
23 )
24}
25
26fn argument_to_rust_field_name(name: &str) -> String {
27 let field_name = name.replace(' ', "").replace(['-', '.'], "_");
28 field_name.to_case(Case::Snake)
29}
30
31#[must_use]
34pub fn key_to_rust_enum_variant(key: &str) -> String {
35 let variant_name = key.replace(' ', "").replace(['-', '.'], "_");
36 variant_name.to_case(Case::UpperCamel)
37}
38
39#[derive(Clone, Copy, Debug)]
44struct Comparisons {
45 eq: bool,
46 partial_ord: bool,
47 ord: bool,
48}
49
50impl Comparisons {
51 const TOTAL: Self = Self {
53 eq: true,
54 partial_ord: true,
55 ord: true,
56 };
57 const PARTIAL: Self = Self {
59 eq: false,
60 partial_ord: true,
61 ord: false,
62 };
63 const EQ_ONLY: Self = Self {
65 eq: true,
66 partial_ord: false,
67 ord: false,
68 };
69
70 fn and(self, other: Self) -> Self {
72 Self {
73 eq: self.eq && other.eq,
74 partial_ord: self.partial_ord && other.partial_ord,
75 ord: self.ord && other.ord,
76 }
77 }
78}
79
80struct RustType {
82 tokens: proc_macro2::TokenStream,
83 borrows: bool,
86 comparisons: Comparisons,
87}
88
89impl RustType {
90 fn owned(tokens: proc_macro2::TokenStream) -> Self {
92 Self {
93 tokens,
94 borrows: false,
95 comparisons: Comparisons::TOTAL,
96 }
97 }
98}
99
100trait IntoRustType {
101 fn into_rust_type(self) -> RustType;
102}
103
104impl IntoRustType for model::ArgumentType {
105 fn into_rust_type(self) -> RustType {
106 match self {
107 Self::Number | Self::Integer => RustType::owned(quote! {i64}),
108 Self::Float => RustType {
109 tokens: quote! {f64},
110 borrows: false,
111 comparisons: Comparisons::PARTIAL,
112 },
113 Self::Boolean => RustType::owned(quote! {bool}),
114 Self::String | Self::Iso8601DateTimeString => RustType {
117 tokens: quote! {&'a str},
118 borrows: true,
119 comparisons: Comparisons::TOTAL,
120 },
121 Self::Any => RustType {
122 tokens: quote! {serde_json::Value},
123 borrows: false,
124 comparisons: Comparisons::EQ_ONLY,
125 },
126 }
127 }
128}
129
130#[derive(thiserror::Error, Debug)]
132pub struct DuplicateIdentifierError {
133 identifier: String,
134 keys: Vec<String>,
135}
136
137impl std::fmt::Display for DuplicateIdentifierError {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 write!(
140 f,
141 "duplicate identifier `{}` (used by {})",
142 self.identifier,
143 self.keys.join(", ")
144 )
145 }
146}
147
148#[derive(thiserror::Error, Debug)]
150pub struct DuplicateFieldError {
151 field: String,
152 enum_variant: String,
153 arguments: Vec<String>,
154 key: String,
155}
156
157impl std::fmt::Display for DuplicateFieldError {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 write!(
160 f,
161 "{}: duplicate field `{}` used by arguments {} of variant `{}`",
162 self.key,
163 self.field,
164 self.arguments
165 .iter()
166 .map(|arg| format!("{arg:?}"))
167 .collect::<Vec<_>>()
168 .join(", "),
169 self.enum_variant,
170 )
171 }
172}
173
174#[derive(thiserror::Error, Debug)]
176pub enum Error {
177 #[error(transparent)]
179 DuplicateIdentifier(#[from] DuplicateIdentifierError),
180 #[error(transparent)]
182 DuplicateField(#[from] DuplicateFieldError),
183 #[error("{0}")]
185 Syn(String),
186}
187
188struct Variant {
190 tokens: proc_macro2::TokenStream,
191 borrows: bool,
193 comparisons: Comparisons,
195}
196
197fn generate_variant(
200 safe_key: &str,
201 key: &str,
202 translation: &model::Translation,
203) -> Result<Variant, Error> {
204 use itertools::Itertools;
205
206 let fields: Vec<_> = translation
207 .arguments
208 .iter()
209 .map(|(name, typ)| (argument_to_rust_field_name(name), name, typ))
210 .collect();
211
212 let duplicates: Vec<_> = fields
213 .iter()
214 .duplicates_by(|(safe_name, _, _)| safe_name)
215 .collect();
216
217 if let Some(first) = duplicates.first() {
218 let field = first.0.clone();
219 let arguments = duplicates
220 .into_iter()
221 .map(|(_, key, _)| (*key).clone())
222 .collect();
223 return Err(Error::from(DuplicateFieldError {
224 field,
225 arguments,
226 enum_variant: safe_key.to_string(),
227 key: key.to_string(),
228 }));
229 }
230
231 let mut borrows = false;
232 let mut comparisons = Comparisons::TOTAL;
233 let fields: Vec<_> = fields
234 .into_iter()
235 .map(|(safe_name, name, typ)| {
236 let field_ident = format_ident!("{safe_name}");
237 let typ = typ.into_rust_type();
238 borrows |= typ.borrows;
239 comparisons = comparisons.and(typ.comparisons);
240 let typ = typ.tokens;
241 quote! {
242 #[serde(rename = #name)]
243 #field_ident: #typ,
244 }
245 })
246 .collect();
247
248 let variant_name_ident = format_ident!("{safe_key}");
249 let tokens = quote! {
250 #variant_name_ident {
251 #(#fields)*
252 },
253 };
254 Ok(Variant {
255 tokens,
256 borrows,
257 comparisons,
258 })
259}
260
261pub fn generate_translation_enum(translations: &model::Translations) -> Result<String, Error> {
272 use itertools::Itertools;
273
274 let enum_variant_names: Vec<_> = translations
276 .0
277 .iter()
278 .map(|(key, translation)| (key_to_rust_enum_variant(key.as_ref()), key, translation))
279 .collect();
280
281 let duplicates: Vec<_> = enum_variant_names
283 .iter()
284 .duplicates_by(|(safe_key, _, _)| safe_key)
285 .collect();
286
287 if let Some(first) = duplicates.first() {
288 let identifier = first.0.clone();
289 let keys = duplicates
290 .into_iter()
291 .map(|(_, key, _)| key.to_string())
292 .collect();
293 return Err(DuplicateIdentifierError { identifier, keys }.into());
294 }
295
296 let mut uses_lifetime = false;
298 let mut comparisons = Comparisons::TOTAL;
299 let mut enum_variants = Vec::with_capacity(enum_variant_names.len());
300 for (safe_key, key, translation) in &enum_variant_names {
301 let variant = generate_variant(safe_key, key.as_ref(), translation)?;
302 uses_lifetime |= variant.borrows;
303 comparisons = comparisons.and(variant.comparisons);
304 enum_variants.push(variant.tokens);
305 }
306
307 let enum_variant_keys: Vec<_> = enum_variant_names
309 .iter()
310 .map(|(safe_key, key, _)| {
311 let variant_name_ident = format_ident!("{safe_key}");
312 let key = key.as_ref();
313 quote! {
314 Self::#variant_name_ident { .. } => #key,
315 }
316 })
317 .collect();
318
319 let generics: syn::Generics = if uses_lifetime {
321 syn::parse_quote!(<'a>)
322 } else {
323 syn::Generics::default()
324 };
325 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
326
327 let eq = comparisons.eq.then(|| quote! { Eq, });
330 let partial_ord = comparisons.partial_ord.then(|| quote! { PartialOrd, });
331 let ord = comparisons.ord.then(|| quote! { Ord, });
332
333 let out = quote! {
334 #[derive(
335 Debug, Clone, PartialEq, #eq #partial_ord #ord ::serde::Serialize, ::serde::Deserialize,
336 )]
337 #[serde(untagged)]
338 pub enum Translation #generics {
339 #(#enum_variants)*
340 }
341
342 impl #impl_generics Translation #ty_generics #where_clause {
343 pub fn key(&self) -> &'static str {
344 match self {
345 #(#enum_variant_keys)*
346 }
347 }
348 }
349 };
350
351 let code = pretty_print(&out).map_err(|err| Error::Syn(err.to_string()))?;
353 let code = format!("{}\n{}", preamble(), code);
354 Ok(code)
355}
356
357fn pretty_print<T>(input: T) -> Result<String, syn::Error>
358where
359 T: quote::ToTokens,
360{
361 let file: syn::File = syn::parse2(quote! { #input })?;
362 Ok(prettyplease::unparse(&file))
363}
364
365#[cfg(test)]
366mod tests {
367 use color_eyre::eyre;
368 use globetrotter_model::{self as model, diagnostics::Spanned};
369 use similar_asserts::assert_eq as sim_assert_eq;
370
371 #[test_util::test]
374 fn generate_enum_with_lifetime() -> eyre::Result<()> {
375 let translations = [
376 (
377 Spanned::dummy("test.one".to_string()),
378 model::Translation {
379 language: [(
380 model::Language::En,
381 Spanned::dummy("test.one in en".to_string()),
382 )]
383 .into_iter()
384 .collect(),
385 arguments: [].into_iter().collect(),
386 file_id: 0,
387 allow: std::collections::BTreeSet::new(),
388 },
389 ),
390 (
391 Spanned::dummy("test.two".to_string()),
392 model::Translation {
393 language: [(
394 model::Language::En,
395 Spanned::dummy("test.two in en".to_string()),
396 )]
397 .into_iter()
398 .collect(),
399 arguments: [
400 ("arg-one".to_string(), model::ArgumentType::String),
401 ("ArgTwo".to_string(), model::ArgumentType::Number),
402 ("Arg_Three".to_string(), model::ArgumentType::Any),
403 ("ArgFour".to_string(), model::ArgumentType::Boolean),
404 ("ArgFive".to_string(), model::ArgumentType::Integer),
405 ]
406 .into_iter()
407 .collect(),
408 file_id: 0,
409 allow: std::collections::BTreeSet::new(),
410 },
411 ),
412 ];
413 let translations = model::Translations(translations.into_iter().collect());
414 let have = super::generate_translation_enum(&translations)?;
415 println!("{have}");
416
417 let want = indoc::indoc! {r#"
418 #[derive(Debug, Clone, PartialEq, Eq, ::serde::Serialize, ::serde::Deserialize)]
419 #[serde(untagged)]
420 pub enum Translation<'a> {
421 TestOne {},
422 TestTwo {
423 #[serde(rename = "arg-one")]
424 arg_one: &'a str,
425 #[serde(rename = "ArgTwo")]
426 arg_two: i64,
427 #[serde(rename = "Arg_Three")]
428 arg_three: serde_json::Value,
429 #[serde(rename = "ArgFour")]
430 arg_four: bool,
431 #[serde(rename = "ArgFive")]
432 arg_five: i64,
433 },
434 }
435 impl<'a> Translation<'a> {
436 pub fn key(&self) -> &'static str {
437 match self {
438 Self::TestOne { .. } => "test.one",
439 Self::TestTwo { .. } => "test.two",
440 }
441 }
442 }
443 "# };
444 let want = format!("{}\n{}", super::preamble(), want);
445 sim_assert_eq!(have: have, want: want);
446 Ok(())
447 }
448
449 #[test_util::test]
451 fn generate_enum_with_float() -> eyre::Result<()> {
452 let translations = [(
453 Spanned::dummy("cart.total".to_string()),
454 model::Translation {
455 language: [(
456 model::Language::En,
457 Spanned::dummy("{{count}} items for {{price}}".to_string()),
458 )]
459 .into_iter()
460 .collect(),
461 arguments: [
462 ("count".to_string(), model::ArgumentType::Integer),
463 ("price".to_string(), model::ArgumentType::Float),
464 ]
465 .into_iter()
466 .collect(),
467 file_id: 0,
468 allow: std::collections::BTreeSet::new(),
469 },
470 )];
471 let translations = model::Translations(translations.into_iter().collect());
472 let have = super::generate_translation_enum(&translations)?;
473 println!("{have}");
474
475 let want = indoc::indoc! {r#"
476 #[derive(Debug, Clone, PartialEq, PartialOrd, ::serde::Serialize, ::serde::Deserialize)]
477 #[serde(untagged)]
478 pub enum Translation {
479 CartTotal {
480 #[serde(rename = "count")]
481 count: i64,
482 #[serde(rename = "price")]
483 price: f64,
484 },
485 }
486 impl Translation {
487 pub fn key(&self) -> &'static str {
488 match self {
489 Self::CartTotal { .. } => "cart.total",
490 }
491 }
492 }
493 "# };
494 let want = format!("{}\n{}", super::preamble(), want);
495 sim_assert_eq!(have: have, want: want);
496 Ok(())
497 }
498
499 #[test_util::test]
501 fn generate_enum_without_lifetime() -> eyre::Result<()> {
502 let translations = [
503 (
504 Spanned::dummy("test.one".to_string()),
505 model::Translation {
506 language: [(
507 model::Language::En,
508 Spanned::dummy("test.one in en".to_string()),
509 )]
510 .into_iter()
511 .collect(),
512 arguments: [].into_iter().collect(),
513 file_id: 0,
514 allow: std::collections::BTreeSet::new(),
515 },
516 ),
517 (
518 Spanned::dummy("test.two".to_string()),
519 model::Translation {
520 language: [(
521 model::Language::En,
522 Spanned::dummy("test.two in en".to_string()),
523 )]
524 .into_iter()
525 .collect(),
526 arguments: [("ArgTwo".to_string(), model::ArgumentType::Number)]
527 .into_iter()
528 .collect(),
529 file_id: 0,
530 allow: std::collections::BTreeSet::new(),
531 },
532 ),
533 ];
534 let translations = model::Translations(translations.into_iter().collect());
535 let have = super::generate_translation_enum(&translations)?;
536 println!("{have}");
537
538 let want = indoc::indoc! {r#"
539 #[derive(
540 Debug,
541 Clone,
542 PartialEq,
543 Eq,
544 PartialOrd,
545 Ord,
546 ::serde::Serialize,
547 ::serde::Deserialize,
548 )]
549 #[serde(untagged)]
550 pub enum Translation {
551 TestOne {},
552 TestTwo { #[serde(rename = "ArgTwo")] arg_two: i64 },
553 }
554 impl Translation {
555 pub fn key(&self) -> &'static str {
556 match self {
557 Self::TestOne { .. } => "test.one",
558 Self::TestTwo { .. } => "test.two",
559 }
560 }
561 }
562 "# };
563 let want = format!("{}\n{}", super::preamble(), want);
564 sim_assert_eq!(have: have, want: want);
565 Ok(())
566 }
567}