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