mold_cli/generators/
prisma.rs

1use crate::generators::{Generator, GeneratorConfig};
2use crate::types::{NestedType, ObjectType, Schema, SchemaType};
3use crate::utils::{is_prisma_reserved, sanitize_identifier, to_pascal_case};
4use anyhow::Result;
5
6pub struct PrismaGenerator;
7
8impl PrismaGenerator {
9    pub fn new() -> Self {
10        Self
11    }
12
13    fn generate_prisma_type(&self, schema_type: &SchemaType) -> Option<String> {
14        match schema_type {
15            SchemaType::String => Some("String".to_string()),
16            SchemaType::Number => Some("Float".to_string()),
17            SchemaType::Integer => Some("Int".to_string()),
18            SchemaType::Boolean => Some("Boolean".to_string()),
19            SchemaType::Array(inner) => {
20                // Prisma only supports arrays of scalar types
21                match inner.as_ref() {
22                    SchemaType::String => Some("String[]".to_string()),
23                    SchemaType::Integer => Some("Int[]".to_string()),
24                    SchemaType::Number => Some("Float[]".to_string()),
25                    SchemaType::Boolean => Some("Boolean[]".to_string()),
26                    _ => None, // Arrays of objects need relations
27                }
28            }
29            SchemaType::Null => None, // Prisma doesn't have null type
30            SchemaType::Optional(inner) => {
31                self.generate_prisma_type(inner).map(|t| format!("{}?", t))
32            }
33            SchemaType::Any | SchemaType::Union(_) => Some("Json".to_string()), // Use Json for complex types
34            SchemaType::Object(_) => None, // Objects need to be separate models
35        }
36    }
37
38    fn generate_model(&self, name: &str, obj: &ObjectType, indent: &str) -> String {
39        let model_name = self.format_model_name(name);
40        let mut lines = vec![format!("model {} {{", model_name)];
41
42        // Add id field
43        lines.push(format!("{}id Int @id @default(autoincrement())", indent));
44
45        // Add regular fields
46        for field in &obj.fields {
47            if let Some(field_str) = self.generate_field(field, indent) {
48                lines.push(field_str);
49            }
50        }
51
52        lines.push("}".to_string());
53        lines.join("\n")
54    }
55
56    fn generate_field(&self, field: &crate::types::Field, indent: &str) -> Option<String> {
57        let field_name = self.format_field_name(&field.name);
58
59        // Handle object types (need relations)
60        if let SchemaType::Object(_) = &field.field_type {
61            // Skip - these need to be handled as relations
62            return None;
63        }
64
65        let prisma_type = self.generate_prisma_type(&field.field_type)?;
66        let optional = if field.optional { "?" } else { "" };
67
68        Some(format!(
69            "{}{} {}{}",
70            indent,
71            field_name,
72            prisma_type,
73            if optional.is_empty() { "" } else { optional }
74        ))
75    }
76
77    fn format_field_name(&self, name: &str) -> String {
78        let sanitized = sanitize_identifier(name);
79        // Prisma field names must be valid identifiers
80        if is_prisma_reserved(&sanitized) {
81            format!("{}_", sanitized)
82        } else {
83            sanitized
84        }
85    }
86
87    fn format_model_name(&self, name: &str) -> String {
88        let pascal = to_pascal_case(name);
89        if is_prisma_reserved(&pascal) {
90            format!("{}Model", pascal)
91        } else {
92            pascal
93        }
94    }
95
96    fn generate_nested_models(&self, nested_types: &[NestedType], indent: &str) -> Vec<String> {
97        // Output deepest nested first (reversed)
98        nested_types
99            .iter()
100            .rev()
101            .map(|nt| self.generate_model(&nt.name, &nt.object, indent))
102            .collect()
103    }
104}
105
106impl Default for PrismaGenerator {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl Generator for PrismaGenerator {
113    fn generate(&self, schema: &Schema, config: &GeneratorConfig) -> Result<String> {
114        let mut output = vec!["// Generated by mold".to_string(), String::new()];
115
116        // Generate nested models first (if not flat mode)
117        if !config.flat_mode && !schema.nested_types.is_empty() {
118            let nested = self.generate_nested_models(&schema.nested_types, &config.indent);
119            for model in nested {
120                output.push(model);
121                output.push(String::new());
122            }
123        }
124
125        // Generate root model
126        if let SchemaType::Object(obj) = &schema.root_type {
127            output.push(self.generate_model(&schema.name, obj, &config.indent));
128        }
129
130        Ok(output.join("\n"))
131    }
132
133    fn file_extension(&self) -> &'static str {
134        "prisma"
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::types::Field;
142
143    #[test]
144    fn test_generate_simple_model() {
145        let gen = PrismaGenerator::new();
146        let obj = ObjectType::new(vec![
147            Field::new("name", SchemaType::String),
148            Field::new("age", SchemaType::Integer),
149            Field::new("active", SchemaType::Boolean),
150        ]);
151        let schema = Schema::new("User", SchemaType::Object(obj));
152        let config = GeneratorConfig::default();
153
154        let output = gen.generate(&schema, &config).unwrap();
155
156        assert!(output.contains("model User {"));
157        assert!(output.contains("id Int @id @default(autoincrement())"));
158        assert!(output.contains("name String"));
159        assert!(output.contains("age Int"));
160        assert!(output.contains("active Boolean"));
161    }
162
163    #[test]
164    fn test_generate_array_field() {
165        let gen = PrismaGenerator::new();
166        let obj = ObjectType::new(vec![Field::new(
167            "tags",
168            SchemaType::Array(Box::new(SchemaType::String)),
169        )]);
170        let schema = Schema::new("Post", SchemaType::Object(obj));
171        let config = GeneratorConfig::default();
172
173        let output = gen.generate(&schema, &config).unwrap();
174
175        assert!(output.contains("tags String[]"));
176    }
177
178    #[test]
179    fn test_generate_float_field() {
180        let gen = PrismaGenerator::new();
181        let obj = ObjectType::new(vec![Field::new("price", SchemaType::Number)]);
182        let schema = Schema::new("Product", SchemaType::Object(obj));
183        let config = GeneratorConfig::default();
184
185        let output = gen.generate(&schema, &config).unwrap();
186
187        assert!(output.contains("price Float"));
188    }
189
190    #[test]
191    fn test_reserved_word_handling() {
192        let gen = PrismaGenerator::new();
193        let obj = ObjectType::new(vec![Field::new("model", SchemaType::String)]);
194        let schema = Schema::new("Test", SchemaType::Object(obj));
195        let config = GeneratorConfig::default();
196
197        let output = gen.generate(&schema, &config).unwrap();
198
199        // Should rename the reserved field
200        assert!(output.contains("model_ String"));
201    }
202}