spacetimedb_codegen/
util.rs

1//! Various utility functions that the generate modules have in common.
2
3use std::{
4    fmt::{Display, Formatter, Result},
5    ops::Deref,
6};
7
8use super::code_indenter::Indenter;
9use convert_case::{Case, Casing};
10use itertools::Itertools;
11use spacetimedb_lib::sats::layout::PrimitiveType;
12use spacetimedb_lib::version;
13use spacetimedb_lib::{db::raw_def::v9::Lifecycle, sats::AlgebraicTypeRef};
14use spacetimedb_primitives::ColList;
15use spacetimedb_schema::schema::TableSchema;
16use spacetimedb_schema::type_for_generate::ProductTypeDef;
17use spacetimedb_schema::{
18    def::{IndexDef, TableDef, TypeDef},
19    type_for_generate::TypespaceForGenerate,
20};
21use spacetimedb_schema::{
22    def::{ModuleDef, ReducerDef},
23    identifier::Identifier,
24    type_for_generate::AlgebraicTypeUse,
25};
26
27/// Turns a closure `f: Fn(&mut Formatter) -> Result` into `fmt::Display`.
28pub(super) fn fmt_fn(f: impl Fn(&mut Formatter) -> Result) -> impl Display {
29    struct FDisplay<F>(F);
30    impl<F: Fn(&mut Formatter) -> Result> Display for FDisplay<F> {
31        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
32            (self.0)(f)
33        }
34    }
35    FDisplay(f)
36}
37
38pub(super) fn collect_case<'a>(case: Case, segs: impl Iterator<Item = &'a Identifier>) -> String {
39    segs.map(|s| s.deref().to_case(case)).join(case.delim())
40}
41
42pub(super) fn print_lines(output: &mut Indenter, lines: &[&str]) {
43    for line in lines {
44        writeln!(output, "{line}");
45    }
46}
47
48pub const AUTO_GENERATED_PREFIX: &str = "// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB";
49
50const AUTO_GENERATED_FILE_COMMENT: &[&str] = &[
51    "// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE",
52    "// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.",
53    "",
54];
55
56pub(super) fn print_auto_generated_file_comment(output: &mut Indenter) {
57    print_lines(output, AUTO_GENERATED_FILE_COMMENT);
58    writeln!(
59        output,
60        "// This was generated using spacetimedb cli version {} (commit {}).",
61        version::spacetimedb_lib_version(),
62        version::GIT_HASH
63    );
64    writeln!(output);
65}
66
67pub(super) fn type_ref_name(module: &ModuleDef, typeref: AlgebraicTypeRef) -> String {
68    let (name, _def) = module.type_def_from_ref(typeref).unwrap();
69    collect_case(Case::Pascal, name.name_segments())
70}
71
72pub(super) fn is_type_filterable(typespace: &TypespaceForGenerate, ty: &AlgebraicTypeUse) -> bool {
73    match ty {
74        AlgebraicTypeUse::Primitive(prim) => !matches!(prim, PrimitiveType::F32 | PrimitiveType::F64),
75        AlgebraicTypeUse::String | AlgebraicTypeUse::Identity | AlgebraicTypeUse::ConnectionId => true,
76        // Sum types with all unit variants:
77        AlgebraicTypeUse::Never => true,
78        AlgebraicTypeUse::Option(inner) => matches!(&**inner, AlgebraicTypeUse::Unit),
79        AlgebraicTypeUse::Ref(r) => typespace[r].is_plain_enum(),
80        _ => false,
81    }
82}
83
84pub(super) fn is_reducer_invokable(reducer: &ReducerDef) -> bool {
85    reducer.lifecycle.is_none()
86}
87
88/// Iterate over all the [`ReducerDef`]s defined by the module, in alphabetical order by name.
89///
90/// The init reducer is skipped because it should never be visible to the clients.
91/// Sorting is not necessary for reducers because they are already stored in an IndexMap.
92pub(super) fn iter_reducers(module: &ModuleDef) -> impl Iterator<Item = &ReducerDef> {
93    module
94        .reducers()
95        .filter(|reducer| reducer.lifecycle != Some(Lifecycle::Init))
96}
97
98/// Iterate over all the [`TableDef`]s defined by the module, in alphabetical order by name.
99///
100/// Sorting is necessary to have deterministic reproducible codegen.
101pub(super) fn iter_tables(module: &ModuleDef) -> impl Iterator<Item = &TableDef> {
102    module.tables().sorted_by_key(|table| &table.name)
103}
104
105pub(super) fn iter_unique_cols<'a>(
106    typespace: &'a TypespaceForGenerate,
107    schema: &'a TableSchema,
108    product_def: &'a ProductTypeDef,
109) -> impl Iterator<Item = &'a (Identifier, AlgebraicTypeUse)> + 'a {
110    let constraints = schema.backcompat_column_constraints();
111    schema.columns().iter().filter_map(move |field| {
112        constraints[&ColList::from(field.col_pos)]
113            .has_unique()
114            .then(|| {
115                let res @ (_, ref ty) = &product_def.elements[field.col_pos.idx()];
116                is_type_filterable(typespace, ty).then_some(res)
117            })
118            .flatten()
119    })
120}
121
122pub(super) fn iter_indexes(table: &TableDef) -> impl Iterator<Item = &IndexDef> {
123    table.indexes.values().sorted_by_key(|index| &index.name)
124}
125
126/// Iterate over all the [`TypeDef`]s defined by the module, in alphabetical order by name.
127///
128/// Sorting is necessary to have deterministic reproducible codegen.
129pub fn iter_types(module: &ModuleDef) -> impl Iterator<Item = &TypeDef> {
130    module.types().sorted_by_key(|table| &table.name)
131}