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