1use globetrotter_model as model;
2use std::sync::Arc;
3use swc_core::{
4 base::{Compiler, PrintArgs},
5 common::DUMMY_SP,
6 ecma::ast,
7};
8
9#[derive(thiserror::Error, Debug)]
10pub enum Error {
11 #[error(transparent)]
12 Codegen(anyhow::Error),
13}
14
15pub trait IntoAST<T> {
16 fn into_ast(self) -> T;
17}
18
19impl IntoAST<ast::TsType> for model::ArgumentType {
20 fn into_ast(self) -> ast::TsType {
21 match self {
22 Self::Number => ast::TsType::TsKeywordType(ast::TsKeywordType {
23 span: DUMMY_SP,
24 kind: ast::TsKeywordTypeKind::TsNumberKeyword,
25 }),
26 Self::String | Self::Iso8601DateTimeString => {
27 ast::TsType::TsKeywordType(ast::TsKeywordType {
28 span: DUMMY_SP,
29 kind: ast::TsKeywordTypeKind::TsStringKeyword,
30 })
31 }
32 Self::Any => ast::TsType::TsKeywordType(ast::TsKeywordType {
33 span: DUMMY_SP,
34 kind: ast::TsKeywordTypeKind::TsAnyKeyword,
35 }),
36 }
37 }
38}
39
40fn emit_code(compiler: &Compiler, program: &ast::Program) -> Result<String, anyhow::Error> {
41 let printed = compiler.print(
42 program,
43 PrintArgs {
44 preamble: &crate::preamble(),
45 ..PrintArgs::default()
46 },
47 )?;
48 Ok(printed.code)
49}
50
51fn type_annotation_for_translation(translation: &model::Translation) -> ast::TsType {
52 if !translation.is_template() {
53 return model::ArgumentType::String.into_ast();
55 }
56
57 let members = translation
59 .arguments
60 .iter()
61 .map(|(name, argument)| {
62 let key = ast::Expr::Lit(ast::Lit::Str(ast::Str {
63 span: DUMMY_SP,
64 value: name.clone().into(),
65 raw: None,
66 }));
67 ast::TsTypeElement::TsPropertySignature(ast::TsPropertySignature {
68 span: DUMMY_SP,
69 readonly: true,
70 key: Box::new(key),
71 computed: false,
72 optional: false,
73 type_ann: Some(Box::new(ast::TsTypeAnn {
74 span: DUMMY_SP,
75 type_ann: Box::new(argument.into_ast()),
76 })),
77 })
78 })
79 .collect::<Vec<_>>();
80
81 let values = ast::TsType::TsTypeLit(ast::TsTypeLit {
82 span: DUMMY_SP,
83 members,
84 });
85
86 let values_param = ast::TsFnParam::Ident(ast::BindingIdent {
87 id: ast::Ident::new_no_ctxt("values".into(), DUMMY_SP),
88 type_ann: Some(Box::new(ast::TsTypeAnn {
89 span: DUMMY_SP,
90 type_ann: Box::new(values),
91 })),
92 });
93
94 let return_type = ast::TsTypeAnn {
95 span: DUMMY_SP,
96 type_ann: Box::new(model::ArgumentType::String.into_ast()),
97 };
98
99 ast::TsType::TsFnOrConstructorType(ast::TsFnOrConstructorType::TsFnType(ast::TsFnType {
100 span: DUMMY_SP,
101 params: vec![values_param],
102 type_params: None,
103 type_ann: Box::new(return_type),
104 }))
105}
106
107fn type_members(
108 translations: &model::Translations,
109) -> impl Iterator<Item = ast::TsTypeElement> + use<'_> {
110 translations.0.iter().map(|(key, translation)| {
111 let type_annotation = type_annotation_for_translation(translation);
112 let key = ast::Expr::Lit(ast::Lit::Str(ast::Str {
113 span: DUMMY_SP,
114 value: key.to_string().into(),
115 raw: None,
116 }));
117 ast::TsTypeElement::TsPropertySignature(ast::TsPropertySignature {
118 span: DUMMY_SP,
119 readonly: true,
120 key: Box::new(key),
121 computed: false,
122 optional: false,
123 type_ann: Some(Box::new(ast::TsTypeAnn {
124 span: DUMMY_SP,
125 type_ann: Box::new(type_annotation),
126 })),
127 })
128 })
129}
130
131pub fn generate_translations_type_export(
137 translations: &model::Translations,
138) -> Result<String, Error> {
139 let members: Vec<_> = type_members(translations).collect();
140
141 let program = ast::Program::Module(ast::Module {
142 span: DUMMY_SP,
143 body: vec![ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportDecl(
144 ast::ExportDecl {
145 span: DUMMY_SP,
146 decl: ast::Decl::TsTypeAlias(Box::new(ast::TsTypeAliasDecl {
147 span: DUMMY_SP,
148 declare: false,
149 id: ast::Ident::new_no_ctxt("Translations".into(), DUMMY_SP),
150 type_params: None,
151 type_ann: Box::new(ast::TsType::TsTypeLit(ast::TsTypeLit {
152 span: DUMMY_SP,
153 members,
154 })),
155 })),
156 },
157 ))],
158 shebang: None,
159 });
160
161 let cm: swc_core::common::sync::Lrc<swc_core::common::SourceMap> =
162 swc_core::common::sync::Lrc::default();
163 let compiler = Compiler::new(cm);
164
165 emit_code(&compiler, &program).map_err(Error::Codegen)
166}
167
168#[cfg(test)]
169mod tests {
170 use color_eyre::eyre;
171 use globetrotter_model::{self as model, diagnostics::Spanned};
172 use indoc::indoc;
173 use similar_asserts::assert_eq as sim_assert_eq;
174 use std::sync::Arc;
175 use swc_core::{
176 base::{Compiler, PrintArgs},
177 common::DUMMY_SP,
178 ecma::ast,
179 };
180
181 trait IntoEyre {
182 fn into_eyre(self) -> eyre::Report;
183 }
184
185 impl IntoEyre for anyhow::Error {
186 fn into_eyre(self) -> eyre::Report {
187 eyre::eyre!(Box::new(self))
188 }
189 }
190
191 fn parse(
192 compiler: &Compiler,
193 fm: Arc<swc_core::common::SourceFile>,
194 ) -> eyre::Result<ast::Program> {
195 let emitter_writer = swc_core::common::errors::EmitterWriter::new(
196 Box::new(std::io::stderr()),
197 Some(compiler.cm.clone()),
198 true,
199 false,
200 );
201 let handler =
202 swc_core::common::errors::Handler::with_emitter(true, false, Box::new(emitter_writer));
203
204 let program = compiler
205 .parse_js(
206 fm,
207 &handler,
208 swc_core::ecma::ast::EsVersion::Es2022,
209 swc_core::ecma::parser::Syntax::Typescript(
210 swc_core::ecma::parser::TsSyntax::default(),
211 ),
212 swc_core::base::config::IsModule::Unknown,
213 Some(compiler.comments()),
214 )
215 .map_err(IntoEyre::into_eyre)?;
216 Ok(program)
217 }
218
219 #[test]
220 fn generate_type() -> eyre::Result<()> {
221 crate::tests::init();
222
223 let translations = model::Translations(
224 [
225 (
226 Spanned::dummy("test.one".to_string()),
227 model::Translation {
228 language: [(
229 model::Language::En,
230 Spanned::dummy("test.one in en".to_string()),
231 )]
232 .into_iter()
233 .collect(),
234 arguments: [].into_iter().collect(),
235 file_id: 0,
236 },
237 ),
238 (
239 Spanned::dummy("test.two".to_string()),
240 model::Translation {
241 language: [(
242 model::Language::En,
243 Spanned::dummy("test.two in en".to_string()),
244 )]
245 .into_iter()
246 .collect(),
247 arguments: [
248 ("arg-one".to_string(), model::ArgumentType::String),
249 ("ArgTwo".to_string(), model::ArgumentType::Number),
250 ("Arg_Three".to_string(), model::ArgumentType::Any),
251 ]
252 .into_iter()
253 .collect(),
254 file_id: 0,
255 },
256 ),
257 ]
258 .into_iter()
259 .collect(),
260 );
261 let have = super::generate_translations_type_export(&translations)?;
262 let want = indoc::indoc! {r#"
263 export type Translations = {
264 readonly "test.one": string;
265 readonly "test.two": (values: {
266 readonly "arg-one": string;
267 readonly "ArgTwo": number;
268 readonly "Arg_Three": any;
269 }) => string;
270 };
271 "# };
272 let want = format!("{}{}", crate::preamble(), want);
273 sim_assert_eq!(have: have, want: want);
274 Ok(())
275 }
276
277 #[test]
278 fn parse_reference_interface() -> eyre::Result<()> {
279 crate::tests::init();
280
281 let source_code = indoc! {r#"
282 export type Translations = {
283 test: string;
284 "other.value": (values: {
285 a: number;
286 b: string;
287 }) => string;
288 };
289 "#};
290
291 let cm: swc_core::common::sync::Lrc<swc_core::common::SourceMap> =
292 swc_core::common::sync::Lrc::default();
293 let fm = cm.new_source_file(
294 swc_core::common::FileName::Custom("./test.ts".to_string()).into(),
295 source_code.to_string(),
296 );
297 let ts_compiler = Compiler::new(cm);
298
299 let program = parse(&ts_compiler, fm)?;
300 dbg!(&program);
301
302 let have = super::emit_code(&ts_compiler, &program).map_err(IntoEyre::into_eyre)?;
303 println!("{have}");
304
305 let want = format!("{}{}", crate::preamble(), source_code);
306 sim_assert_eq!(have: have, want: want);
307 Ok(())
308 }
309}