use clap::{Parser, Subcommand};
use indexmap::IndexMap;
use std::path::PathBuf;
use zoko_parser::{ZokoFile, parse_zoko, parse_zoko_to_json};
#[derive(Parser)]
#[command(name = "zoko")]
#[command(about = "Zoko - A JSON-like format for data storing", long_about = None)]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Parse {
#[arg(value_name = "FILE")]
file: PathBuf,
#[arg(short, long, value_name = "FILE")]
output: Option<PathBuf>,
#[arg(short, long, default_value_t = true)]
pretty: bool,
},
Validate {
#[arg(value_name = "FILE")]
file: PathBuf,
},
Fmt {
#[arg(value_name = "FILE")]
file: PathBuf,
#[arg(short, long, value_name = "FILE")]
output: Option<PathBuf>,
#[arg(short, long)]
check: bool,
},
Check {
#[arg(value_name = "FILE")]
file: PathBuf,
},
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
match cli.command {
Commands::Parse {
file,
output,
pretty,
} => {
let content = std::fs::read_to_string(&file)
.map_err(|e| format!("Failed to read file {}: {}", file.display(), e))?;
let json = parse_zoko_to_json(&content).map_err(|e| format!("Parse error: {}", e))?;
let output_json = if pretty {
json
} else {
let parsed: serde_json::Value = serde_json::from_str(&json)?;
serde_json::to_string(&parsed)?
};
match output {
Some(path) => std::fs::write(&path, output_json)
.map_err(|e| format!("Failed to write output: {}", e))?,
None => println!("{}", output_json),
}
}
Commands::Validate { file } => {
let content = std::fs::read_to_string(&file)
.map_err(|e| format!("Failed to read file {}: {}", file.display(), e))?;
parse_zoko(&content).map_err(|e| format!("Validation failed: {}", e))?;
println!("✓ File is valid: {}", file.display());
}
Commands::Fmt {
file,
output,
check,
} => {
let content = std::fs::read_to_string(&file)
.map_err(|e| format!("Failed to read file {}: {}", file.display(), e))?;
let zoko: ZokoFile = parse_zoko(&content).map_err(|e| format!("Parse error: {}", e))?;
let formatted = format_zoko(&zoko);
if check {
if content.trim() == formatted.trim() {
println!("✓ File is properly formatted: {}", file.display());
} else {
eprintln!("✗ File is not properly formatted: {}", file.display());
std::process::exit(1);
}
} else {
let output_path = output.unwrap_or(file.clone());
std::fs::write(&output_path, formatted)
.map_err(|e| format!("Failed to write file: {}", e))?;
println!("✓ Formatted: {}", output_path.display());
}
}
Commands::Check { file } => {
let content = std::fs::read_to_string(&file)
.map_err(|e| format!("Failed to read file {}: {}", file.display(), e))?;
parse_zoko(&content).map_err(|e| {
eprintln!("✗ Check failed: {}", e);
std::process::exit(1);
})?;
println!("✓ Check passed: {}", file.display());
}
}
Ok(())
}
fn format_zoko(zoko: &ZokoFile) -> String {
let mut output = String::new();
format_entries(&zoko.entries, 0, &mut output);
output
}
fn format_entries(
entries: &IndexMap<String, zoko_parser::Value>,
indent: usize,
output: &mut String,
) {
let indent_str = " ".repeat(indent);
for (key, value) in entries {
output.push_str(&indent_str);
output.push_str(key);
output.push_str(": ");
format_value(value, indent, output);
output.push_str(",\n");
}
}
fn format_value(value: &zoko_parser::Value, indent: usize, output: &mut String) {
match value {
zoko_parser::Value::String(s) => {
if s.contains('\n') {
output.push('`');
output.push_str(s);
output.push('`');
} else if s.contains('"') {
output.push('\'');
output.push_str(s);
output.push('\'');
} else {
output.push('"');
output.push_str(s);
output.push('"');
}
}
zoko_parser::Value::Number(n) => {
if n.fract() == 0.0 && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
output.push_str(&format!("{}", *n as i64));
} else {
output.push_str(&n.to_string());
}
}
zoko_parser::Value::Boolean(b) => output.push_str(if *b { "true" } else { "false" }),
zoko_parser::Value::Null => output.push_str("null"),
zoko_parser::Value::Array(arr) => {
if arr.is_empty() {
output.push_str("[]");
} else if arr.iter().all(|v| {
matches!(
v,
zoko_parser::Value::String(_)
| zoko_parser::Value::Number(_)
| zoko_parser::Value::Boolean(_)
| zoko_parser::Value::Null
)
}) && arr.len() <= 3
{
output.push('[');
for (i, v) in arr.iter().enumerate() {
if i > 0 {
output.push_str(", ");
}
format_value(v, indent, output);
}
output.push(']');
} else {
output.push_str("[\n");
let next_indent = indent + 1;
let indent_str = " ".repeat(next_indent);
for v in arr {
output.push_str(&indent_str);
format_value(v, next_indent, output);
output.push_str(",\n");
}
output.push_str(&" ".repeat(indent));
output.push(']');
}
}
zoko_parser::Value::Object(obj) => {
if obj.is_empty() {
output.push_str("{}");
} else {
output.push_str("{\n");
let next_indent = indent + 1;
format_entries(obj, next_indent, output);
output.push_str(&" ".repeat(indent));
output.push('}');
}
}
}
}