1use crate::generate::util::{is_reducer_invokable, iter_reducers, iter_tables, iter_types, iter_unique_cols};
2use crate::indent_scope;
3
4use super::util::{collect_case, print_auto_generated_file_comment, type_ref_name};
5
6use std::collections::BTreeSet;
7use std::fmt::{self, Write};
8use std::ops::Deref;
9
10use convert_case::{Case, Casing};
11use spacetimedb_lib::sats::AlgebraicTypeRef;
12use spacetimedb_schema::def::{ModuleDef, ReducerDef, ScopedTypeName, TableDef, TypeDef};
13use spacetimedb_schema::identifier::Identifier;
14use spacetimedb_schema::schema::{Schema, TableSchema};
15use spacetimedb_schema::type_for_generate::{AlgebraicTypeDef, AlgebraicTypeUse, PrimitiveType};
16
17use super::code_indenter::{CodeIndenter, Indenter};
18use super::Lang;
19use std::path::PathBuf;
20
21type Imports = BTreeSet<AlgebraicTypeRef>;
22
23const INDENT: &str = " ";
24
25pub struct TypeScript;
26
27impl Lang for TypeScript {
28 fn table_filename(
29 &self,
30 _module: &spacetimedb_schema::def::ModuleDef,
31 table: &spacetimedb_schema::def::TableDef,
32 ) -> String {
33 table_module_name(&table.name) + ".ts"
34 }
35
36 fn type_filename(&self, type_name: &ScopedTypeName) -> String {
37 type_module_name(type_name) + ".ts"
38 }
39
40 fn reducer_filename(&self, reducer_name: &Identifier) -> String {
41 reducer_module_name(reducer_name) + ".ts"
42 }
43
44 fn format_files(&self, _generated_files: BTreeSet<PathBuf>) -> anyhow::Result<()> {
45 Ok(())
47 }
48
49 fn generate_type(&self, module: &ModuleDef, typ: &TypeDef) -> String {
50 let type_name = collect_case(Case::Pascal, typ.name.name_segments());
53
54 let mut output = CodeIndenter::new(String::new(), INDENT);
55 let out = &mut output;
56
57 print_file_header(out);
58
59 match &module.typespace_for_generate()[typ.ty] {
60 AlgebraicTypeDef::Product(product) => {
61 gen_and_print_imports(module, out, &product.elements, &[typ.ty]);
62 define_namespace_and_object_type_for_product(module, out, &type_name, &product.elements);
63 }
64 AlgebraicTypeDef::Sum(sum) => {
65 gen_and_print_imports(module, out, &sum.variants, &[typ.ty]);
66 define_namespace_and_types_for_sum(module, out, &type_name, &sum.variants);
67 }
68 AlgebraicTypeDef::PlainEnum(plain_enum) => {
69 let variants = plain_enum
70 .variants
71 .iter()
72 .cloned()
73 .map(|var| (var, AlgebraicTypeUse::Unit))
74 .collect::<Vec<_>>();
75 define_namespace_and_types_for_sum(module, out, &type_name, &variants);
76 }
77 }
78 out.newline();
79
80 output.into_inner()
81 }
82
83 fn generate_table(&self, module: &ModuleDef, table: &TableDef) -> String {
84 let schema = TableSchema::from_module_def(module, table, (), 0.into())
85 .validated()
86 .expect("Failed to generate table due to validation errors");
87
88 let mut output = CodeIndenter::new(String::new(), INDENT);
89 let out = &mut output;
90
91 print_file_header(out);
92
93 let type_ref = table.product_type_ref;
94 let row_type = type_ref_name(module, type_ref);
95 let row_type_module = type_ref_module_name(module, type_ref);
96
97 writeln!(out, "import {{ {row_type} }} from \"./{row_type_module}\";");
98
99 let product_def = module.typespace_for_generate()[type_ref].as_product().unwrap();
100
101 gen_and_print_imports(
105 module,
106 out,
107 &product_def.elements,
108 &[], );
110
111 writeln!(
112 out,
113 "import {{ EventContext, Reducer, RemoteReducers, RemoteTables }} from \".\";"
114 );
115
116 let table_name = table.name.deref();
117 let table_name_pascalcase = table.name.deref().to_case(Case::Pascal);
118 let table_handle = table_name_pascalcase.clone() + "TableHandle";
119 let accessor_method = table_method_name(&table.name);
120
121 writeln!(out);
122
123 write!(
124 out,
125 "/**
126 * Table handle for the table `{table_name}`.
127 *
128 * Obtain a handle from the [`{accessor_method}`] property on [`RemoteTables`],
129 * like `ctx.db.{accessor_method}`.
130 *
131 * Users are encouraged not to explicitly reference this type,
132 * but to directly chain method calls,
133 * like `ctx.db.{accessor_method}.on_insert(...)`.
134 */
135export class {table_handle} {{
136"
137 );
138 out.indent(1);
139 writeln!(out, "tableCache: TableCache<{row_type}>;");
140 writeln!(out);
141 writeln!(out, "constructor(tableCache: TableCache<{row_type}>) {{");
142 out.with_indent(|out| writeln!(out, "this.tableCache = tableCache;"));
143 writeln!(out, "}}");
144 writeln!(out);
145 writeln!(out, "count(): number {{");
146 out.with_indent(|out| {
147 writeln!(out, "return this.tableCache.count();");
148 });
149 writeln!(out, "}}");
150 writeln!(out);
151 writeln!(out, "iter(): Iterable<{row_type}> {{");
152 out.with_indent(|out| {
153 writeln!(out, "return this.tableCache.iter();");
154 });
155 writeln!(out, "}}");
156
157 for (unique_field_ident, unique_field_type_use) in iter_unique_cols(&schema, product_def) {
158 let unique_field_name = unique_field_ident.deref().to_case(Case::Snake);
159 let unique_field_name_pascalcase = unique_field_name.to_case(Case::Pascal);
160
161 let unique_constraint = table_name_pascalcase.clone() + &unique_field_name_pascalcase + "Unique";
162 let unique_field_type = type_name(module, unique_field_type_use);
163
164 writeln!(
165 out,
166 "/**
167 * Access to the `{unique_field_name}` unique index on the table `{table_name}`,
168 * which allows point queries on the field of the same name
169 * via the [`{unique_constraint}.find`] method.
170 *
171 * Users are encouraged not to explicitly reference this type,
172 * but to directly chain method calls,
173 * like `ctx.db.{accessor_method}.{unique_field_name}().find(...)`.
174 *
175 * Get a handle on the `{unique_field_name}` unique index on the table `{table_name}`.
176 */"
177 );
178 writeln!(out, "{unique_field_name} = {{");
179 out.with_indent(|out| {
180 writeln!(
181 out,
182 "// Find the subscribed row whose `{unique_field_name}` column value is equal to `col_val`,"
183 );
184 writeln!(out, "// if such a row is present in the client cache.");
185 writeln!(
186 out,
187 "find: (col_val: {unique_field_type}): {row_type} | undefined => {{"
188 );
189 out.with_indent(|out| {
190 writeln!(out, "for (let row of this.tableCache.iter()) {{");
191 out.with_indent(|out| {
192 writeln!(out, "if (deepEqual(row.{unique_field_name}, col_val)) {{");
193 out.with_indent(|out| {
194 writeln!(out, "return row;");
195 });
196 writeln!(out, "}}");
197 });
198 writeln!(out, "}}");
199 });
200 writeln!(out, "}},");
201 });
202 writeln!(out, "}};");
203 }
204
205 writeln!(out);
206
207 writeln!(
210 out,
211 "onInsert = (cb: (ctx: EventContext, row: {row_type}) => void) => {{
212{INDENT}return this.tableCache.onInsert(cb);
213}}
214
215removeOnInsert = (cb: (ctx: EventContext, row: {row_type}) => void) => {{
216{INDENT}return this.tableCache.removeOnInsert(cb);
217}}
218
219onDelete = (cb: (ctx: EventContext, row: {row_type}) => void) => {{
220{INDENT}return this.tableCache.onDelete(cb);
221}}
222
223removeOnDelete = (cb: (ctx: EventContext, row: {row_type}) => void) => {{
224{INDENT}return this.tableCache.removeOnDelete(cb);
225}}"
226 );
227
228 if schema.pk().is_some() {
229 write!(
230 out,
231 "
232// Updates are only defined for tables with primary keys.
233onUpdate = (cb: (ctx: EventContext, oldRow: {row_type}, newRow: {row_type}) => void) => {{
234{INDENT}return this.tableCache.onUpdate(cb);
235}}
236
237removeOnUpdate = (cb: (ctx: EventContext, onRow: {row_type}, newRow: {row_type}) => void) => {{
238{INDENT}return this.tableCache.removeOnUpdate(cb);
239}}"
240 );
241 }
242 out.dedent(1);
243
244 writeln!(out, "}}");
245 output.into_inner()
246 }
247
248 fn generate_reducer(&self, module: &ModuleDef, reducer: &ReducerDef) -> String {
249 let mut output = CodeIndenter::new(String::new(), INDENT);
250 let out = &mut output;
251
252 print_file_header(out);
253
254 out.newline();
255
256 gen_and_print_imports(
257 module,
258 out,
259 &reducer.params_for_generate.elements,
260 &[],
262 );
263
264 let args_type = reducer_args_type_name(&reducer.name);
265
266 define_namespace_and_object_type_for_product(module, out, &args_type, &reducer.params_for_generate.elements);
267
268 output.into_inner()
269 }
270
271 fn generate_globals(&self, module: &ModuleDef) -> Vec<(String, String)> {
272 let mut output = CodeIndenter::new(String::new(), INDENT);
273 let out = &mut output;
274
275 print_file_header(out);
276
277 out.newline();
278
279 writeln!(out, "// Import and reexport all reducer arg types");
280 for reducer in iter_reducers(module) {
281 let reducer_name = &reducer.name;
282 let reducer_module_name = reducer_module_name(reducer_name) + ".ts";
283 let args_type = reducer_args_type_name(&reducer.name);
284 writeln!(out, "import {{ {args_type} }} from \"./{reducer_module_name}\";");
285 writeln!(out, "export {{ {args_type} }};");
286 }
287
288 writeln!(out);
289 writeln!(out, "// Import and reexport all table handle types");
290 for table in iter_tables(module) {
291 let table_name = &table.name;
292 let table_module_name = table_module_name(table_name) + ".ts";
293 let table_name_pascalcase = table.name.deref().to_case(Case::Pascal);
294 let table_handle = table_name_pascalcase.clone() + "TableHandle";
295 writeln!(out, "import {{ {table_handle} }} from \"./{table_module_name}\";");
296 writeln!(out, "export {{ {table_handle} }};");
297 }
298
299 writeln!(out);
300 writeln!(out, "// Import and reexport all types");
301 for ty in iter_types(module) {
302 let type_name = collect_case(Case::Pascal, ty.name.name_segments());
303 let type_module_name = type_module_name(&ty.name) + ".ts";
304 writeln!(out, "import {{ {type_name} }} from \"./{type_module_name}\";");
305 writeln!(out, "export {{ {type_name} }};");
306 }
307
308 out.newline();
309
310 writeln!(out, "const REMOTE_MODULE = {{");
312 out.indent(1);
313 writeln!(out, "tables: {{");
314 out.indent(1);
315 for table in iter_tables(module) {
316 let type_ref = table.product_type_ref;
317 let row_type = type_ref_name(module, type_ref);
318 let schema = TableSchema::from_module_def(module, table, (), 0.into())
319 .validated()
320 .expect("Failed to generate table due to validation errors");
321 writeln!(out, "{}: {{", table.name);
322 out.indent(1);
323 writeln!(out, "tableName: \"{}\",", table.name);
324 writeln!(out, "rowType: {row_type}.getTypeScriptAlgebraicType(),");
325 if let Some(pk) = schema.pk() {
326 writeln!(out, "primaryKey: \"{}\",", pk.col_name.to_string().to_case(Case::Camel));
327 }
328 out.dedent(1);
329 writeln!(out, "}},");
330 }
331 out.dedent(1);
332 writeln!(out, "}},");
333 writeln!(out, "reducers: {{");
334 out.indent(1);
335 for reducer in iter_reducers(module) {
336 writeln!(out, "{}: {{", reducer.name);
337 out.indent(1);
338 writeln!(out, "reducerName: \"{}\",", reducer.name);
339 writeln!(
340 out,
341 "argsType: {args_type}.getTypeScriptAlgebraicType(),",
342 args_type = reducer_args_type_name(&reducer.name)
343 );
344 out.dedent(1);
345 writeln!(out, "}},");
346 }
347 out.dedent(1);
348 writeln!(out, "}},");
349 writeln!(
350 out,
351 "// Constructors which are used by the DbConnectionImpl to
352// extract type information from the generated RemoteModule.
353//
354// NOTE: This is not strictly necessary for `eventContextConstructor` because
355// all we do is build a TypeScript object which we could have done inside the
356// SDK, but if in the future we wanted to create a class this would be
357// necessary because classes have methods, so we'll keep it.
358eventContextConstructor: (imp: DbConnectionImpl, event: Event<Reducer>) => {{
359 return {{
360 ...(imp as DbConnection),
361 event
362 }}
363}},
364dbViewConstructor: (imp: DbConnectionImpl) => {{
365 return new RemoteTables(imp);
366}},
367reducersConstructor: (imp: DbConnectionImpl, setReducerFlags: SetReducerFlags) => {{
368 return new RemoteReducers(imp, setReducerFlags);
369}},
370setReducerFlagsConstructor: () => {{
371 return new SetReducerFlags();
372}}"
373 );
374 out.dedent(1);
375 writeln!(out, "}}");
376
377 writeln!(out);
379 print_reducer_enum_defn(module, out);
380
381 out.newline();
382
383 print_remote_reducers(module, out);
384
385 out.newline();
386
387 print_set_reducer_flags(module, out);
388
389 out.newline();
390
391 print_remote_tables(module, out);
392
393 out.newline();
394
395 print_subscription_builder(module, out);
396
397 out.newline();
398
399 print_db_connection(module, out);
400
401 out.newline();
402
403 writeln!(
404 out,
405 "export type EventContext = EventContextInterface<RemoteTables, RemoteReducers, SetReducerFlags, Reducer>;"
406 );
407
408 writeln!(
409 out,
410 "export type ReducerEventContext = ReducerEventContextInterface<RemoteTables, RemoteReducers, SetReducerFlags, Reducer>;"
411 );
412
413 writeln!(
414 out,
415 "export type SubscriptionEventContext = SubscriptionEventContextInterface<RemoteTables, RemoteReducers, SetReducerFlags>;"
416 );
417
418 writeln!(
419 out,
420 "export type ErrorContext = ErrorContextInterface<RemoteTables, RemoteReducers, SetReducerFlags>;"
421 );
422
423 vec![("index.ts".to_string(), (output.into_inner()))]
424 }
425
426 fn clap_value() -> clap::builder::PossibleValue {
427 clap::builder::PossibleValue::new("typescript").aliases(["ts", "TS"])
428 }
429}
430
431fn print_remote_reducers(module: &ModuleDef, out: &mut Indenter) {
432 writeln!(out, "export class RemoteReducers {{");
433 out.indent(1);
434 writeln!(
435 out,
436 "constructor(private connection: DbConnectionImpl, private setCallReducerFlags: SetReducerFlags) {{}}"
437 );
438 out.newline();
439
440 for reducer in iter_reducers(module) {
441 let mut arg_list = "".to_string();
445 let mut arg_name_list = "".to_string();
446 for (arg_ident, arg_ty) in &reducer.params_for_generate.elements[..] {
447 let arg_name = arg_ident.deref().to_case(Case::Camel);
448 arg_name_list += &arg_name;
449 arg_list += &arg_name;
450 arg_list += ": ";
451 write_type(module, &mut arg_list, arg_ty, None).unwrap();
452 arg_list += ", ";
453 arg_name_list += ", ";
454 }
455 let arg_list = arg_list.trim_end_matches(", ");
456 let arg_name_list = arg_name_list.trim_end_matches(", ");
457
458 let reducer_name = &reducer.name;
459
460 if is_reducer_invokable(reducer) {
461 let reducer_function_name = reducer_function_name(reducer);
462 let reducer_variant = reducer_variant_name(&reducer.name);
463 if reducer.params_for_generate.elements.is_empty() {
464 writeln!(out, "{reducer_function_name}() {{");
465 out.with_indent(|out| {
466 writeln!(
467 out,
468 "this.connection.callReducer(\"{reducer_name}\", new Uint8Array(0), this.setCallReducerFlags.{reducer_function_name}Flags);"
469 );
470 });
471 } else {
472 writeln!(out, "{reducer_function_name}({arg_list}) {{");
473 out.with_indent(|out| {
474 writeln!(out, "const __args = {{ {arg_name_list} }};");
475 writeln!(out, "let __writer = new BinaryWriter(1024);");
476 writeln!(
477 out,
478 "{reducer_variant}.getTypeScriptAlgebraicType().serialize(__writer, __args);"
479 );
480 writeln!(out, "let __argsBuffer = __writer.getBuffer();");
481 writeln!(out, "this.connection.callReducer(\"{reducer_name}\", __argsBuffer, this.setCallReducerFlags.{reducer_function_name}Flags);");
482 });
483 }
484 writeln!(out, "}}");
485 out.newline();
486 }
487
488 let arg_list_padded = if arg_list.is_empty() {
489 String::new()
490 } else {
491 format!(", {arg_list}")
492 };
493 let reducer_name_pascal = reducer_name.deref().to_case(Case::Pascal);
494 writeln!(
495 out,
496 "on{reducer_name_pascal}(callback: (ctx: ReducerEventContext{arg_list_padded}) => void) {{"
497 );
498 out.indent(1);
499 writeln!(out, "this.connection.onReducer(\"{reducer_name}\", callback);");
500 out.dedent(1);
501 writeln!(out, "}}");
502 out.newline();
503 writeln!(
504 out,
505 "removeOn{reducer_name_pascal}(callback: (ctx: ReducerEventContext{arg_list_padded}) => void) {{"
506 );
507 out.indent(1);
508 writeln!(out, "this.connection.offReducer(\"{reducer_name}\", callback);");
509 out.dedent(1);
510 writeln!(out, "}}");
511 out.newline();
512 }
513
514 out.dedent(1);
515 writeln!(out, "}}");
516}
517
518fn print_set_reducer_flags(module: &ModuleDef, out: &mut Indenter) {
519 writeln!(out, "export class SetReducerFlags {{");
520 out.indent(1);
521
522 for reducer in iter_reducers(module).filter(|r| is_reducer_invokable(r)) {
523 let reducer_function_name = reducer_function_name(reducer);
524 writeln!(out, "{reducer_function_name}Flags: CallReducerFlags = 'FullUpdate';");
525 writeln!(out, "{reducer_function_name}(flags: CallReducerFlags) {{");
526 out.with_indent(|out| {
527 writeln!(out, "this.{reducer_function_name}Flags = flags;");
528 });
529 writeln!(out, "}}");
530 out.newline();
531 }
532
533 out.dedent(1);
534 writeln!(out, "}}");
535}
536
537fn print_remote_tables(module: &ModuleDef, out: &mut Indenter) {
538 writeln!(out, "export class RemoteTables {{");
539 out.indent(1);
540 writeln!(out, "constructor(private connection: DbConnectionImpl) {{}}");
541
542 for table in iter_tables(module) {
543 writeln!(out);
544 let table_name = table.name.deref();
545 let table_name_pascalcase = table.name.deref().to_case(Case::Pascal);
546 let table_name_camelcase = table.name.deref().to_case(Case::Camel);
547 let table_handle = table_name_pascalcase.clone() + "TableHandle";
548 let type_ref = table.product_type_ref;
549 let row_type = type_ref_name(module, type_ref);
550 writeln!(out, "get {table_name_camelcase}(): {table_handle} {{");
551 out.with_indent(|out| {
552 writeln!(
553 out,
554 "return new {table_handle}(this.connection.clientCache.getOrCreateTable<{row_type}>(REMOTE_MODULE.tables.{table_name}));"
555 );
556 });
557 writeln!(out, "}}");
558 }
559
560 out.dedent(1);
561 writeln!(out, "}}");
562}
563
564fn print_subscription_builder(_module: &ModuleDef, out: &mut Indenter) {
565 writeln!(
566 out,
567 "export class SubscriptionBuilder extends SubscriptionBuilderImpl<RemoteTables, RemoteReducers, SetReducerFlags> {{ }}"
568 );
569}
570
571fn print_db_connection(_module: &ModuleDef, out: &mut Indenter) {
572 writeln!(
573 out,
574 "export class DbConnection extends DbConnectionImpl<RemoteTables, RemoteReducers, SetReducerFlags> {{"
575 );
576 out.indent(1);
577 writeln!(
578 out,
579 "static builder = (): DbConnectionBuilder<DbConnection, ErrorContext, SubscriptionEventContext> => {{"
580 );
581 out.indent(1);
582 writeln!(
583 out,
584 "return new DbConnectionBuilder<DbConnection, ErrorContext, SubscriptionEventContext>(REMOTE_MODULE, (imp: DbConnectionImpl) => imp as DbConnection);"
585 );
586 out.dedent(1);
587 writeln!(out, "}}");
588 writeln!(out, "subscriptionBuilder = (): SubscriptionBuilder => {{");
589 out.indent(1);
590 writeln!(out, "return new SubscriptionBuilder(this);");
591 out.dedent(1);
592 writeln!(out, "}}");
593 out.dedent(1);
594 writeln!(out, "}}");
595}
596
597fn print_reducer_enum_defn(module: &ModuleDef, out: &mut Indenter) {
598 writeln!(out, "// A type representing all the possible variants of a reducer.");
599 writeln!(out, "export type Reducer = never");
600 for reducer in iter_reducers(module) {
601 writeln!(
602 out,
603 "| {{ name: \"{}\", args: {} }}",
604 reducer_variant_name(&reducer.name),
605 reducer_args_type_name(&reducer.name)
606 );
607 }
608 writeln!(out, ";");
609}
610
611fn print_spacetimedb_imports(out: &mut Indenter) {
612 let mut types = [
613 "AlgebraicType",
614 "ProductType",
615 "ProductTypeElement",
616 "SumType",
617 "SumTypeVariant",
618 "AlgebraicValue",
619 "Identity",
620 "ConnectionId",
621 "Timestamp",
622 "TimeDuration",
623 "DbConnectionBuilder",
624 "TableCache",
625 "BinaryWriter",
626 "CallReducerFlags",
627 "EventContextInterface",
628 "ReducerEventContextInterface",
629 "SubscriptionEventContextInterface",
630 "ErrorContextInterface",
631 "SubscriptionBuilderImpl",
632 "BinaryReader",
633 "DbConnectionImpl",
634 "DbContext",
635 "Event",
636 "deepEqual",
637 ];
638 types.sort();
639 writeln!(out, "import {{");
640 out.indent(1);
641 for ty in &types {
642 writeln!(out, "{ty},");
643 }
644 out.dedent(1);
645 writeln!(out, "}} from \"@clockworklabs/spacetimedb-sdk\";");
646}
647
648fn print_file_header(output: &mut Indenter) {
649 print_auto_generated_file_comment(output);
650 print_lint_suppression(output);
651 print_spacetimedb_imports(output);
652}
653
654fn print_lint_suppression(output: &mut Indenter) {
655 writeln!(output, "/* eslint-disable */");
656 writeln!(output, "/* tslint:disable */");
657 writeln!(output, "// @ts-nocheck");
658}
659
660fn write_get_algebraic_type_for_product(
661 module: &ModuleDef,
662 out: &mut Indenter,
663 elements: &[(Identifier, AlgebraicTypeUse)],
664) {
665 writeln!(
666 out,
667 "/**
668* A function which returns this type represented as an AlgebraicType.
669* This function is derived from the AlgebraicType used to generate this type.
670*/"
671 );
672 writeln!(out, "export function getTypeScriptAlgebraicType(): AlgebraicType {{");
673 {
674 out.indent(1);
675 write!(out, "return ");
676 convert_product_type(module, out, elements, "__");
677 writeln!(out, ";");
678 out.dedent(1);
679 }
680 writeln!(out, "}}");
681}
682
683fn define_namespace_and_object_type_for_product(
684 module: &ModuleDef,
685 out: &mut Indenter,
686 name: &str,
687 elements: &[(Identifier, AlgebraicTypeUse)],
688) {
689 write!(out, "export type {name} = {{");
690 if elements.is_empty() {
691 writeln!(out, "}};");
692 } else {
693 writeln!(out);
694 out.with_indent(|out| write_arglist_no_delimiters(module, out, elements, None, true).unwrap());
695 writeln!(out, "}};");
696 }
697
698 out.newline();
699
700 writeln!(
701 out,
702 "/**
703 * A namespace for generated helper functions.
704 */"
705 );
706 writeln!(out, "export namespace {name} {{");
707 out.indent(1);
708 write_get_algebraic_type_for_product(module, out, elements);
709 writeln!(out);
710
711 writeln!(
712 out,
713 "export function serialize(writer: BinaryWriter, value: {name}): void {{"
714 );
715 out.indent(1);
716 writeln!(out, "{name}.getTypeScriptAlgebraicType().serialize(writer, value);");
717 out.dedent(1);
718 writeln!(out, "}}");
719 writeln!(out);
720
721 writeln!(out, "export function deserialize(reader: BinaryReader): {name} {{");
722 out.indent(1);
723 writeln!(out, "return {name}.getTypeScriptAlgebraicType().deserialize(reader);");
724 out.dedent(1);
725 writeln!(out, "}}");
726 writeln!(out);
727
728 out.dedent(1);
729 writeln!(out, "}}");
730
731 out.newline();
732}
733
734fn write_arglist_no_delimiters(
735 module: &ModuleDef,
736 out: &mut impl Write,
737 elements: &[(Identifier, AlgebraicTypeUse)],
738 prefix: Option<&str>,
739 convert_case: bool,
740) -> anyhow::Result<()> {
741 for (ident, ty) in elements {
742 if let Some(prefix) = prefix {
743 write!(out, "{prefix} ")?;
744 }
745
746 let name = if convert_case {
747 ident.deref().to_case(Case::Camel)
748 } else {
749 ident.deref().into()
750 };
751
752 write!(out, "{name}: ")?;
753 write_type(module, out, ty, Some("__"))?;
754 writeln!(out, ",")?;
755 }
756
757 Ok(())
758}
759
760fn write_sum_variant_type(module: &ModuleDef, out: &mut Indenter, ident: &Identifier, ty: &AlgebraicTypeUse) {
761 let name = ident.deref().to_case(Case::Pascal);
762 write!(out, "export type {name} = ");
763
764 write!(out, "{{ ");
770 write!(out, "tag: \"{name}\"");
771
772 if !matches!(ty, AlgebraicTypeUse::Unit) {
791 write!(out, ", value: ");
792 write_type(module, out, ty, Some("__")).unwrap();
793 }
794
795 writeln!(out, " }};");
796}
797
798fn write_variant_types(module: &ModuleDef, out: &mut Indenter, variants: &[(Identifier, AlgebraicTypeUse)]) {
799 for (ident, ty) in variants {
801 write_sum_variant_type(module, out, ident, ty);
802 }
803}
804
805fn write_variant_constructors(
806 module: &ModuleDef,
807 out: &mut Indenter,
808 name: &str,
809 variants: &[(Identifier, AlgebraicTypeUse)],
810) {
811 for (ident, ty) in variants {
814 if matches!(ty, AlgebraicTypeUse::Unit) {
815 write!(out, "export const {ident} = ");
820 writeln!(out, "{{ tag: \"{ident}\" }};");
821 continue;
822 }
823 let variant_name = ident.deref().to_case(Case::Pascal);
824 write!(out, "export const {variant_name} = (value: ");
825 write_type(module, out, ty, Some("__")).unwrap();
826 writeln!(out, "): {name} => ({{ tag: \"{variant_name}\", value }});");
827 }
828}
829
830fn write_get_algebraic_type_for_sum(
831 module: &ModuleDef,
832 out: &mut Indenter,
833 variants: &[(Identifier, AlgebraicTypeUse)],
834) {
835 writeln!(out, "export function getTypeScriptAlgebraicType(): AlgebraicType {{");
836 {
837 indent_scope!(out);
838 write!(out, "return ");
839 convert_sum_type(module, &mut out, variants, "__");
840 writeln!(out, ";");
841 }
842 writeln!(out, "}}");
843}
844
845fn define_namespace_and_types_for_sum(
846 module: &ModuleDef,
847 out: &mut Indenter,
848 name: &str,
849 variants: &[(Identifier, AlgebraicTypeUse)],
850) {
851 writeln!(out, "// A namespace for generated variants and helper functions.");
852 writeln!(out, "export namespace {name} {{");
853 out.indent(1);
854
855 writeln!(
857 out,
858 "// These are the generated variant types for each variant of the tagged union.
859// One type is generated per variant and will be used in the `value` field of
860// the tagged union."
861 );
862 write_variant_types(module, out, variants);
863 writeln!(out);
864
865 writeln!(
867 out,
868 "// Helper functions for constructing each variant of the tagged union.
869// ```
870// const foo = Foo.A(42);
871// assert!(foo.tag === \"A\");
872// assert!(foo.value === 42);
873// ```"
874 );
875 write_variant_constructors(module, out, name, variants);
876 writeln!(out);
877
878 write_get_algebraic_type_for_sum(module, out, variants);
880 writeln!(out);
881
882 writeln!(
883 out,
884 "export function serialize(writer: BinaryWriter, value: {name}): void {{
885 {name}.getTypeScriptAlgebraicType().serialize(writer, value);
886}}"
887 );
888 writeln!(out);
889
890 writeln!(
891 out,
892 "export function deserialize(reader: BinaryReader): {name} {{
893 return {name}.getTypeScriptAlgebraicType().deserialize(reader);
894}}"
895 );
896 writeln!(out);
897
898 out.dedent(1);
899
900 writeln!(out, "}}");
901 out.newline();
902
903 writeln!(out, "// The tagged union or sum type for the algebraic type `{name}`.");
904 write!(out, "export type {name} = ");
905
906 let names = variants
907 .iter()
908 .map(|(ident, _)| format!("{name}.{}", ident.deref().to_case(Case::Pascal)))
909 .collect::<Vec<String>>()
910 .join(" | ");
911
912 writeln!(out, "{names};");
913 out.newline();
914
915 writeln!(out, "export default {name};");
916}
917
918fn type_ref_module_name(module: &ModuleDef, type_ref: AlgebraicTypeRef) -> String {
919 let (name, _) = module.type_def_from_ref(type_ref).unwrap();
920 type_module_name(name)
921}
922
923fn type_module_name(type_name: &ScopedTypeName) -> String {
924 collect_case(Case::Snake, type_name.name_segments()) + "_type"
925}
926
927fn table_module_name(table_name: &Identifier) -> String {
928 table_name.deref().to_case(Case::Snake) + "_table"
929}
930
931fn table_method_name(table_name: &Identifier) -> String {
932 table_name.deref().to_case(Case::Camel)
933}
934
935fn reducer_args_type_name(reducer_name: &Identifier) -> String {
936 reducer_name.deref().to_case(Case::Pascal)
937}
938
939fn reducer_variant_name(reducer_name: &Identifier) -> String {
940 reducer_name.deref().to_case(Case::Pascal)
941}
942
943fn reducer_module_name(reducer_name: &Identifier) -> String {
944 reducer_name.deref().to_case(Case::Snake) + "_reducer"
945}
946
947fn reducer_function_name(reducer: &ReducerDef) -> String {
948 reducer.name.deref().to_case(Case::Camel)
949}
950
951pub fn type_name(module: &ModuleDef, ty: &AlgebraicTypeUse) -> String {
952 let mut s = String::new();
953 write_type(module, &mut s, ty, None).unwrap();
954 s
955}
956
957pub fn write_type<W: Write>(
958 module: &ModuleDef,
959 out: &mut W,
960 ty: &AlgebraicTypeUse,
961 ref_prefix: Option<&str>,
962) -> fmt::Result {
963 match ty {
964 AlgebraicTypeUse::Unit => write!(out, "void")?,
965 AlgebraicTypeUse::Never => write!(out, "never")?,
966 AlgebraicTypeUse::Identity => write!(out, "Identity")?,
967 AlgebraicTypeUse::ConnectionId => write!(out, "ConnectionId")?,
968 AlgebraicTypeUse::Timestamp => write!(out, "Timestamp")?,
969 AlgebraicTypeUse::TimeDuration => write!(out, "TimeDuration")?,
970 AlgebraicTypeUse::ScheduleAt => write!(
971 out,
972 "{{ tag: \"Interval\", value: TimeDuration }} | {{ tag: \"Time\", value: Timestamp }}"
973 )?,
974 AlgebraicTypeUse::Option(inner_ty) => {
975 write_type(module, out, inner_ty, ref_prefix)?;
976 write!(out, " | undefined")?;
977 }
978 AlgebraicTypeUse::Primitive(prim) => match prim {
979 PrimitiveType::Bool => write!(out, "boolean")?,
980 PrimitiveType::I8 => write!(out, "number")?,
981 PrimitiveType::U8 => write!(out, "number")?,
982 PrimitiveType::I16 => write!(out, "number")?,
983 PrimitiveType::U16 => write!(out, "number")?,
984 PrimitiveType::I32 => write!(out, "number")?,
985 PrimitiveType::U32 => write!(out, "number")?,
986 PrimitiveType::I64 => write!(out, "bigint")?,
987 PrimitiveType::U64 => write!(out, "bigint")?,
988 PrimitiveType::I128 => write!(out, "bigint")?,
989 PrimitiveType::U128 => write!(out, "bigint")?,
990 PrimitiveType::I256 => write!(out, "bigint")?,
991 PrimitiveType::U256 => write!(out, "bigint")?,
992 PrimitiveType::F32 => write!(out, "number")?,
993 PrimitiveType::F64 => write!(out, "number")?,
994 },
995 AlgebraicTypeUse::String => write!(out, "string")?,
996 AlgebraicTypeUse::Array(elem_ty) => {
997 if matches!(&**elem_ty, AlgebraicTypeUse::Primitive(PrimitiveType::U8)) {
998 return write!(out, "Uint8Array");
999 }
1000 write_type(module, out, elem_ty, ref_prefix)?;
1001 write!(out, "[]")?;
1002 }
1003 AlgebraicTypeUse::Ref(r) => {
1004 if let Some(prefix) = ref_prefix {
1005 write!(out, "{prefix}")?;
1006 }
1007 write!(out, "{}", type_ref_name(module, *r))?;
1008 }
1009 }
1010 Ok(())
1011}
1012
1013fn convert_algebraic_type<'a>(
1014 module: &'a ModuleDef,
1015 out: &mut Indenter,
1016 ty: &'a AlgebraicTypeUse,
1017 ref_prefix: &'a str,
1018) {
1019 match ty {
1020 AlgebraicTypeUse::ScheduleAt => write!(out, "AlgebraicType.createScheduleAtType()"),
1021 AlgebraicTypeUse::Identity => write!(out, "AlgebraicType.createIdentityType()"),
1022 AlgebraicTypeUse::ConnectionId => write!(out, "AlgebraicType.createConnectionIdType()"),
1023 AlgebraicTypeUse::Timestamp => write!(out, "AlgebraicType.createTimestampType()"),
1024 AlgebraicTypeUse::TimeDuration => write!(out, "AlgebraicType.createTimeDurationType()"),
1025 AlgebraicTypeUse::Option(inner_ty) => {
1026 write!(out, "AlgebraicType.createOptionType(");
1027 convert_algebraic_type(module, out, inner_ty, ref_prefix);
1028 write!(out, ")");
1029 }
1030 AlgebraicTypeUse::Array(ty) => {
1031 write!(out, "AlgebraicType.createArrayType(");
1032 convert_algebraic_type(module, out, ty, ref_prefix);
1033 write!(out, ")");
1034 }
1035 AlgebraicTypeUse::Ref(r) => write!(
1036 out,
1037 "{ref_prefix}{}.getTypeScriptAlgebraicType()",
1038 type_ref_name(module, *r)
1039 ),
1040 AlgebraicTypeUse::Primitive(prim) => {
1041 write!(out, "AlgebraicType.create{prim:?}Type()");
1042 }
1043 AlgebraicTypeUse::Unit => write!(out, "AlgebraicType.createProductType([])"),
1044 AlgebraicTypeUse::Never => unimplemented!(),
1045 AlgebraicTypeUse::String => write!(out, "AlgebraicType.createStringType()"),
1046 }
1047}
1048
1049fn convert_sum_type<'a>(
1050 module: &'a ModuleDef,
1051 out: &mut Indenter,
1052 variants: &'a [(Identifier, AlgebraicTypeUse)],
1053 ref_prefix: &'a str,
1054) {
1055 writeln!(out, "AlgebraicType.createSumType([");
1056 out.indent(1);
1057 for (ident, ty) in variants {
1058 write!(out, "new SumTypeVariant(\"{ident}\", ",);
1059 convert_algebraic_type(module, out, ty, ref_prefix);
1060 writeln!(out, "),");
1061 }
1062 out.dedent(1);
1063 write!(out, "])")
1064}
1065
1066fn convert_product_type<'a>(
1067 module: &'a ModuleDef,
1068 out: &mut Indenter,
1069 elements: &'a [(Identifier, AlgebraicTypeUse)],
1070 ref_prefix: &'a str,
1071) {
1072 writeln!(out, "AlgebraicType.createProductType([");
1073 out.indent(1);
1074 for (ident, ty) in elements {
1075 write!(
1076 out,
1077 "new ProductTypeElement(\"{}\", ",
1078 ident.deref().to_case(Case::Camel)
1079 );
1080 convert_algebraic_type(module, out, ty, ref_prefix);
1081 writeln!(out, "),");
1082 }
1083 out.dedent(1);
1084 write!(out, "])")
1085}
1086
1087fn print_imports(module: &ModuleDef, out: &mut Indenter, imports: Imports) {
1089 for typeref in imports {
1090 let module_name = type_ref_module_name(module, typeref);
1091 let type_name = type_ref_name(module, typeref);
1092 writeln!(
1093 out,
1094 "import {{ {type_name} as __{type_name} }} from \"./{module_name}\";"
1095 );
1096 }
1097}
1098
1099fn gen_and_print_imports(
1105 module: &ModuleDef,
1106 out: &mut Indenter,
1107 roots: &[(Identifier, AlgebraicTypeUse)],
1108 dont_import: &[AlgebraicTypeRef],
1109) {
1110 let mut imports = BTreeSet::new();
1111
1112 for (_, ty) in roots {
1113 ty.for_each_ref(|r| {
1114 imports.insert(r);
1115 });
1116 }
1117 for skip in dont_import {
1118 imports.remove(skip);
1119 }
1120 let len = imports.len();
1121
1122 print_imports(module, out, imports);
1123
1124 if len > 0 {
1125 out.newline();
1126 }
1127}
1128
1129