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            // Xml, Char, VarChar: string-like operators.
176            ScalarType::Xml | ScalarType::Char { .. } | ScalarType::VarChar { .. } => {
177                let str_t = self.scalar_to_type(scalar);
178                let arr = self.array_type(str_t);
179                ops.push(FilterOperator {
180                    suffix: "contains".to_string(),
181                    type_name: str_t.to_string(),
182                });
183                ops.push(FilterOperator {
184                    suffix: self.startswith_suffix().to_string(),
185                    type_name: str_t.to_string(),
186                });
187                ops.push(FilterOperator {
188                    suffix: self.endswith_suffix().to_string(),
189                    type_name: str_t.to_string(),
190                });
191                ops.push(FilterOperator {
192                    suffix: "in".to_string(),
193                    type_name: arr.clone(),
194                });
195                ops.push(FilterOperator {
196                    suffix: self.not_in_suffix().to_string(),
197                    type_name: arr,
198                });
199            }
200            // Boolean, Bytes, Json, Jsonb: only equality via the direct field value.
201            ScalarType::Boolean | ScalarType::Bytes | ScalarType::Json | ScalarType::Jsonb => {}
202        }
203
204        // `not` is supported for all scalar types.
205        ops.push(FilterOperator {
206            suffix: "not".to_string(),
207            type_name: self.scalar_to_type(scalar).to_string(),
208        });
209
210        ops
211    }
212
213    /// Returns filter operators for a field, considering its resolved type
214    /// (scalar, enum, or relation).
215    fn get_filter_operators_for_field(
216        &self,
217        field: &FieldIr,
218        enums: &HashMap<String, EnumIr>,
219    ) -> Vec<FilterOperator> {
220        let mut ops: Vec<FilterOperator> = Vec::new();
221
222        match &field.field_type {
223            ResolvedFieldType::Scalar(scalar) => {
224                ops = self.get_filter_operators_for_scalar(scalar);
225            }
226            ResolvedFieldType::Enum { enum_name } => {
227                let enum_type = if enums.contains_key(enum_name) {
228                    enum_name.clone()
229                } else {
230                    // Fall back to the language's string type.
231                    self.scalar_to_type(&ScalarType::String).to_string()
232                };
233                let arr = self.array_type(&enum_type);
234                ops.push(FilterOperator {
235                    suffix: "in".to_string(),
236                    type_name: arr.clone(),
237                });
238                ops.push(FilterOperator {
239                    suffix: self.not_in_suffix().to_string(),
240                    type_name: arr,
241                });
242                ops.push(FilterOperator {
243                    suffix: "not".to_string(),
244                    type_name: enum_type,
245                });
246            }
247            ResolvedFieldType::Relation(_) | ResolvedFieldType::CompositeType { .. } => {
248                // Relations and composite types are not filterable via scalar operators.
249            }
250        }
251
252        // Null-check operator for optional / auto-generated fields.
253        if !field.is_required || self.is_auto_generated(field) {
254            ops.push(FilterOperator {
255                suffix: self.null_suffix().to_string(),
256                // Use the language's boolean type (Python: "bool", TS: "boolean").
257                type_name: self.scalar_to_type(&ScalarType::Boolean).to_string(),
258            });
259        }
260
261        ops
262    }
263
264    // --- Language-literal primitives for default-value formatting ---
265
266    /// The null literal in this language (Python: `"None"`, TS: `"null"`).
267    fn null_literal(&self) -> &'static str;
268
269    /// The boolean true literal (Python: `"True"`, TS: `"true"`).
270    fn true_literal(&self) -> &'static str;
271
272    /// The boolean false literal (Python: `"False"`, TS: `"false"`).
273    fn false_literal(&self) -> &'static str;
274
275    /// Format a string literal (Python: `"\"hello\""`, TS: `"'hello'"`).
276    fn string_literal(&self, s: &str) -> String;
277
278    /// The empty-array factory expression (Python: `"Field(default_factory=list)"`, TS: `"[]"`).
279    fn empty_array_literal(&self) -> &'static str;
280
281    /// Format an enum variant as a default value (Python: unquoted, TS: single-quoted).
282    fn enum_variant_literal(&self, variant: &str) -> String {
283        variant.to_string()
284    }
285
286    /// Resolves the base type name for a relation field.
287    ///
288    /// Python uses the model name directly; TypeScript appends `Model`.
289    fn relation_type(&self, target_model: &str) -> String {
290        target_model.to_string()
291    }
292
293    // --- Shared derived methods ---
294
295    /// Returns the bare base type for a field without array or optional wrappers.
296    fn get_base_type(&self, field: &FieldIr, enums: &HashMap<String, EnumIr>) -> String {
297        match &field.field_type {
298            ResolvedFieldType::Scalar(scalar) => self.scalar_to_type(scalar).to_string(),
299            ResolvedFieldType::Enum { enum_name } => {
300                if enums.contains_key(enum_name) {
301                    enum_name.clone()
302                } else {
303                    self.scalar_to_type(&ScalarType::String).to_string()
304                }
305            }
306            ResolvedFieldType::CompositeType { type_name } => type_name.clone(),
307            ResolvedFieldType::Relation(rel) => self.relation_type(&rel.target_model),
308        }
309    }
310
311    /// Returns the default value expression for a field, or `None` if no default.
312    fn get_default_value(&self, field: &FieldIr) -> Option<String> {
313        if let Some(default) = &field.default_value {
314            match default {
315                DefaultValue::Function(FunctionCall { name, .. })
316                    if matches!(name.as_str(), "now" | "uuid" | "autoincrement") =>
317                {
318                    return Some(self.null_literal().to_string());
319                }
320                DefaultValue::Function(_) => return None,
321                DefaultValue::String(s) => return Some(self.string_literal(s)),
322                DefaultValue::Number(n) => return Some(n.clone()),
323                DefaultValue::Boolean(b) => {
324                    return Some(if *b {
325                        self.true_literal().to_string()
326                    } else {
327                        self.false_literal().to_string()
328                    });
329                }
330                DefaultValue::EnumVariant(v) => return Some(self.enum_variant_literal(v)),
331            }
332        }
333
334        if field.is_array {
335            Some(self.empty_array_literal().to_string())
336        } else if !field.is_required {
337            Some(self.null_literal().to_string())
338        } else {
339            None
340        }
341    }
342}
343
344/// Python language backend.
345pub struct PythonBackend;
346
347impl LanguageBackend for PythonBackend {
348    fn scalar_to_type(&self, scalar: &ScalarType) -> &'static str {
349        match scalar {
350            ScalarType::String => "str",
351            ScalarType::Int => "int",
352            ScalarType::BigInt => "int",
353            ScalarType::Float => "float",
354            ScalarType::Decimal { .. } => "Decimal",
355            ScalarType::Boolean => "bool",
356            ScalarType::DateTime => "datetime",
357            ScalarType::Bytes => "bytes",
358            ScalarType::Json => "Dict[str, Any]",
359            ScalarType::Uuid => "UUID",
360            ScalarType::Jsonb => "Dict[str, Any]",
361            ScalarType::Xml | ScalarType::Char { .. } | ScalarType::VarChar { .. } => "str",
362        }
363    }
364
365    fn array_type(&self, inner: &str) -> String {
366        format!("List[{}]", inner)
367    }
368
369    fn not_in_suffix(&self) -> &'static str {
370        "not_in"
371    }
372
373    fn startswith_suffix(&self) -> &'static str {
374        "startswith"
375    }
376
377    fn endswith_suffix(&self) -> &'static str {
378        "endswith"
379    }
380
381    fn null_suffix(&self) -> &'static str {
382        "is_null"
383    }
384
385    fn null_literal(&self) -> &'static str {
386        "None"
387    }
388    fn true_literal(&self) -> &'static str {
389        "True"
390    }
391    fn false_literal(&self) -> &'static str {
392        "False"
393    }
394    fn string_literal(&self, s: &str) -> String {
395        format!("\"{}\"", s)
396    }
397    fn empty_array_literal(&self) -> &'static str {
398        "Field(default_factory=list)"
399    }
400}
401
402/// TypeScript / JavaScript language backend.
403pub struct JsBackend;
404
405impl LanguageBackend for JsBackend {
406    fn scalar_to_type(&self, scalar: &ScalarType) -> &'static str {
407        match scalar {
408            ScalarType::String => "string",
409            ScalarType::Int => "number",
410            ScalarType::BigInt => "number",
411            ScalarType::Float => "number",
412            ScalarType::Decimal { .. } => "string", // preserve precision; no decimal.js dependency
413            ScalarType::Boolean => "boolean",
414            ScalarType::DateTime => "Date",
415            ScalarType::Bytes => "Buffer",
416            ScalarType::Json => "Record<string, unknown>",
417            ScalarType::Uuid => "string",
418            ScalarType::Jsonb => "Record<string, unknown>",
419            ScalarType::Xml | ScalarType::Char { .. } | ScalarType::VarChar { .. } => "string",
420        }
421    }
422
423    fn array_type(&self, inner: &str) -> String {
424        format!("{}[]", inner)
425    }
426
427    fn not_in_suffix(&self) -> &'static str {
428        "notIn"
429    }
430
431    fn startswith_suffix(&self) -> &'static str {
432        "startsWith"
433    }
434
435    fn endswith_suffix(&self) -> &'static str {
436        "endsWith"
437    }
438
439    fn null_suffix(&self) -> &'static str {
440        "isNull"
441    }
442
443    fn null_literal(&self) -> &'static str {
444        "null"
445    }
446    fn true_literal(&self) -> &'static str {
447        "true"
448    }
449    fn false_literal(&self) -> &'static str {
450        "false"
451    }
452    fn string_literal(&self, s: &str) -> String {
453        format!("'{}'", s)
454    }
455    fn empty_array_literal(&self) -> &'static str {
456        "[]"
457    }
458    fn enum_variant_literal(&self, variant: &str) -> String {
459        format!("'{}'", variant)
460    }
461    fn relation_type(&self, target_model: &str) -> String {
462        format!("{}Model", target_model)
463    }
464}