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            // Add attributes
185            for attr in &field.attributes {
186                output.push_str(&format!(" @{}", attr.name.as_str()));
187
188                // Add attribute arguments if present
189                if !attr.args.is_empty() {
190                    output.push('(');
191                    for (i, arg) in attr.args.iter().enumerate() {
192                        if i > 0 {
193                            output.push_str(", ");
194                        }
195                        if let Some(name) = &arg.name {
196                            output.push_str(&format!("{}: ", name.as_str()));
197                        }
198                        output.push_str(&format_attribute_value(&arg.value));
199                    }
200                    output.push(')');
201                }
202            }
203
204            output.push('\n');
205        }
206
207        // Add model attributes
208        for attr in &model.attributes {
209            output.push_str(&format!("  @@{}", attr.name.as_str()));
210
211            if !attr.args.is_empty() {
212                output.push('(');
213                for (i, arg) in attr.args.iter().enumerate() {
214                    if i > 0 {
215                        output.push_str(", ");
216                    }
217                    if let Some(name) = &arg.name {
218                        output.push_str(&format!("{}: ", name.as_str()));
219                    }
220                    output.push_str(&format_attribute_value(&arg.value));
221                }
222                output.push(')');
223            }
224
225            output.push('\n');
226        }
227
228        output.push_str("}\n\n");
229    }
230
231    output
232}
233
234/// Format a field type with its modifier.
235fn format_field_type(
236    field_type: &prax_schema::FieldType,
237    modifier: &prax_schema::TypeModifier,
238) -> String {
239    let base = field_type.type_name().to_string();
240
241    match modifier {
242        prax_schema::TypeModifier::Required => base,
243        prax_schema::TypeModifier::Optional => format!("{}?", base),
244        prax_schema::TypeModifier::List => format!("{}[]", base),
245        prax_schema::TypeModifier::OptionalList => format!("{}[]?", base),
246    }
247}
248
249/// Format an attribute value.
250fn format_attribute_value(value: &prax_schema::AttributeValue) -> String {
251    use prax_schema::AttributeValue;
252
253    match value {
254        AttributeValue::String(s) => format!("\"{}\"", s),
255        AttributeValue::Int(i) => i.to_string(),
256        AttributeValue::Float(f) => f.to_string(),
257        AttributeValue::Boolean(b) => b.to_string(),
258        AttributeValue::Ident(id) => id.to_string(),
259        AttributeValue::Function(name, args) => {
260            if args.is_empty() {
261                format!("{}()", name)
262            } else {
263                let args_str = args
264                    .iter()
265                    .map(format_attribute_value)
266                    .collect::<Vec<_>>()
267                    .join(", ");
268                format!("{}({})", name, args_str)
269            }
270        }
271        AttributeValue::Array(items) => {
272            let items_str = items
273                .iter()
274                .map(format_attribute_value)
275                .collect::<Vec<_>>()
276                .join(", ");
277            format!("[{}]", items_str)
278        }
279        AttributeValue::FieldRef(name) => name.to_string(),
280        AttributeValue::FieldRefList(names) => {
281            let names_str = names
282                .iter()
283                .map(|n| n.to_string())
284                .collect::<Vec<_>>()
285                .join(", ");
286            format!("[{}]", names_str)
287        }
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn test_format_field_type() {
297        use prax_schema::{FieldType, ScalarType, TypeModifier};
298
299        assert_eq!(
300            format_field_type(
301                &FieldType::Scalar(ScalarType::String),
302                &TypeModifier::Required
303            ),
304            "String"
305        );
306
307        assert_eq!(
308            format_field_type(&FieldType::Scalar(ScalarType::Int), &TypeModifier::Optional),
309            "Int?"
310        );
311
312        assert_eq!(
313            format_field_type(&FieldType::Scalar(ScalarType::String), &TypeModifier::List),
314            "String[]"
315        );
316    }
317}