Skip to main content

nautilus_codegen/
backend.rs

1//! Language-backend abstraction for Nautilus code generation.
2//!
3//! Each target language (Python, TypeScript, …) implements [`LanguageBackend`],
4//! which defines how Nautilus scalar types map to language types and how
5//! operator names are spelled.  The trait provides default implementations for
6//! logic that is identical across backends — most notably
7//! [`is_auto_generated`](LanguageBackend::is_auto_generated) and the full
8//! filter-operator construction pipeline — so that concrete backends only need
9//! to supply the language-specific primitives.
10
11use nautilus_schema::ir::{
12    DefaultValue, EnumIr, FieldIr, FunctionCall, ResolvedFieldType, ScalarType,
13};
14use serde::Serialize;
15use std::collections::HashMap;
16
17/// A filter operator entry produced by a [`LanguageBackend`].
18///
19/// The `type_name` field holds the target-language type as a string (e.g.
20/// `"str"`, `"List[int]"`, `"string[]"`).  Generator code converts this into a
21/// template-context struct whose field may be named differently
22/// (`python_type`, `ts_type`, …).
23#[derive(Debug, Clone, Serialize)]
24pub struct FilterOperator {
25    pub suffix: String,
26    pub type_name: String,
27}
28
29/// Common interface for language-specific code generation backends.
30///
31/// ## Abstract methods
32/// Backends must implement the four type-mapping primitives and the four
33/// operator-naming conventions.
34///
35/// ## Default methods
36/// Everything else — `is_auto_generated`, the numeric-operator helper, and the
37/// full filter-operator builders — is provided as a default implementation that
38/// composes the abstract methods.  Backends only override these defaults when
39/// their language genuinely diverges from the shared logic.
40pub trait LanguageBackend {
41    /// Maps a Nautilus scalar type to the target language's type name.
42    fn scalar_to_type(&self, scalar: &ScalarType) -> &'static str;
43
44    /// Wraps a type name in the language's array/list syntax.
45    ///
46    /// Examples: `"List[T]"` (Python) vs `"T[]"` (TypeScript).
47    fn array_type(&self, inner: &str) -> String;
48
49    /// Suffix for the "not in collection" operator.
50    ///
51    /// Python: `"not_in"` — TypeScript: `"notIn"`
52    fn not_in_suffix(&self) -> &'static str;
53
54    /// Suffix for the "starts with" string operator.
55    ///
56    /// Python: `"startswith"` — TypeScript: `"startsWith"`
57    fn startswith_suffix(&self) -> &'static str;
58
59    /// Suffix for the "ends with" string operator.
60    ///
61    /// Python: `"endswith"` — TypeScript: `"endsWith"`
62    fn endswith_suffix(&self) -> &'static str;
63
64    /// Suffix for the null-check operator.
65    ///
66    /// Python: `"is_null"` — TypeScript: `"isNull"`
67    fn null_suffix(&self) -> &'static str;
68
69    /// Returns `true` for fields whose values are supplied automatically by the
70    /// database: `autoincrement()`, `uuid()`, or `now()`.
71    ///
72    /// This implementation is identical for Python and TypeScript.  The Rust
73    /// backend intentionally differs (it exposes `now()` fields as writable),
74    /// which is why it lives in `type_helpers.rs` and does not use this trait.
75    fn is_auto_generated(&self, field: &FieldIr) -> bool {
76        if field.computed.is_some() {
77            return true;
78        }
79        if let Some(default) = &field.default_value {
80            matches!(
81                default,
82                DefaultValue::Function(f)
83                    if f.name == "autoincrement" || f.name == "uuid" || f.name == "now"
84            )
85        } else {
86            false
87        }
88    }
89
90    /// Returns the standard comparison operators (`lt`, `lte`, `gt`, `gte`,
91    /// `in`, `not_in`/`notIn`) for a numeric-like type.
92    fn numeric_operators(&self, type_name: &str) -> Vec<FilterOperator> {
93        let arr = self.array_type(type_name);
94        vec![
95            FilterOperator {
96                suffix: "lt".to_string(),
97                type_name: type_name.to_string(),
98            },
99            FilterOperator {
100                suffix: "lte".to_string(),
101                type_name: type_name.to_string(),
102            },
103            FilterOperator {
104                suffix: "gt".to_string(),
105                type_name: type_name.to_string(),
106            },
107            FilterOperator {
108                suffix: "gte".to_string(),
109                type_name: type_name.to_string(),
110            },
111            FilterOperator {
112                suffix: "in".to_string(),
113                type_name: arr.clone(),
114            },
115            FilterOperator {
116                suffix: self.not_in_suffix().to_string(),
117                type_name: arr,
118            },
119        ]
120    }
121
122    /// Returns the filter operators available for a given scalar type.
123    fn get_filter_operators_for_scalar(&self, scalar: &ScalarType) -> Vec<FilterOperator> {
124        let mut ops: Vec<FilterOperator> = Vec::new();
125
126        match scalar {
127            ScalarType::String => {
128                let str_t = self.scalar_to_type(&ScalarType::String);
129                let arr = self.array_type(str_t);
130                ops.push(FilterOperator {
131                    suffix: "contains".to_string(),
132                    type_name: str_t.to_string(),
133                });
134                ops.push(FilterOperator {
135                    suffix: self.startswith_suffix().to_string(),
136                    type_name: str_t.to_string(),
137                });
138                ops.push(FilterOperator {
139                    suffix: self.endswith_suffix().to_string(),
140                    type_name: str_t.to_string(),
141                });
142                ops.push(FilterOperator {
143                    suffix: "in".to_string(),
144                    type_name: arr.clone(),
145                });
146                ops.push(FilterOperator {
147                    suffix: self.not_in_suffix().to_string(),
148                    type_name: arr,
149                });
150            }
151            ScalarType::Int | ScalarType::BigInt => {
152                ops.extend(self.numeric_operators(self.scalar_to_type(scalar)));
153            }
154            ScalarType::Float => {
155                ops.extend(self.numeric_operators(self.scalar_to_type(scalar)));
156            }
157            ScalarType::Decimal { .. } => {
158                ops.extend(self.numeric_operators(self.scalar_to_type(scalar)));
159            }
160            ScalarType::DateTime => {
161                ops.extend(self.numeric_operators(self.scalar_to_type(scalar)));
162            }
163            ScalarType::Uuid => {
164                let uuid_t = self.scalar_to_type(&ScalarType::Uuid);
165                let arr = self.array_type(uuid_t);
166                ops.push(FilterOperator {
167                    suffix: "in".to_string(),
168                    type_name: arr.clone(),
169                });
170                ops.push(FilterOperator {
171                    suffix: self.not_in_suffix().to_string(),
172                    type_name: arr,
173                });
174            }
175            ScalarType::Xml | ScalarType::Char { .. } | ScalarType::VarChar { .. } => {
176                let str_t = self.scalar_to_type(scalar);
177                let arr = self.array_type(str_t);
178                ops.push(FilterOperator {
179                    suffix: "contains".to_string(),
180                    type_name: str_t.to_string(),
181                });
182                ops.push(FilterOperator {
183                    suffix: self.startswith_suffix().to_string(),
184                    type_name: str_t.to_string(),
185                });
186                ops.push(FilterOperator {
187                    suffix: self.endswith_suffix().to_string(),
188                    type_name: str_t.to_string(),
189                });
190                ops.push(FilterOperator {
191                    suffix: "in".to_string(),
192                    type_name: arr.clone(),
193                });
194                ops.push(FilterOperator {
195                    suffix: self.not_in_suffix().to_string(),
196                    type_name: arr,
197                });
198            }
199            // Boolean, Bytes, Json, Jsonb: only equality via the direct field value.
200            ScalarType::Boolean | ScalarType::Bytes | ScalarType::Json | ScalarType::Jsonb => {}
201        }
202
203        // `not` is supported for all scalar types.
204        ops.push(FilterOperator {
205            suffix: "not".to_string(),
206            type_name: self.scalar_to_type(scalar).to_string(),
207        });
208
209        ops
210    }
211
212    /// Returns filter operators for a field, considering its resolved type
213    /// (scalar, enum, or relation).
214    fn get_filter_operators_for_field(
215        &self,
216        field: &FieldIr,
217        enums: &HashMap<String, EnumIr>,
218    ) -> Vec<FilterOperator> {
219        let mut ops: Vec<FilterOperator> = Vec::new();
220
221        match &field.field_type {
222            ResolvedFieldType::Scalar(scalar) => {
223                ops = self.get_filter_operators_for_scalar(scalar);
224            }
225            ResolvedFieldType::Enum { enum_name } => {
226                let enum_type = if enums.contains_key(enum_name) {
227                    enum_name.clone()
228                } else {
229                    // Fall back to the language's string type.
230                    self.scalar_to_type(&ScalarType::String).to_string()
231                };
232                let arr = self.array_type(&enum_type);
233                ops.push(FilterOperator {
234                    suffix: "in".to_string(),
235                    type_name: arr.clone(),
236                });
237                ops.push(FilterOperator {
238                    suffix: self.not_in_suffix().to_string(),
239                    type_name: arr,
240                });
241                ops.push(FilterOperator {
242                    suffix: "not".to_string(),
243                    type_name: enum_type,
244                });
245            }
246            ResolvedFieldType::Relation(_) | ResolvedFieldType::CompositeType { .. } => {
247                // Relations and composite types are not filterable via scalar operators.
248            }
249        }
250
251        // Null-check operator for optional / auto-generated fields.
252        if !field.is_required || self.is_auto_generated(field) {
253            ops.push(FilterOperator {
254                suffix: self.null_suffix().to_string(),
255                type_name: self.scalar_to_type(&ScalarType::Boolean).to_string(),
256            });
257        }
258
259        ops
260    }
261
262    /// The null literal in this language (Python: `"None"`, TS: `"null"`).
263    fn null_literal(&self) -> &'static str;
264
265    /// The boolean true literal (Python: `"True"`, TS: `"true"`).
266    fn true_literal(&self) -> &'static str;
267
268    /// The boolean false literal (Python: `"False"`, TS: `"false"`).
269    fn false_literal(&self) -> &'static str;
270
271    /// Format a string literal (Python: `"\"hello\""`, TS: `"'hello'"`).
272    fn string_literal(&self, s: &str) -> String;
273
274    /// The empty-array factory expression (Python: `"Field(default_factory=list)"`, TS: `"[]"`).
275    fn empty_array_literal(&self) -> &'static str;
276
277    /// Format an enum variant as a default value (Python: unquoted, TS: single-quoted).
278    fn enum_variant_literal(&self, variant: &str) -> String {
279        variant.to_string()
280    }
281
282    /// Resolves the base type name for a relation field.
283    ///
284    /// Python uses the model name directly; TypeScript appends `Model`.
285    fn relation_type(&self, target_model: &str) -> String {
286        target_model.to_string()
287    }
288
289    /// Returns the bare base type for a field without array or optional wrappers.
290    fn get_base_type(&self, field: &FieldIr, enums: &HashMap<String, EnumIr>) -> String {
291        match &field.field_type {
292            ResolvedFieldType::Scalar(scalar) => self.scalar_to_type(scalar).to_string(),
293            ResolvedFieldType::Enum { enum_name } => {
294                if enums.contains_key(enum_name) {
295                    enum_name.clone()
296                } else {
297                    self.scalar_to_type(&ScalarType::String).to_string()
298                }
299            }
300            ResolvedFieldType::CompositeType { type_name } => type_name.clone(),
301            ResolvedFieldType::Relation(rel) => self.relation_type(&rel.target_model),
302        }
303    }
304
305    /// Returns the default value expression for a field, or `None` if no default.
306    fn get_default_value(&self, field: &FieldIr) -> Option<String> {
307        if let Some(default) = &field.default_value {
308            match default {
309                DefaultValue::Function(FunctionCall { name, .. })
310                    if matches!(name.as_str(), "now" | "uuid" | "autoincrement") =>
311                {
312                    return Some(self.null_literal().to_string());
313                }
314                DefaultValue::Function(_) => return None,
315                DefaultValue::String(s) => return Some(self.string_literal(s)),
316                DefaultValue::Number(n) => return Some(n.clone()),
317                DefaultValue::Boolean(b) => {
318                    return Some(if *b {
319                        self.true_literal().to_string()
320                    } else {
321                        self.false_literal().to_string()
322                    });
323                }
324                DefaultValue::EnumVariant(v) => return Some(self.enum_variant_literal(v)),
325            }
326        }
327
328        if field.is_array {
329            Some(self.empty_array_literal().to_string())
330        } else if !field.is_required {
331            Some(self.null_literal().to_string())
332        } else {
333            None
334        }
335    }
336}
337
338/// Python language backend.
339pub struct PythonBackend;
340
341impl LanguageBackend for PythonBackend {
342    fn scalar_to_type(&self, scalar: &ScalarType) -> &'static str {
343        match scalar {
344            ScalarType::String => "str",
345            ScalarType::Int => "int",
346            ScalarType::BigInt => "int",
347            ScalarType::Float => "float",
348            ScalarType::Decimal { .. } => "Decimal",
349            ScalarType::Boolean => "bool",
350            ScalarType::DateTime => "datetime",
351            ScalarType::Bytes => "bytes",
352            ScalarType::Json => "Dict[str, Any]",
353            ScalarType::Uuid => "UUID",
354            ScalarType::Jsonb => "Dict[str, Any]",
355            ScalarType::Xml | ScalarType::Char { .. } | ScalarType::VarChar { .. } => "str",
356        }
357    }
358
359    fn array_type(&self, inner: &str) -> String {
360        format!("List[{}]", inner)
361    }
362
363    fn not_in_suffix(&self) -> &'static str {
364        "not_in"
365    }
366
367    fn startswith_suffix(&self) -> &'static str {
368        "startswith"
369    }
370
371    fn endswith_suffix(&self) -> &'static str {
372        "endswith"
373    }
374
375    fn null_suffix(&self) -> &'static str {
376        "is_null"
377    }
378
379    fn null_literal(&self) -> &'static str {
380        "None"
381    }
382    fn true_literal(&self) -> &'static str {
383        "True"
384    }
385    fn false_literal(&self) -> &'static str {
386        "False"
387    }
388    fn string_literal(&self, s: &str) -> String {
389        format!("\"{}\"", s)
390    }
391    fn empty_array_literal(&self) -> &'static str {
392        "Field(default_factory=list)"
393    }
394}
395
396/// TypeScript / JavaScript language backend.
397pub struct JsBackend;
398
399impl LanguageBackend for JsBackend {
400    fn scalar_to_type(&self, scalar: &ScalarType) -> &'static str {
401        match scalar {
402            ScalarType::String => "string",
403            ScalarType::Int => "number",
404            ScalarType::BigInt => "number",
405            ScalarType::Float => "number",
406            ScalarType::Decimal { .. } => "string", // preserve precision; no decimal.js dependency
407            ScalarType::Boolean => "boolean",
408            ScalarType::DateTime => "Date",
409            ScalarType::Bytes => "Buffer",
410            ScalarType::Json => "Record<string, unknown>",
411            ScalarType::Uuid => "string",
412            ScalarType::Jsonb => "Record<string, unknown>",
413            ScalarType::Xml | ScalarType::Char { .. } | ScalarType::VarChar { .. } => "string",
414        }
415    }
416
417    fn array_type(&self, inner: &str) -> String {
418        format!("{}[]", inner)
419    }
420
421    fn not_in_suffix(&self) -> &'static str {
422        "notIn"
423    }
424
425    fn startswith_suffix(&self) -> &'static str {
426        "startsWith"
427    }
428
429    fn endswith_suffix(&self) -> &'static str {
430        "endsWith"
431    }
432
433    fn null_suffix(&self) -> &'static str {
434        "isNull"
435    }
436
437    fn null_literal(&self) -> &'static str {
438        "null"
439    }
440    fn true_literal(&self) -> &'static str {
441        "true"
442    }
443    fn false_literal(&self) -> &'static str {
444        "false"
445    }
446    fn string_literal(&self, s: &str) -> String {
447        format!("'{}'", s)
448    }
449    fn empty_array_literal(&self) -> &'static str {
450        "[]"
451    }
452    fn enum_variant_literal(&self, variant: &str) -> String {
453        format!("'{}'", variant)
454    }
455    fn relation_type(&self, target_model: &str) -> String {
456        format!("{}Model", target_model)
457    }
458}