1use 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
13pub 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 if !args.input.exists() {
30 return Err(CliError::Config(format!(
31 "Input not found: {}",
32 args.input.display()
33 )));
34 }
35
36 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 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 let schema_text = format_schema(&prax_schema);
82
83 if args.print {
85 println!("{}", schema_text);
87 } else {
88 let output_path = args.output.unwrap_or_else(|| {
90 std::path::PathBuf::from(SCHEMA_FILE_PATH)
92 });
93
94 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 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 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
125fn 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
133fn 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
141fn 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
160fn format_schema(schema: &prax_schema::Schema) -> String {
165 let mut output = String::new();
166
167 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 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 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 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 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 for attr in &field.attributes {
227 output.push_str(&format!(" @{}", attr.name.as_str()));
228
229 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 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
275fn 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
290fn 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}