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