Skip to main content

prax_cli/commands/
import.rs

1//! Import schemas from Prisma, Diesel, or SeaORM.
2
3use std::fs;
4use std::path::Path;
5
6use prax_import::prelude::*;
7
8use crate::cli::{ImportArgs, ImportSource};
9use crate::config::SCHEMA_FILE_PATH;
10use crate::error::{CliError, CliResult};
11use crate::output;
12
13/// Run the import command.
14pub async fn run(args: ImportArgs) -> CliResult<()> {
15    output::info(&format!(
16        "Importing schema from {} → {}",
17        match args.from {
18            ImportSource::Prisma => "Prisma",
19            ImportSource::Diesel => "Diesel",
20            ImportSource::SeaOrm => "SeaORM",
21        },
22        args.output
23            .as_ref()
24            .map(|p| p.display().to_string())
25            .unwrap_or_else(|| "stdout".to_string())
26    ));
27
28    // Check if input file exists
29    if !args.input.exists() {
30        return Err(CliError::Config(format!(
31            "Input file not found: {}",
32            args.input.display()
33        )));
34    }
35
36    // Import the schema
37    let prax_schema = match args.from {
38        ImportSource::Prisma => import_from_prisma(&args.input)?,
39        ImportSource::Diesel => import_from_diesel(&args.input)?,
40        ImportSource::SeaOrm => import_from_seaorm(&args.input)?,
41    };
42
43    output::success(&format!(
44        "✓ Successfully imported {} models, {} enums",
45        prax_schema.models.len(),
46        prax_schema.enums.len()
47    ));
48
49    // Format the schema as text
50    let schema_text = format_schema(&prax_schema);
51
52    // Output the result
53    if args.print {
54        // Print to stdout
55        println!("{}", schema_text);
56    } else {
57        // Determine output path
58        let output_path = args.output.unwrap_or_else(|| {
59            // Default to prax/schema.prax
60            std::path::PathBuf::from(SCHEMA_FILE_PATH)
61        });
62
63        // Check if file exists and prompt if not forcing
64        if output_path.exists() && !args.force {
65            return Err(CliError::Config(format!(
66                "Output file already exists: {}. Use --force to overwrite.",
67                output_path.display()
68            )));
69        }
70
71        // Write the schema to file
72        fs::write(&output_path, schema_text).map_err(|e| {
73            CliError::Config(format!(
74                "Failed to write schema to {}: {}",
75                output_path.display(),
76                e
77            ))
78        })?;
79
80        output::success(&format!("✓ Schema written to {}", output_path.display()));
81    }
82
83    // Print helpful next steps
84    output::newline();
85    output::info("Next steps:");
86    output::info("  1. Review the generated schema file");
87    output::info("  2. Run `prax validate` to check for any issues");
88    output::info("  3. Run `prax generate` to generate Rust client code");
89    output::info("  4. Run `prax migrate dev` to apply migrations");
90
91    Ok(())
92}
93
94/// Import from a Prisma schema file.
95fn import_from_prisma(input: &Path) -> CliResult<prax_schema::Schema> {
96    output::info(&format!("Reading Prisma schema from {}", input.display()));
97
98    import_prisma_schema_file(input)
99        .map_err(|e| CliError::Schema(format!("Failed to import Prisma schema: {}", e)))
100}
101
102/// Import from a Diesel schema file.
103fn import_from_diesel(input: &Path) -> CliResult<prax_schema::Schema> {
104    output::info(&format!("Reading Diesel schema from {}", input.display()));
105
106    import_diesel_schema_file(input)
107        .map_err(|e| CliError::Schema(format!("Failed to import Diesel schema: {}", e)))
108}
109
110/// Import from a SeaORM entity file.
111fn import_from_seaorm(input: &Path) -> CliResult<prax_schema::Schema> {
112    output::info(&format!("Reading SeaORM entity from {}", input.display()));
113
114    #[cfg(feature = "seaorm")]
115    {
116        use prax_import::seaorm::import_seaorm_entity_file;
117        import_seaorm_entity_file(input)
118            .map_err(|e| CliError::Schema(format!("Failed to import SeaORM entity: {}", e)))
119    }
120
121    #[cfg(not(feature = "seaorm"))]
122    {
123        Err(CliError::Config(
124            "SeaORM import support not enabled. Rebuild with --features seaorm".to_string(),
125        ))
126    }
127}
128
129/// Format a Prax schema as a string.
130///
131/// This is a simple formatter that outputs the schema in Prax DSL format.
132/// TODO: Use prax-schema's built-in formatter when available.
133fn format_schema(schema: &prax_schema::Schema) -> String {
134    let mut output = String::new();
135
136    // Add datasource if present
137    if let Some(datasource) = &schema.datasource {
138        output.push_str(&format!(
139            "datasource db {{\n  provider = \"{}\"\n",
140            datasource.provider.as_str()
141        ));
142
143        if let Some(url) = &datasource.url {
144            output.push_str(&format!("  url      = \"{}\"\n", url));
145        }
146
147        output.push_str("}\n\n");
148    }
149
150    // Add enums
151    for (_, enum_def) in &schema.enums {
152        output.push_str(&format!("enum {} {{\n", enum_def.name()));
153
154        for variant in &enum_def.variants {
155            output.push_str(&format!("  {}\n", variant.name()));
156        }
157
158        output.push_str("}\n\n");
159    }
160
161    // Add models
162    for (_, model) in &schema.models {
163        if let Some(doc) = &model.documentation {
164            for line in doc.text.lines() {
165                output.push_str(&format!("/// {}\n", line));
166            }
167        }
168
169        output.push_str(&format!("model {} {{\n", model.name()));
170
171        // Add fields
172        for (_, field) in &model.fields {
173            if let Some(doc) = &field.documentation {
174                for line in doc.text.lines() {
175                    output.push_str(&format!("  /// {}\n", line));
176                }
177            }
178
179            let field_name = field.name();
180            let field_type = format_field_type(&field.field_type, &field.modifier);
181
182            output.push_str(&format!("  {} {}", field_name, field_type));
183
184            // Pgvector scalars (Vector/HalfVector/SparseVector/Bit) carry the
185            // dimension in `ScalarType::Vector(Option<u32>)` etc. The schema
186            // parser expects the dimension as a separate `@dim(N)` attribute
187            // rather than inline `Vector(N)` syntax, so emit it explicitly.
188            if let prax_schema::FieldType::Scalar(scalar) = &field.field_type {
189                if let Some(dim) = scalar.dimension() {
190                    output.push_str(&format!(" @dim({})", dim));
191                }
192            }
193
194            // Add attributes
195            for attr in &field.attributes {
196                output.push_str(&format!(" @{}", attr.name.as_str()));
197
198                // Add attribute arguments if present
199                if !attr.args.is_empty() {
200                    output.push('(');
201                    for (i, arg) in attr.args.iter().enumerate() {
202                        if i > 0 {
203                            output.push_str(", ");
204                        }
205                        if let Some(name) = &arg.name {
206                            output.push_str(&format!("{}: ", name.as_str()));
207                        }
208                        output.push_str(&format_attribute_value(&arg.value));
209                    }
210                    output.push(')');
211                }
212            }
213
214            output.push('\n');
215        }
216
217        // Add model attributes
218        for attr in &model.attributes {
219            output.push_str(&format!("  @@{}", attr.name.as_str()));
220
221            if !attr.args.is_empty() {
222                output.push('(');
223                for (i, arg) in attr.args.iter().enumerate() {
224                    if i > 0 {
225                        output.push_str(", ");
226                    }
227                    if let Some(name) = &arg.name {
228                        output.push_str(&format!("{}: ", name.as_str()));
229                    }
230                    output.push_str(&format_attribute_value(&arg.value));
231                }
232                output.push(')');
233            }
234
235            output.push('\n');
236        }
237
238        output.push_str("}\n\n");
239    }
240
241    output
242}
243
244/// Format a field type with its modifier.
245fn format_field_type(
246    field_type: &prax_schema::FieldType,
247    modifier: &prax_schema::TypeModifier,
248) -> String {
249    let base = field_type.type_name().to_string();
250
251    match modifier {
252        prax_schema::TypeModifier::Required => base,
253        prax_schema::TypeModifier::Optional => format!("{}?", base),
254        prax_schema::TypeModifier::List => format!("{}[]", base),
255        prax_schema::TypeModifier::OptionalList => format!("{}[]?", base),
256    }
257}
258
259/// Format an attribute value.
260fn format_attribute_value(value: &prax_schema::AttributeValue) -> String {
261    use prax_schema::AttributeValue;
262
263    match value {
264        AttributeValue::String(s) => format!("\"{}\"", s),
265        AttributeValue::Int(i) => i.to_string(),
266        AttributeValue::Float(f) => f.to_string(),
267        AttributeValue::Boolean(b) => b.to_string(),
268        AttributeValue::Ident(id) => id.to_string(),
269        AttributeValue::Function(name, args) => {
270            if args.is_empty() {
271                format!("{}()", name)
272            } else {
273                let args_str = args
274                    .iter()
275                    .map(format_attribute_value)
276                    .collect::<Vec<_>>()
277                    .join(", ");
278                format!("{}({})", name, args_str)
279            }
280        }
281        AttributeValue::Array(items) => {
282            let items_str = items
283                .iter()
284                .map(format_attribute_value)
285                .collect::<Vec<_>>()
286                .join(", ");
287            format!("[{}]", items_str)
288        }
289        AttributeValue::FieldRef(name) => name.to_string(),
290        AttributeValue::FieldRefList(names) => {
291            let names_str = names
292                .iter()
293                .map(|n| n.to_string())
294                .collect::<Vec<_>>()
295                .join(", ");
296            format!("[{}]", names_str)
297        }
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn test_format_field_type() {
307        use prax_schema::{FieldType, ScalarType, TypeModifier};
308
309        assert_eq!(
310            format_field_type(
311                &FieldType::Scalar(ScalarType::String),
312                &TypeModifier::Required
313            ),
314            "String"
315        );
316
317        assert_eq!(
318            format_field_type(&FieldType::Scalar(ScalarType::Int), &TypeModifier::Optional),
319            "Int?"
320        );
321
322        assert_eq!(
323            format_field_type(&FieldType::Scalar(ScalarType::String), &TypeModifier::List),
324            "String[]"
325        );
326    }
327}