spacetimedb_codegen/
typescript.rs

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        // TODO(cloutiertyler): I do think TypeScript does support namespaces:
47        // https://www.typescriptlang.org/docs/handbook/namespaces.html
48        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        // Import the types of all fields.
98        // We only need to import fields which have indices or unique constraints,
99        // but it's easier to just import all of 'em, since we have `// @ts-nocheck` anyway.
100        gen_and_print_imports(
101            module,
102            out,
103            &product_def.elements,
104            &[], // No need to skip any imports; we're not defining a type, so there's no chance of circular imports.
105        );
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        // TODO: expose non-unique indices.
206
207        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            // No need to skip any imports; we're not emitting a type that other modules can import.
259            &[],
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        // Define SpacetimeModule
309        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                // This is left here so we can release the codegen change before releasing a new
325                // version of the SDK.
326                //
327                // Eventually we can remove this and only generate use the `primaryKeyInfo` field.
328                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        // Define `type Reducer` enum.
396        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        // The reducer argument names and types as `ident: ty, ident: ty, ident: ty`,
456        // and the argument names as `ident, ident, ident`
457        // for passing to function call and struct literal expressions.
458        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    // If the contained type is the unit type, i.e. this variant has no members,
779    // write only the tag.
780    // ```
781    // { tag: "Foo" }
782    // ```
783    write!(out, "{{ ");
784    write!(out, "tag: \"{name}\"");
785
786    // If the contained type is not the unit type, write the tag and the value.
787    // ```
788    // { tag: "Bar", value: Bar }
789    // { tag: "Bar", value: number }
790    // { tag: "Bar", value: string }
791    // ```
792    // Note you could alternatively do:
793    // ```
794    // { tag: "Bar" } & Bar
795    // ```
796    // for non-primitive types but that doesn't extend to primitives.
797    // Another alternative would be to name the value field the same as the tag field, but lowercased
798    // ```
799    // { tag: "Bar", bar: Bar }
800    // { tag: "Bar", bar: number }
801    // { tag: "Bar", bar: string }
802    // ```
803    // but this is a departure from our previous convention and is not much different.
804    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    // Write all the variant types.
814    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    // Write all the variant constructors.
826    // Write all of the variant constructors.
827    for (ident, ty) in variants {
828        if matches!(ty, AlgebraicTypeUse::Unit) {
829            // If the variant has no members, we can export a simple object.
830            // ```
831            // export const Foo = { tag: "Foo" };
832            // ```
833            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    // Write all of the variant types.
870    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    // Write all of the variant constructors.
880    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 the function that generates the algebraic type.
893    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
971// This should return true if we should wrap the type in parentheses when it is the element type of
972// an array. This is needed if the type has a `|` in it, e.g. `Option<T>` or `Foo | Bar`, since
973// without parens, `Foo | Bar[]` would be parsed as `Foo | (Bar[])`.
974fn 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(_) // We use the type name for these.
985        | 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            // We wrap the inner type in parentheses to avoid ambiguity with the [] binding.
1039            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
1132/// Print imports for each of the `imports`.
1133fn 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
1144/// Use `search_function` on `roots` to detect required imports, then print them with `print_imports`.
1145///
1146/// `this_file` is passed and excluded for the case of recursive types:
1147/// without it, the definition for a type like `struct Foo { foos: Vec<Foo> }`
1148/// would attempt to include `import { Foo } from "./foo"`.
1149fn 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// const RESERVED_KEYWORDS: [&str; 36] = [
1175//     "break",
1176//     "case",
1177//     "catch",
1178//     "class",
1179//     "const",
1180//     "continue",
1181//     "debugger",
1182//     "default",
1183//     "delete",
1184//     "do",
1185//     "else",
1186//     "enum",
1187//     "export",
1188//     "extends",
1189//     "false",
1190//     "finally",
1191//     "for",
1192//     "function",
1193//     "if",
1194//     "import",
1195//     "in",
1196//     "instanceof",
1197//     "new",
1198//     "null",
1199//     "return",
1200//     "super",
1201//     "switch",
1202//     "this",
1203//     "throw",
1204//     "true",
1205//     "try",
1206//     "typeof",
1207//     "var",
1208//     "void",
1209//     "while",
1210//     "with",
1211// ];
1212
1213// fn typescript_field_name(field_name: String) -> String {
1214//     if RESERVED_KEYWORDS
1215//         .into_iter()
1216//         .map(String::from)
1217//         .collect::<Vec<String>>()
1218//         .contains(&field_name)
1219//     {
1220//         return format!("_{field_name}");
1221//     }
1222
1223//     field_name
1224// }