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