spacetimedb_cli/subcommands/generate/
typescript.rs

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        // TODO: implement formatting.
46        Ok(())
47    }
48
49    fn generate_type(&self, module: &ModuleDef, typ: &TypeDef) -> String {
50        // TODO(cloutiertyler): I do think TypeScript does support namespaces:
51        // https://www.typescriptlang.org/docs/handbook/namespaces.html
52        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        // Import the types of all fields.
102        // We only need to import fields which have indices or unique constraints,
103        // but it's easier to just import all of 'em, since we have `// @ts-nocheck` anyway.
104        gen_and_print_imports(
105            module,
106            out,
107            &product_def.elements,
108            &[], // No need to skip any imports; we're not defining a type, so there's no chance of circular imports.
109        );
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        // TODO: expose non-unique indices.
208
209        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            // No need to skip any imports; we're not emitting a type that other modules can import.
261            &[],
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        // Define SpacetimeModule
311        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        // Define `type Reducer` enum.
378        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        // The reducer argument names and types as `ident: ty, ident: ty, ident: ty`,
442        // and the argument names as `ident, ident, ident`
443        // for passing to function call and struct literal expressions.
444        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    // If the contained type is the unit type, i.e. this variant has no members,
765    // write only the tag.
766    // ```
767    // { tag: "Foo" }
768    // ```
769    write!(out, "{{ ");
770    write!(out, "tag: \"{name}\"");
771
772    // If the contained type is not the unit type, write the tag and the value.
773    // ```
774    // { tag: "Bar", value: Bar }
775    // { tag: "Bar", value: number }
776    // { tag: "Bar", value: string }
777    // ```
778    // Note you could alternatively do:
779    // ```
780    // { tag: "Bar" } & Bar
781    // ```
782    // for non-primitive types but that doesn't extend to primitives.
783    // Another alternative would be to name the value field the same as the tag field, but lowercased
784    // ```
785    // { tag: "Bar", bar: Bar }
786    // { tag: "Bar", bar: number }
787    // { tag: "Bar", bar: string }
788    // ```
789    // but this is a departure from our previous convention and is not much different.
790    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    // Write all the variant types.
800    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    // Write all the variant constructors.
812    // Write all of the variant constructors.
813    for (ident, ty) in variants {
814        if matches!(ty, AlgebraicTypeUse::Unit) {
815            // If the variant has no members, we can export a simple object.
816            // ```
817            // export const Foo = { tag: "Foo" };
818            // ```
819            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    // Write all of the variant types.
856    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    // Write all of the variant constructors.
866    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 the function that generates the algebraic type.
879    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
1087/// Print imports for each of the `imports`.
1088fn 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
1099/// Use `search_function` on `roots` to detect required imports, then print them with `print_imports`.
1100///
1101/// `this_file` is passed and excluded for the case of recursive types:
1102/// without it, the definition for a type like `struct Foo { foos: Vec<Foo> }`
1103/// would attempt to include `import { Foo } from "./foo"`.
1104fn 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// const RESERVED_KEYWORDS: [&str; 36] = [
1130//     "break",
1131//     "case",
1132//     "catch",
1133//     "class",
1134//     "const",
1135//     "continue",
1136//     "debugger",
1137//     "default",
1138//     "delete",
1139//     "do",
1140//     "else",
1141//     "enum",
1142//     "export",
1143//     "extends",
1144//     "false",
1145//     "finally",
1146//     "for",
1147//     "function",
1148//     "if",
1149//     "import",
1150//     "in",
1151//     "instanceof",
1152//     "new",
1153//     "null",
1154//     "return",
1155//     "super",
1156//     "switch",
1157//     "this",
1158//     "throw",
1159//     "true",
1160//     "try",
1161//     "typeof",
1162//     "var",
1163//     "void",
1164//     "while",
1165//     "with",
1166// ];
1167
1168// fn typescript_field_name(field_name: String) -> String {
1169//     if RESERVED_KEYWORDS
1170//         .into_iter()
1171//         .map(String::from)
1172//         .collect::<Vec<String>>()
1173//         .contains(&field_name)
1174//     {
1175//         return format!("_{field_name}");
1176//     }
1177
1178//     field_name
1179// }