rson-cli 1.0.0

Command-line tools for RSON
//! RSON CLI Tool (rsonc)
//!
//! Command-line tool for working with RSON files.

use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use colored::*;
use rson_core::{parse, format_pretty, format_compact, FormatOptions};
use std::fs;
use std::io::{self, Read, Write};

#[derive(Parser)]
#[command(name = "rsonc")]
#[command(about = "RSON (Rust Serialized Object Notation) CLI tool")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(long_about = "A command-line tool for formatting, linting, and converting RSON files")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Format RSON files
    Format {
        /// Input file (use - for stdin)
        #[arg(value_name = "FILE")]
        file: String,
        
        /// Output file (use - for stdout)
        #[arg(short, long)]
        output: Option<String>,
        
        /// Use compact formatting
        #[arg(short, long)]
        compact: bool,
        
        /// Disable trailing commas
        #[arg(long)]
        no_trailing_commas: bool,
        
        /// Sort map keys
        #[arg(long)]
        sort_keys: bool,
        
        /// Indent size
        #[arg(long, default_value = "2")]
        indent_size: usize,
    },
    
    /// Lint RSON files for syntax errors
    Lint {
        /// Input files to check
        #[arg(value_name = "FILES")]
        files: Vec<String>,
        
        /// Exit with code 0 even if errors found
        #[arg(long)]
        no_error_exit: bool,
    },
    
    /// Convert between RSON and JSON
    Convert {
        /// Input file (use - for stdin)
        #[arg(value_name = "FILE")]
        file: String,
        
        /// Output file (use - for stdout)
        #[arg(short, long)]
        output: Option<String>,
        
        /// Convert to JSON format
        #[arg(long, conflicts_with = "from_json")]
        to_json: bool,
        
        /// Convert from JSON format
        #[arg(long, conflicts_with = "to_json")]
        from_json: bool,
        
        /// Use compact output formatting
        #[arg(short, long)]
        compact: bool,
    },
    
    /// Validate RSON syntax
    Validate {
        /// Input files to validate
        #[arg(value_name = "FILES")]
        files: Vec<String>,
        
        /// Verbose output
        #[arg(short, long)]
        verbose: bool,
    },
}

fn main() -> Result<()> {
    let cli = Cli::parse();
    
    match cli.command {
        Commands::Format { 
            file, 
            output, 
            compact, 
            no_trailing_commas, 
            sort_keys, 
            indent_size 
        } => {
            cmd_format(file, output, compact, !no_trailing_commas, sort_keys, indent_size)
        }
        Commands::Lint { files, no_error_exit } => {
            cmd_lint(files, no_error_exit)
        }
        Commands::Convert { file, output, to_json, from_json, compact } => {
            cmd_convert(file, output, to_json, from_json, compact)
        }
        Commands::Validate { files, verbose } => {
            cmd_validate(files, verbose)
        }
    }
}

fn cmd_format(
    file: String, 
    output: Option<String>, 
    compact: bool, 
    trailing_commas: bool, 
    sort_keys: bool, 
    indent_size: usize
) -> Result<()> {
    let input_content = read_input(&file)?;
    
    // Parse the RSON
    let value = parse(&input_content)
        .with_context(|| format!("Failed to parse RSON from {}", file))?;
    
    // Format with options
    let options = FormatOptions {
        compact,
        trailing_commas,
        sort_keys,
        indent_size,
        ..Default::default()
    };
    
    let formatted = rson_core::format_rson(&value, &options)
        .context("Failed to format RSON")?;
    
    write_output(&output, &formatted)?;
    
    if output.is_none() && file != "-" {
        eprintln!("{} Formatted {}", "✓".green(), file);
    }
    
    Ok(())
}

fn cmd_lint(files: Vec<String>, no_error_exit: bool) -> Result<()> {
    let mut has_errors = false;
    
    for file in files {
        print!("Checking {}... ", file);
        io::stdout().flush().unwrap();
        
        let content = read_input(&file)?;
        
        match parse(&content) {
            Ok(_) => {
                println!("{}", "OK".green());
            }
            Err(e) => {
                println!("{}", "ERROR".red());
                eprintln!("  {}: {}", "Error".red(), e);
                has_errors = true;
            }
        }
    }
    
    if has_errors {
        eprintln!("\n{} Some files had syntax errors", "✗".red());
        if !no_error_exit {
            std::process::exit(1);
        }
    } else {
        eprintln!("\n{} All files are valid", "✓".green());
    }
    
    Ok(())
}

fn cmd_convert(
    file: String, 
    output: Option<String>, 
    to_json: bool, 
    from_json: bool, 
    compact: bool
) -> Result<()> {
    let input_content = read_input(&file)?;
    
    if from_json {
        // Parse JSON and convert to RSON
        let json_value: serde_json::Value = serde_json::from_str(&input_content)
            .with_context(|| format!("Failed to parse JSON from {}", file))?;
        
        let rson_value = json_to_rson(json_value);
        
        let formatted = if compact {
            format_compact(&rson_value)?
        } else {
            format_pretty(&rson_value)?
        };
        
        write_output(&output, &formatted)?;
        
    } else if to_json {
        // Parse RSON and convert to JSON
        let rson_value = parse(&input_content)
            .with_context(|| format!("Failed to parse RSON from {}", file))?;
        
        let json_value = rson_to_json(rson_value)?;
        
        let formatted = if compact {
            serde_json::to_string(&json_value)?
        } else {
            serde_json::to_string_pretty(&json_value)?
        };
        
        write_output(&output, &formatted)?;
        
    } else {
        return Err(anyhow::anyhow!("Must specify either --to-json or --from-json"));
    }
    
    Ok(())
}

fn cmd_validate(files: Vec<String>, verbose: bool) -> Result<()> {
    let mut valid_count = 0;
    let mut invalid_count = 0;
    
    for file in files {
        let content = read_input(&file)?;
        
        match parse(&content) {
            Ok(value) => {
                valid_count += 1;
                if verbose {
                    println!("{}: {} (type: {})", 
                        file.green(), 
                        "valid".green(),
                        value.value_type()
                    );
                }
            }
            Err(e) => {
                invalid_count += 1;
                println!("{}: {} - {}", 
                    file.red(), 
                    "invalid".red(),
                    e
                );
            }
        }
    }
    
    println!("\nValidation complete:");
    println!("  {} valid files", valid_count.to_string().green());
    if invalid_count > 0 {
        println!("  {} invalid files", invalid_count.to_string().red());
        std::process::exit(1);
    }
    
    Ok(())
}

fn read_input(file: &str) -> Result<String> {
    if file == "-" {
        let mut buffer = String::new();
        io::stdin().read_to_string(&mut buffer)?;
        Ok(buffer)
    } else {
        fs::read_to_string(file)
            .with_context(|| format!("Failed to read file {}", file))
    }
}

fn write_output(output: &Option<String>, content: &str) -> Result<()> {
    match output {
        Some(file) if file != "-" => {
            fs::write(file, content)
                .with_context(|| format!("Failed to write to file {}", file))?;
        }
        _ => {
            print!("{}", content);
        }
    }
    Ok(())
}

fn json_to_rson(json: serde_json::Value) -> rson_core::RsonValue {
    use rson_core::RsonValue;
    use serde_json::Value;
    
    match json {
        Value::Null => RsonValue::Null,
        Value::Bool(b) => RsonValue::Bool(b),
        Value::Number(n) => {
            if let Some(i) = n.as_i64() {
                RsonValue::Int(i)
            } else if let Some(f) = n.as_f64() {
                RsonValue::Float(f)
            } else {
                RsonValue::Null // Fallback
            }
        }
        Value::String(s) => RsonValue::String(s),
        Value::Array(arr) => {
            RsonValue::Array(arr.into_iter().map(json_to_rson).collect())
        }
        Value::Object(map) => {
            let mut rson_map = indexmap::IndexMap::new();
            for (k, v) in map {
                rson_map.insert(k, json_to_rson(v));
            }
            RsonValue::Map(rson_map)
        }
    }
}

fn rson_to_json(rson: rson_core::RsonValue) -> Result<serde_json::Value> {
    use rson_core::RsonValue;
    use serde_json::Value;
    
    match rson {
        RsonValue::Null => Ok(Value::Null),
        RsonValue::Bool(b) => Ok(Value::Bool(b)),
        RsonValue::Int(i) => Ok(Value::Number(i.into())),
        RsonValue::Float(f) => {
            Ok(Value::Number(serde_json::Number::from_f64(f)
                .ok_or_else(|| anyhow::anyhow!("Invalid float value"))?))
        }
        RsonValue::String(s) => Ok(Value::String(s)),
        RsonValue::Char(c) => Ok(Value::String(c.to_string())),
        RsonValue::Array(arr) => {
            let json_arr: Result<Vec<_>> = arr.into_iter()
                .map(rson_to_json)
                .collect();
            Ok(Value::Array(json_arr?))
        }
        RsonValue::Map(map) => {
            let mut json_map = serde_json::Map::new();
            for (k, v) in map {
                json_map.insert(k, rson_to_json(v)?);
            }
            Ok(Value::Object(json_map))
        }
        RsonValue::Struct { name: _, fields } => {
            // Convert struct to JSON object (lose type name)
            let mut json_map = serde_json::Map::new();
            for (k, v) in fields {
                json_map.insert(k, rson_to_json(v)?);
            }
            Ok(Value::Object(json_map))
        }
        RsonValue::Tuple(values) => {
            // Convert tuple to JSON array
            let json_arr: Result<Vec<_>> = values.into_iter()
                .map(rson_to_json)
                .collect();
            Ok(Value::Array(json_arr?))
        }
        RsonValue::Enum { name: _, variant, value } => {
            // Convert enum to JSON representation
            if let Some(val) = value {
                let mut map = serde_json::Map::new();
                map.insert(variant, rson_to_json(*val)?);
                Ok(Value::Object(map))
            } else {
                Ok(Value::String(variant))
            }
        }
        RsonValue::Option(opt) => {
            match opt {
                Some(val) => rson_to_json(*val),
                None => Ok(Value::Null),
            }
        }
    }
}