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 {
#[arg(value_name = "FILE")]
file: String,
#[arg(short, long)]
output: Option<String>,
#[arg(short, long)]
compact: bool,
#[arg(long)]
no_trailing_commas: bool,
#[arg(long)]
sort_keys: bool,
#[arg(long, default_value = "2")]
indent_size: usize,
},
Lint {
#[arg(value_name = "FILES")]
files: Vec<String>,
#[arg(long)]
no_error_exit: bool,
},
Convert {
#[arg(value_name = "FILE")]
file: String,
#[arg(short, long)]
output: Option<String>,
#[arg(long, conflicts_with = "from_json")]
to_json: bool,
#[arg(long, conflicts_with = "to_json")]
from_json: bool,
#[arg(short, long)]
compact: bool,
},
Validate {
#[arg(value_name = "FILES")]
files: Vec<String>,
#[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)?;
let value = parse(&input_content)
.with_context(|| format!("Failed to parse RSON from {}", file))?;
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 {
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 {
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 }
}
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 } => {
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) => {
let json_arr: Result<Vec<_>> = values.into_iter()
.map(rson_to_json)
.collect();
Ok(Value::Array(json_arr?))
}
RsonValue::Enum { name: _, variant, value } => {
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),
}
}
}
}