Skip to main content

warcraft_rs/commands/
dbc.rs

1//! DBC database command implementations
2
3use anyhow::{Context, Result};
4use clap::Subcommand;
5use std::fs::File;
6use std::io::{self, BufReader, BufWriter};
7use std::path::{Path, PathBuf};
8use std::time::Instant;
9use wow_cdbc::{
10    DbcParser, RecordSet, SchemaDefinition, SchemaDiscoverer, Value, export_to_csv, export_to_json,
11};
12
13#[cfg(feature = "yaml")]
14use wow_cdbc::FieldType;
15
16#[derive(Subcommand)]
17pub enum DbcCommands {
18    /// Show information about a DBC file
19    Info {
20        /// Path to the DBC file
21        file: PathBuf,
22    },
23
24    /// Validate a DBC file against a schema
25    Validate {
26        /// Path to the DBC file
27        file: PathBuf,
28
29        /// Path to the schema YAML file
30        #[arg(short, long)]
31        schema: PathBuf,
32    },
33
34    /// List records in a DBC file
35    List {
36        /// Path to the DBC file
37        file: PathBuf,
38
39        /// Path to the schema YAML file (optional)
40        #[arg(short, long)]
41        schema: Option<PathBuf>,
42
43        /// Maximum number of records to display
44        #[arg(short, long, default_value_t = 10)]
45        limit: usize,
46    },
47
48    /// Export DBC data to various formats
49    Export {
50        /// Path to the DBC file
51        file: PathBuf,
52
53        /// Path to the schema YAML file
54        #[arg(short, long)]
55        schema: PathBuf,
56
57        /// Output format (json, csv)
58        #[arg(short, long, value_enum, default_value = "json")]
59        format: ExportFormat,
60
61        /// Output file (stdout if not specified)
62        #[arg(short, long)]
63        output: Option<PathBuf>,
64    },
65
66    /// Analyze a DBC file for performance and structure
67    Analyze {
68        /// Path to the DBC file
69        file: PathBuf,
70
71        /// Path to the schema YAML file (optional)
72        #[arg(short, long)]
73        schema: Option<PathBuf>,
74
75        /// Use memory-mapped file (requires mmap feature)
76        #[arg(long)]
77        mmap: bool,
78
79        /// Use lazy loading (requires mmap feature)
80        #[arg(long)]
81        lazy: bool,
82
83        /// Enable string caching for performance
84        #[arg(long)]
85        cache_strings: bool,
86
87        /// Create sorted key map for binary search
88        #[arg(long)]
89        sorted_keys: bool,
90    },
91
92    /// Discover the schema of a DBC file through analysis
93    Discover {
94        /// Path to the DBC file
95        file: PathBuf,
96
97        /// Maximum number of records to analyze (0 = all)
98        #[arg(short, long, default_value_t = 100)]
99        max_records: u32,
100
101        /// Whether to validate string references
102        #[arg(long, default_value_t = true)]
103        validate_strings: bool,
104
105        /// Whether to detect array fields
106        #[arg(long, default_value_t = true)]
107        detect_arrays: bool,
108
109        /// Whether to detect the key field
110        #[arg(long, default_value_t = true)]
111        detect_key: bool,
112
113        /// Output schema as YAML format
114        #[arg(short, long)]
115        yaml: bool,
116
117        /// Output file path for the generated schema
118        #[arg(short, long)]
119        output: Option<PathBuf>,
120    },
121}
122
123#[derive(Clone, Copy, Debug, clap::ValueEnum)]
124pub enum ExportFormat {
125    Json,
126    Csv,
127}
128
129pub fn execute(command: DbcCommands) -> Result<()> {
130    match command {
131        DbcCommands::Info { file } => info_command(&file),
132        DbcCommands::List {
133            file,
134            schema,
135            limit,
136        } => list_command(&file, schema.as_deref(), limit),
137        DbcCommands::Export {
138            file,
139            schema,
140            format,
141            output,
142        } => export_command(&file, &schema, format, output.as_deref()),
143        DbcCommands::Analyze {
144            file,
145            schema,
146            mmap,
147            lazy,
148            cache_strings,
149            sorted_keys,
150        } => analyze_command(
151            &file,
152            schema.as_deref(),
153            mmap,
154            lazy,
155            cache_strings,
156            sorted_keys,
157        ),
158        DbcCommands::Validate { file, schema } => validate_command(&file, &schema),
159        DbcCommands::Discover {
160            file,
161            max_records,
162            validate_strings,
163            detect_arrays,
164            detect_key,
165            yaml,
166            output,
167        } => discover_command(
168            &file,
169            max_records,
170            validate_strings,
171            detect_arrays,
172            detect_key,
173            yaml,
174            output.as_deref(),
175        ),
176    }
177}
178
179/// Display information about a DBC file
180fn info_command(file: &Path) -> Result<()> {
181    let dbc_file =
182        File::open(file).with_context(|| format!("Failed to open DBC file: {}", file.display()))?;
183    let mut reader = BufReader::new(dbc_file);
184
185    let parser = DbcParser::parse(&mut reader)
186        .with_context(|| format!("Failed to parse DBC file: {}", file.display()))?;
187    let header = parser.header();
188
189    println!("DBC File Information");
190    println!("===================");
191    println!();
192    println!("File: {}", file.display());
193    println!(
194        "Magic: {} ({})",
195        std::str::from_utf8(&header.magic).unwrap_or("Invalid"),
196        header
197            .magic
198            .iter()
199            .map(|b| format!("{b:02X}"))
200            .collect::<Vec<_>>()
201            .join(" ")
202    );
203    println!("Version: {:?}", parser.version());
204    println!("Record Count: {}", header.record_count);
205    println!("Field Count: {}", header.field_count);
206    println!("Record Size: {} bytes", header.record_size);
207    println!("String Block Size: {} bytes", header.string_block_size);
208    println!("Total File Size: {} bytes", header.total_size());
209
210    // Parse raw records (no schema) to show sample data
211    let record_set = parser.parse_records().context("Failed to parse records")?;
212
213    println!();
214    println!("String Block Stats:");
215    println!("  Offset: {}", header.string_block_offset());
216    println!("  Size: {}", header.string_block_size);
217
218    // Show sample record
219    if let Some(record) = record_set.get_record(0) {
220        println!();
221        println!("Sample Record (First Record - Raw Values):");
222        println!("=========================================");
223        for (i, value) in record.values().iter().enumerate() {
224            match value {
225                Value::UInt32(v) => println!("  Field {i:2}: {v:10} (UInt32)"),
226                Value::Int32(v) => println!("  Field {i:2}: {v:10} (Int32)"),
227                Value::Float32(v) => println!("  Field {i:2}: {v:10.4} (Float32)"),
228                Value::StringRef(v) => match record_set.get_string(*v) {
229                    Ok(s) => println!("  Field {i:2}: \"{s}\" (String)"),
230                    Err(_) => println!(
231                        "  Field {:2}: <Invalid string ref: {}> (String)",
232                        i,
233                        v.offset()
234                    ),
235                },
236                Value::Bool(v) => println!("  Field {i:2}: {v:10} (Bool)"),
237                Value::UInt8(v) => println!("  Field {i:2}: {v:10} (UInt8)"),
238                Value::Int8(v) => println!("  Field {i:2}: {v:10} (Int8)"),
239                Value::UInt16(v) => println!("  Field {i:2}: {v:10} (UInt16)"),
240                Value::Int16(v) => println!("  Field {i:2}: {v:10} (Int16)"),
241                Value::Array(vals) => {
242                    println!("  Field {:2}: Array[{}]", i, vals.len());
243                    for (j, val) in vals.iter().enumerate().take(3) {
244                        println!("            [{j}]: {val:?}");
245                    }
246                    if vals.len() > 3 {
247                        println!("            ... {} more items", vals.len() - 3);
248                    }
249                }
250            }
251        }
252    }
253
254    Ok(())
255}
256
257/// List records from a DBC file
258fn list_command(file: &Path, schema_path: Option<&Path>, limit: usize) -> Result<()> {
259    let dbc_file =
260        File::open(file).with_context(|| format!("Failed to open DBC file: {}", file.display()))?;
261    let mut reader = BufReader::new(dbc_file);
262
263    let mut parser = DbcParser::parse(&mut reader)
264        .with_context(|| format!("Failed to parse DBC file: {}", file.display()))?;
265
266    // Load schema if provided
267    if let Some(schema_path) = schema_path {
268        let schema_def = SchemaDefinition::from_yaml(schema_path).map_err(|e| {
269            anyhow::anyhow!("Failed to load schema {}: {}", schema_path.display(), e)
270        })?;
271        let schema = schema_def
272            .to_schema()
273            .map_err(|e| anyhow::anyhow!("Failed to convert schema definition: {}", e))?;
274        parser = parser
275            .with_schema(schema)
276            .context("Failed to apply schema")?;
277    }
278
279    let record_set = parser.parse_records().context("Failed to parse records")?;
280
281    println!("DBC Records");
282    println!("===========");
283    println!("Total records: {}", record_set.len());
284    println!("Showing first {} records:", limit.min(record_set.len()));
285    println!();
286
287    for i in 0..limit.min(record_set.len()) {
288        if let Some(record) = record_set.get_record(i) {
289            println!("Record {i}:");
290            if let Some(schema) = record.schema() {
291                // With schema - show field names
292                for (field_idx, field) in schema.fields.iter().enumerate() {
293                    if let Some(value) = record.get_value(field_idx) {
294                        print!("  {}: ", field.name);
295                        print_value(value, &record_set)?;
296                    }
297                }
298            } else {
299                // Without schema - show field indices
300                for (field_idx, value) in record.values().iter().enumerate() {
301                    print!("  Field {field_idx}: ");
302                    print_value(value, &record_set)?;
303                }
304            }
305            println!();
306        }
307    }
308
309    if record_set.len() > limit {
310        println!("... {} more records", record_set.len() - limit);
311    }
312
313    Ok(())
314}
315
316/// Export DBC data to file or stdout
317fn export_command(
318    file: &Path,
319    schema_path: &Path,
320    format: ExportFormat,
321    output_path: Option<&Path>,
322) -> Result<()> {
323    let dbc_file =
324        File::open(file).with_context(|| format!("Failed to open DBC file: {}", file.display()))?;
325    let mut reader = BufReader::new(dbc_file);
326
327    // Load schema
328    let schema_def = SchemaDefinition::from_yaml(schema_path)
329        .map_err(|e| anyhow::anyhow!("Failed to load schema {}: {}", schema_path.display(), e))?;
330    let schema = schema_def
331        .to_schema()
332        .map_err(|e| anyhow::anyhow!("Failed to convert schema definition: {}", e))?;
333
334    // Parse DBC file with schema
335    let parser = DbcParser::parse(&mut reader)
336        .with_context(|| format!("Failed to parse DBC file: {}", file.display()))?;
337    let parser = parser
338        .with_schema(schema)
339        .context("Failed to apply schema")?;
340    let record_set = parser.parse_records().context("Failed to parse records")?;
341
342    // Export to output
343    match output_path {
344        Some(path) => {
345            let output_file = File::create(path)
346                .with_context(|| format!("Failed to create output file: {}", path.display()))?;
347            let writer = BufWriter::new(output_file);
348
349            match format {
350                ExportFormat::Json => {
351                    export_to_json(&record_set, writer).context("Failed to export to JSON")?;
352                }
353                ExportFormat::Csv => {
354                    export_to_csv(&record_set, writer).context("Failed to export to CSV")?;
355                }
356            }
357
358            println!(
359                "Exported {} records to {}: {}",
360                record_set.len(),
361                match format {
362                    ExportFormat::Json => "JSON",
363                    ExportFormat::Csv => "CSV",
364                },
365                path.display()
366            );
367        }
368        None => {
369            // Export to stdout
370            let stdout = io::stdout();
371            let writer = stdout.lock();
372
373            match format {
374                ExportFormat::Json => {
375                    export_to_json(&record_set, writer).context("Failed to export to JSON")?;
376                }
377                ExportFormat::Csv => {
378                    export_to_csv(&record_set, writer).context("Failed to export to CSV")?;
379                }
380            }
381        }
382    }
383
384    Ok(())
385}
386
387/// Analyze DBC file performance and structure
388fn analyze_command(
389    file: &Path,
390    schema_path: Option<&Path>,
391    mmap: bool,
392    lazy: bool,
393    cache_strings: bool,
394    sorted_keys: bool,
395) -> Result<()> {
396    println!("Analyzing DBC file: {}", file.display());
397    println!();
398
399    let start = Instant::now();
400
401    if mmap || lazy {
402        anyhow::bail!(
403            "Memory-mapped file support is not available in the current build.\n\
404             The --mmap and --lazy flags require additional build configuration."
405        );
406    }
407
408    let dbc_file =
409        File::open(file).with_context(|| format!("Failed to open DBC file: {}", file.display()))?;
410    let mut reader = BufReader::new(dbc_file);
411
412    let parser = DbcParser::parse(&mut reader)
413        .with_context(|| format!("Failed to parse DBC file: {}", file.display()))?;
414    println!("  Header parsed in: {:?}", start.elapsed());
415
416    let schema_obj = if let Some(schema_path) = schema_path {
417        println!("Loading schema: {}", schema_path.display());
418        let schema_def = SchemaDefinition::from_yaml(schema_path).map_err(|e| {
419            anyhow::anyhow!("Failed to load schema {}: {}", schema_path.display(), e)
420        })?;
421        Some(
422            schema_def
423                .to_schema()
424                .map_err(|e| anyhow::anyhow!("Failed to convert schema definition: {}", e))?,
425        )
426    } else {
427        None
428    };
429
430    if let Some(schema_obj) = schema_obj {
431        let parser = parser
432            .with_schema(schema_obj)
433            .context("Failed to apply schema")?;
434        println!("  Schema applied in: {:?}", start.elapsed());
435
436        let mut record_set = parser.parse_records().context("Failed to parse records")?;
437        println!("  Records parsed in: {:?}", start.elapsed());
438
439        if cache_strings {
440            println!();
441            println!("Enabling string caching");
442            record_set.enable_string_caching();
443            println!("  String cache built in: {:?}", start.elapsed());
444        }
445
446        if sorted_keys {
447            println!();
448            println!("Creating sorted key map");
449            record_set
450                .create_sorted_key_map()
451                .context("Failed to create sorted key map")?;
452            println!("  Sorted key map created in: {:?}", start.elapsed());
453        }
454
455        println!();
456        println!("Total records: {}", record_set.len());
457    } else {
458        let record_set = parser.parse_records().context("Failed to parse records")?;
459        println!("  Records parsed in: {:?}", start.elapsed());
460        println!();
461        println!("Total records: {}", record_set.len());
462    }
463
464    println!();
465    println!("Total time: {:?}", start.elapsed());
466
467    Ok(())
468}
469
470/// Validate a DBC file against a schema
471fn validate_command(file: &Path, schema_path: &Path) -> Result<()> {
472    let dbc_file =
473        File::open(file).with_context(|| format!("Failed to open DBC file: {}", file.display()))?;
474    let mut reader = BufReader::new(dbc_file);
475
476    // Load schema
477    let schema_def = SchemaDefinition::from_yaml(schema_path)
478        .map_err(|e| anyhow::anyhow!("Failed to load schema {}: {}", schema_path.display(), e))?;
479    let schema = schema_def
480        .to_schema()
481        .map_err(|e| anyhow::anyhow!("Failed to convert schema definition: {}", e))?;
482
483    // Parse DBC file
484    let parser = DbcParser::parse(&mut reader)
485        .with_context(|| format!("Failed to parse DBC file: {}", file.display()))?;
486
487    println!("Validating DBC file against schema");
488    println!("==================================");
489    println!();
490    println!("File: {}", file.display());
491    println!("Schema: {}", schema_path.display());
492    println!();
493
494    // Apply schema (this validates it)
495    match parser.with_schema(schema) {
496        Ok(parser) => {
497            println!("✓ Schema validation passed");
498            println!();
499
500            // Try to parse records to ensure they can be read
501            println!("Parsing records...");
502            match parser.parse_records() {
503                Ok(record_set) => {
504                    println!("✓ Successfully parsed {} records", record_set.len());
505
506                    // Validate string references
507                    let mut invalid_strings = 0;
508                    for i in 0..record_set.len() {
509                        if let Some(record) = record_set.get_record(i) {
510                            for value in record.values() {
511                                if let Value::StringRef(str_ref) = value
512                                    && record_set.get_string(*str_ref).is_err()
513                                {
514                                    invalid_strings += 1;
515                                }
516                            }
517                        }
518                    }
519
520                    if invalid_strings > 0 {
521                        println!("⚠ Found {invalid_strings} invalid string references");
522                    } else {
523                        println!("✓ All string references are valid");
524                    }
525                }
526                Err(e) => {
527                    println!("✗ Failed to parse records: {e}");
528                    return Err(e.into());
529                }
530            }
531        }
532        Err(e) => {
533            println!("✗ Schema validation failed: {e}");
534            return Err(anyhow::anyhow!("Schema validation failed: {}", e));
535        }
536    }
537
538    println!();
539    println!("Validation complete!");
540
541    Ok(())
542}
543
544/// Helper function to print a value
545fn print_value(value: &Value, record_set: &RecordSet) -> Result<()> {
546    match value {
547        Value::UInt32(v) => println!("{v}"),
548        Value::Int32(v) => println!("{v}"),
549        Value::Float32(v) => println!("{v:.4}"),
550        Value::StringRef(v) => match record_set.get_string(*v) {
551            Ok(s) => println!("\"{s}\""),
552            Err(_) => println!("<Invalid string ref: {}>", v.offset()),
553        },
554        Value::Bool(v) => println!("{v}"),
555        Value::UInt8(v) => println!("{v}"),
556        Value::Int8(v) => println!("{v}"),
557        Value::UInt16(v) => println!("{v}"),
558        Value::Int16(v) => println!("{v}"),
559        Value::Array(vals) => {
560            print!("[");
561            for (i, val) in vals.iter().enumerate() {
562                if i > 0 {
563                    print!(", ");
564                }
565                match val {
566                    Value::UInt32(v) => print!("{v}"),
567                    Value::Int32(v) => print!("{v}"),
568                    Value::Float32(v) => print!("{v:.4}"),
569                    _ => print!("{val:?}"),
570                }
571            }
572            println!("]");
573        }
574    }
575    Ok(())
576}
577
578/// Discover the schema of a DBC file through analysis
579fn discover_command(
580    file: &Path,
581    max_records: u32,
582    validate_strings: bool,
583    detect_arrays: bool,
584    detect_key: bool,
585    yaml: bool,
586    output_path: Option<&Path>,
587) -> Result<()> {
588    println!("Discovering schema for: {}", file.display());
589    println!();
590
591    // Open and parse the DBC file
592    let start = Instant::now();
593    let dbc_file =
594        File::open(file).with_context(|| format!("Failed to open DBC file: {}", file.display()))?;
595    let mut reader = BufReader::new(dbc_file);
596
597    let parser = DbcParser::parse(&mut reader)
598        .with_context(|| format!("Failed to parse DBC file: {}", file.display()))?;
599    println!("File parsed in: {:?}", start.elapsed());
600
601    // Print header information
602    let header = parser.header();
603    println!("DBC Header Information:");
604    println!("======================");
605    println!(
606        "Magic: {} ({:02X?})",
607        std::str::from_utf8(&header.magic).unwrap_or("Invalid"),
608        header.magic
609    );
610    println!("Version: {:?}", parser.version());
611    println!("Record Count: {}", header.record_count);
612    println!("Field Count: {}", header.field_count);
613    println!("Record Size: {} bytes", header.record_size);
614    println!("String Block Size: {} bytes", header.string_block_size);
615    println!();
616
617    // Parse records to get the string block
618    let record_set = parser.parse_records().context("Failed to parse records")?;
619    println!("Records parsed in: {:?}", start.elapsed());
620
621    // Create schema discoverer
622    let discoverer = SchemaDiscoverer::new(header, parser.data(), record_set.string_block())
623        .with_max_records(max_records)
624        .with_validate_strings(validate_strings)
625        .with_detect_arrays(detect_arrays)
626        .with_detect_key(detect_key);
627
628    // Discover schema
629    let start = Instant::now();
630    let discovered = discoverer.discover().context("Failed to discover schema")?;
631    println!("Schema discovered in: {:?}", start.elapsed());
632    println!();
633
634    // Check validation status
635    println!("Schema Validation:");
636    println!("==================");
637    if discovered.is_valid {
638        println!("✓ Schema is valid!");
639    } else {
640        println!(
641            "✗ Schema validation failed: {}",
642            discovered
643                .validation_message
644                .unwrap_or_else(|| "Unknown validation error".to_string())
645        );
646    }
647    println!();
648
649    // Print discovered fields
650    println!("Discovered Fields:");
651    println!("==================");
652    for (i, field) in discovered.fields.iter().enumerate() {
653        println!(
654            "Field {:2}: {:8} (confidence: {:?})",
655            i,
656            format!("{:?}", field.field_type),
657            field.confidence
658        );
659
660        if field.is_array {
661            println!("           Array size: {}", field.array_size.unwrap_or(0));
662        }
663
664        if field.is_key_candidate {
665            println!("           ⚷ Key candidate");
666        }
667
668        if field.is_locstring {
669            let locale_names = [
670                "enUS", "koKR", "frFR", "deDE", "zhCN", "zhTW", "esES", "esMX", "flags",
671            ];
672            if let Some(idx) = field.locstring_index {
673                let name = locale_names.get(idx as usize).unwrap_or(&"?");
674                println!("           🌐 Locstring ({})", name);
675            }
676        }
677
678        // Print sample values
679        let samples: Vec<String> = field
680            .sample_values
681            .iter()
682            .take(5)
683            .map(|v| v.to_string())
684            .collect();
685        println!("           Sample values: [{}]", samples.join(", "));
686    }
687    println!();
688
689    // Print key field
690    if let Some(key_index) = discovered.key_field_index {
691        println!(
692            "Key Field: Field {} ({})",
693            key_index,
694            if key_index < discovered.fields.len() {
695                format!("{:?}", discovered.fields[key_index].field_type)
696            } else {
697                "Unknown".to_string()
698            }
699        );
700    } else {
701        println!("Key Field: None detected");
702    }
703    println!();
704
705    // Generate schema
706    let file_stem = file.file_stem().unwrap_or_default().to_string_lossy();
707    let schema = discoverer
708        .generate_schema(&file_stem)
709        .context("Failed to generate schema")?;
710
711    // Output schema
712    if yaml {
713        #[cfg(feature = "yaml")]
714        {
715            use serde::{Deserialize, Serialize};
716
717            // Convert to YAML-compatible structures
718            #[derive(Serialize, Deserialize)]
719            struct YamlSchemaField {
720                name: String,
721                type_name: String,
722                #[serde(skip_serializing_if = "Option::is_none")]
723                is_array: Option<bool>,
724                #[serde(skip_serializing_if = "Option::is_none")]
725                array_size: Option<usize>,
726            }
727
728            #[derive(Serialize, Deserialize)]
729            struct YamlSchema {
730                name: String,
731                #[serde(skip_serializing_if = "Option::is_none")]
732                key_field: Option<String>,
733                fields: Vec<YamlSchemaField>,
734            }
735
736            let mut fields = Vec::new();
737            for field in schema.fields.iter() {
738                let type_name = match field.field_type {
739                    FieldType::Int32 => "Int32",
740                    FieldType::UInt32 => "UInt32",
741                    FieldType::Float32 => "Float32",
742                    FieldType::String => "String",
743                    FieldType::Bool => "Bool",
744                    FieldType::UInt8 => "UInt8",
745                    FieldType::Int8 => "Int8",
746                    FieldType::UInt16 => "UInt16",
747                    FieldType::Int16 => "Int16",
748                };
749
750                fields.push(YamlSchemaField {
751                    name: field.name.clone(),
752                    type_name: type_name.to_string(),
753                    is_array: if field.is_array { Some(true) } else { None },
754                    array_size: field.array_size,
755                });
756            }
757
758            let key_field = if let Some(key_index) = schema.key_field_index {
759                if key_index < schema.fields.len() {
760                    Some(schema.fields[key_index].name.clone())
761                } else {
762                    None
763                }
764            } else {
765                None
766            };
767
768            let yaml_schema = YamlSchema {
769                name: schema.name.clone(),
770                key_field,
771                fields,
772            };
773
774            let yaml_content = serde_yaml_ng::to_string(&yaml_schema)
775                .context("Failed to serialize schema to YAML")?;
776
777            // Output to file or stdout
778            if let Some(output_path) = output_path {
779                std::fs::write(output_path, yaml_content).with_context(|| {
780                    format!("Failed to write schema to: {}", output_path.display())
781                })?;
782                println!("Schema written to: {}", output_path.display());
783            } else {
784                println!("Generated Schema (YAML):");
785                println!("========================");
786                println!("{yaml_content}");
787            }
788        }
789
790        #[cfg(not(feature = "yaml"))]
791        {
792            anyhow::bail!(
793                "YAML output requested but yaml feature is not enabled. Rebuild with --features yaml to enable YAML support."
794            );
795        }
796    } else {
797        // Output schema in text format
798        println!("Generated Schema:");
799        println!("=================");
800        println!("Name: {}", schema.name);
801
802        if let Some(key_index) = schema.key_field_index
803            && key_index < schema.fields.len()
804        {
805            println!("Key Field: {}", schema.fields[key_index].name);
806        }
807
808        println!("Fields:");
809
810        for (i, field) in schema.fields.iter().enumerate() {
811            let field_desc = if field.is_array {
812                format!(
813                    "{}: {:?}[{}]",
814                    field.name,
815                    field.field_type,
816                    field.array_size.unwrap_or(0)
817                )
818            } else {
819                format!("{}: {:?}", field.name, field.field_type)
820            };
821
822            if Some(i) == schema.key_field_index {
823                println!("  {field_desc} (Key field)");
824            } else {
825                println!("  {field_desc}");
826            }
827        }
828
829        // Write schema to file if requested
830        if let Some(output_path) = output_path {
831            let mut contents = format!("Schema: {}\n", schema.name);
832
833            if let Some(key_index) = schema.key_field_index
834                && key_index < schema.fields.len()
835            {
836                contents.push_str(&format!("Key Field: {}\n", schema.fields[key_index].name));
837            }
838
839            contents.push_str("Fields:\n");
840
841            for (i, field) in schema.fields.iter().enumerate() {
842                let field_desc = if field.is_array {
843                    format!(
844                        "  {}: {:?}[{}]\n",
845                        field.name,
846                        field.field_type,
847                        field.array_size.unwrap_or(0)
848                    )
849                } else {
850                    format!("  {}: {:?}\n", field.name, field.field_type)
851                };
852
853                contents.push_str(&field_desc);
854
855                if Some(i) == schema.key_field_index {
856                    contents.push_str("    (Key field)\n");
857                }
858            }
859
860            std::fs::write(output_path, contents)
861                .with_context(|| format!("Failed to write schema to: {}", output_path.display()))?;
862            println!("\nSchema written to: {}", output_path.display());
863        }
864    }
865
866    Ok(())
867}