Skip to main content

nautilus_codegen/
backend.rs

1//! Language-backend abstraction for Nautilus code generation.
2//!
3//! Concrete backends live in their language-specific modules (for example
4//! `crate::python::backend` and `crate::js::backend`). This module keeps only
5//! the shared trait, data structures, and default logic used by all language
6//! backends.
7
8use nautilus_schema::ir::{
9    DefaultValue, EnumIr, FieldIr, FunctionCall, ResolvedFieldType, ScalarType,
10};
11use serde::Serialize;
12use std::collections::HashMap;
13
14/// A filter operator entry produced by a [`LanguageBackend`].
15///
16/// The `type_name` field holds the target-language type as a string (e.g.
17/// `"str"`, `"List[int]"`, `"string[]"`).  Generator code converts this into a
18/// template-context struct whose field may be named differently
19/// (`python_type`, `ts_type`, …).
20#[derive(Debug, Clone, Serialize)]
21pub struct FilterOperator {
22    pub suffix: String,
23    pub type_name: String,
24}
25
26/// Common interface for language-specific code generation backends.
27///
28/// ## Abstract methods
29/// Backends must implement the four type-mapping primitives and the four
30/// operator-naming conventions.
31///
32/// ## Default methods
33/// Everything else — `is_auto_generated`, the numeric-operator helper, and the
34/// full filter-operator builders — is provided as a default implementation that
35/// composes the abstract methods.  Backends only override these defaults when
36/// their language genuinely diverges from the shared logic.
37pub trait LanguageBackend {
38    /// Maps a Nautilus scalar type to the target language's type name.
39    fn scalar_to_type(&self, scalar: &ScalarType) -> &'static str;
40
41    /// Wraps a type name in the language's array/list syntax.
42    ///
43    /// Examples: `"List[T]"` (Python) vs `"T[]"` (TypeScript).
44    fn array_type(&self, inner: &str) -> String;
45
46    /// Suffix for the "not in collection" operator.
47    ///
48    /// Python: `"not_in"` — TypeScript: `"notIn"`
49    fn not_in_suffix(&self) -> &'static str;
50
51    /// Suffix for the "starts with" string operator.
52    ///
53    /// Python: `"startswith"` — TypeScript: `"startsWith"`
54    fn startswith_suffix(&self) -> &'static str;
55
56    /// Suffix for the "ends with" string operator.
57    ///
58    /// Python: `"endswith"` — TypeScript: `"endsWith"`
59    fn endswith_suffix(&self) -> &'static str;
60
61    /// Suffix for the null-check operator.
62    ///
63    /// Python: `"is_null"` — TypeScript: `"isNull"`
64    fn null_suffix(&self) -> &'static str;
65
66    /// Returns `true` for fields whose values are supplied automatically by the
67    /// database: `autoincrement()`, `uuid()`, or `now()`.
68    ///
69    /// This implementation is identical for Python and TypeScript.  The Rust
70    /// backend intentionally differs (it exposes `now()` fields as writable),
71    /// which is why it lives in `type_helpers.rs` and does not use this trait.
72    fn is_auto_generated(&self, field: &FieldIr) -> bool {
73        if field.computed.is_some() {
74            return true;
75        }
76        if let Some(default) = &field.default_value {
77            matches!(
78                default,
79                DefaultValue::Function(f)
80                    if f.name == "autoincrement" || f.name == "uuid" || f.name == "now"
81            )
82        } else {
83            false
84        }
85    }
86
87    /// Returns the standard comparison operators (`lt`, `lte`, `gt`, `gte`,
88    /// `in`, `not_in`/`notIn`) for a numeric-like type.
89    fn numeric_operators(&self, type_name: &str) -> Vec<FilterOperator> {
90        let arr = self.array_type(type_name);
91        vec![
92            FilterOperator {
93                suffix: "lt".to_string(),
94                type_name: type_name.to_string(),
95            },
96            FilterOperator {
97                suffix: "lte".to_string(),
98                type_name: type_name.to_string(),
99            },
100            FilterOperator {
101                suffix: "gt".to_string(),
102                type_name: type_name.to_string(),
103            },
104            FilterOperator {
105                suffix: "gte".to_string(),
106                type_name: type_name.to_string(),
107            },
108            FilterOperator {
109                suffix: "in".to_string(),
110                type_name: arr.clone(),
111            },
112            FilterOperator {
113                suffix: self.not_in_suffix().to_string(),
114                type_name: arr,
115            },
116        ]
117    }
118
119    /// Returns the filter operators available for a given scalar type.
120    fn get_filter_operators_for_scalar(&self, scalar: &ScalarType) -> Vec<FilterOperator> {
121        let mut ops: Vec<FilterOperator> = Vec::new();
122
123        match scalar {
124            ScalarType::String => {
125                let str_t = self.scalar_to_type(&ScalarType::String);
126                let arr = self.array_type(str_t);
127                ops.push(FilterOperator {
128                    suffix: "contains".to_string(),
129                    type_name: str_t.to_string(),
130                });
131                ops.push(FilterOperator {
132                    suffix: self.startswith_suffix().to_string(),
133                    type_name: str_t.to_string(),
134                });
135                ops.push(FilterOperator {
136                    suffix: self.endswith_suffix().to_string(),
137                    type_name: str_t.to_string(),
138                });
139                ops.push(FilterOperator {
140                    suffix: "in".to_string(),
141                    type_name: arr.clone(),
142                });
143                ops.push(FilterOperator {
144                    suffix: self.not_in_suffix().to_string(),
145                    type_name: arr,
146                });
147            }
148            ScalarType::Int | ScalarType::BigInt => {
149                ops.extend(self.numeric_operators(self.scalar_to_type(scalar)));
150            }
151            ScalarType::Float => {
152                ops.extend(self.numeric_operators(self.scalar_to_type(scalar)));
153            }
154            ScalarType::Decimal { .. } => {
155                ops.extend(self.numeric_operators(self.scalar_to_type(scalar)));
156            }
157            ScalarType::DateTime => {
158                ops.extend(self.numeric_operators(self.scalar_to_type(scalar)));
159            }
160            ScalarType::Uuid => {
161                let uuid_t = self.scalar_to_type(&ScalarType::Uuid);
162                let arr = self.array_type(uuid_t);
163                ops.push(FilterOperator {
164                    suffix: "in".to_string(),
165                    type_name: arr.clone(),
166                });
167                ops.push(FilterOperator {
168                    suffix: self.not_in_suffix().to_string(),
169                    type_name: arr,
170                });
171            }
172            ScalarType::Xml | ScalarType::Char { .. } | ScalarType::VarChar { .. } => {
173                let str_t = self.scalar_to_type(scalar);
174                let arr = self.array_type(str_t);
175                ops.push(FilterOperator {
176                    suffix: "contains".to_string(),
177                    type_name: str_t.to_string(),
178                });
179                ops.push(FilterOperator {
180                    suffix: self.startswith_suffix().to_string(),
181                    type_name: str_t.to_string(),
182                });
183                ops.push(FilterOperator {
184                    suffix: self.endswith_suffix().to_string(),
185                    type_name: str_t.to_string(),
186                });
187                ops.push(FilterOperator {
188                    suffix: "in".to_string(),
189                    type_name: arr.clone(),
190                });
191                ops.push(FilterOperator {
192                    suffix: self.not_in_suffix().to_string(),
193                    type_name: arr,
194                });
195            }
196            // Boolean, Bytes, Json, Jsonb: only equality via the direct field value.
197            ScalarType::Boolean | ScalarType::Bytes | ScalarType::Json | ScalarType::Jsonb => {}
198        }
199
200        // `not` is supported for all scalar types.
201        ops.push(FilterOperator {
202            suffix: "not".to_string(),
203            type_name: self.scalar_to_type(scalar).to_string(),
204        });
205
206        ops
207    }
208
209    /// Returns filter operators for a field, considering its resolved type
210    /// (scalar, enum, or relation).
211    fn get_filter_operators_for_field(
212        &self,
213        field: &FieldIr,
214        enums: &HashMap<String, EnumIr>,
215    ) -> Vec<FilterOperator> {
216        let mut ops: Vec<FilterOperator> = Vec::new();
217
218        match &field.field_type {
219            ResolvedFieldType::Scalar(scalar) => {
220                ops = self.get_filter_operators_for_scalar(scalar);
221            }
222            ResolvedFieldType::Enum { enum_name } => {
223                let enum_type = if enums.contains_key(enum_name) {
224                    enum_name.clone()
225                } else {
226                    // Fall back to the language's string type.
227                    self.scalar_to_type(&ScalarType::String).to_string()
228                };
229                let arr = self.array_type(&enum_type);
230                ops.push(FilterOperator {
231                    suffix: "in".to_string(),
232                    type_name: arr.clone(),
233                });
234                ops.push(FilterOperator {
235                    suffix: self.not_in_suffix().to_string(),
236                    type_name: arr,
237                });
238                ops.push(FilterOperator {
239                    suffix: "not".to_string(),
240                    type_name: enum_type,
241                });
242            }
243            ResolvedFieldType::Relation(_) | ResolvedFieldType::CompositeType { .. } => {
244                // Relations and composite types are not filterable via scalar operators.
245            }
246        }
247
248        // Null-check operator for optional / auto-generated fields.
249        if !field.is_required || self.is_auto_generated(field) {
250            ops.push(FilterOperator {
251                suffix: self.null_suffix().to_string(),
252                type_name: self.scalar_to_type(&ScalarType::Boolean).to_string(),
253            });
254        }
255
256        ops
257    }
258
259    /// The null literal in this language (Python: `"None"`, TS: `"null"`).
260    fn null_literal(&self) -> &'static str;
261
262    /// The boolean true literal (Python: `"True"`, TS: `"true"`).
263    fn true_literal(&self) -> &'static str;
264
265    /// The boolean false literal (Python: `"False"`, TS: `"false"`).
266    fn false_literal(&self) -> &'static str;
267
268    /// Format a string literal (Python: `"\"hello\""`, TS: `"'hello'"`).
269    fn string_literal(&self, s: &str) -> String;
270
271    /// The empty-array factory expression (Python: `"Field(default_factory=list)"`, TS: `"[]"`).
272    fn empty_array_literal(&self) -> &'static str;
273
274    /// Format an enum variant as a default value (Python: unquoted, TS: single-quoted).
275    fn enum_variant_literal(&self, variant: &str) -> String {
276        variant.to_string()
277    }
278
279    /// Resolves the base type name for a relation field.
280    ///
281    /// Python uses the model name directly; TypeScript appends `Model`.
282    fn relation_type(&self, target_model: &str) -> String {
283        target_model.to_string()
284    }
285
286    /// Returns the bare base type for a field without array or optional wrappers.
287    fn get_base_type(&self, field: &FieldIr, enums: &HashMap<String, EnumIr>) -> String {
288        match &field.field_type {
289            ResolvedFieldType::Scalar(scalar) => self.scalar_to_type(scalar).to_string(),
290            ResolvedFieldType::Enum { enum_name } => {
291                if enums.contains_key(enum_name) {
292                    enum_name.clone()
293                } else {
294                    self.scalar_to_type(&ScalarType::String).to_string()
295                }
296            }
297            ResolvedFieldType::CompositeType { type_name } => type_name.clone(),
298            ResolvedFieldType::Relation(rel) => self.relation_type(&rel.target_model),
299        }
300    }
301
302    /// Returns the default value expression for a field, or `None` if no default.
303    fn get_default_value(&self, field: &FieldIr) -> Option<String> {
304        if let Some(default) = &field.default_value {
305            match default {
306                DefaultValue::Function(FunctionCall { name, .. })
307                    if matches!(name.as_str(), "now" | "uuid" | "autoincrement") =>
308                {
309                    return Some(self.null_literal().to_string());
310                }
311                DefaultValue::Function(_) => return None,
312                DefaultValue::String(s) => return Some(self.string_literal(s)),
313                DefaultValue::Number(n) => return Some(n.clone()),
314                DefaultValue::Boolean(b) => {
315                    return Some(if *b {
316                        self.true_literal().to_string()
317                    } else {
318                        self.false_literal().to_string()
319                    });
320                }
321                DefaultValue::EnumVariant(v) => return Some(self.enum_variant_literal(v)),
322            }
323        }
324
325        if field.is_array {
326            Some(self.empty_array_literal().to_string())
327        } else if !field.is_required {
328            Some(self.null_literal().to_string())
329        } else {
330            None
331        }
332    }
333}