mod parser;
mod engine;
mod tests;
use anyhow::{Context, Result, anyhow};
use clap::{Parser, ValueEnum};
use std::fs::File;
use std::io::{self, Read};
use std::path::PathBuf;
use serde_json::Value;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(index = 1)]
query: String,
#[arg(short, long)]
file: Option<PathBuf>,
#[arg(short = 'i', long, value_enum)]
input_format: Option<InputFormat>,
#[arg(short, long, value_enum, default_value_t = OutputFormat::Compact)]
output: OutputFormat,
}
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
enum InputFormat {
Json,
Yaml,
Toml,
}
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
enum OutputFormat {
Pretty,
Compact,
Raw,
}
fn main() -> Result<()> {
let cli = Cli::parse();
let input_format = determine_input_format(&cli.file, cli.input_format)?;
let input = read_input(&cli.file, input_format)?;
let query = parser::parse_query(&cli.query)
.context(format!("Failed to parse query: {}", cli.query))?;
let result = engine::apply_query(&input, &query)
.context("Failed to apply query")?;
match cli.output {
OutputFormat::Pretty => {
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize result")?);
},
OutputFormat::Compact => {
println!("{}", serde_json::to_string(&result)
.context("Failed to serialize result")?);
},
OutputFormat::Raw => {
match result {
Value::String(s) => println!("{}", s),
Value::Number(n) => println!("{}", n),
Value::Bool(b) => println!("{}", b),
Value::Null => println!("null"),
_ => println!("{}", serde_json::to_string(&result)
.context("Failed to serialize result")?),
}
},
}
Ok(())
}
fn determine_input_format(
file_path: &Option<PathBuf>,
explicit_format: Option<InputFormat>,
) -> Result<InputFormat> {
if let Some(format) = explicit_format {
return Ok(format);
}
if let Some(path) = file_path {
if let Some(ext) = path.extension() {
if let Some(ext_str) = ext.to_str() {
match ext_str.to_lowercase().as_str() {
"json" => return Ok(InputFormat::Json),
"yml" | "yaml" => return Ok(InputFormat::Yaml),
"toml" => return Ok(InputFormat::Toml),
_ => {}
}
}
}
}
Ok(InputFormat::Json)
}
fn read_input(file_path: &Option<PathBuf>, format: InputFormat) -> Result<Value> {
let input_text = if let Some(path) = file_path {
let mut file = File::open(path)
.context(format!("Failed to open file: {}", path.display()))?;
let mut content = String::new();
file.read_to_string(&mut content)
.context("Failed to read file")?;
content
} else {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)
.context("Failed to read from stdin")?;
buffer
};
match format {
InputFormat::Json => {
serde_json::from_str(&input_text)
.context("Failed to parse JSON input")
},
InputFormat::Yaml => {
serde_yaml::from_str(&input_text)
.context("Failed to parse YAML input")
},
InputFormat::Toml => {
let value = toml::from_str(&input_text)
.context("Failed to parse TOML input")?;
toml_to_json_value(value)
},
}
}
fn toml_to_json_value(toml_value: toml::Value) -> Result<Value> {
match toml_value {
toml::Value::String(s) => Ok(Value::String(s)),
toml::Value::Integer(i) => Ok(Value::Number(serde_json::Number::from(i))),
toml::Value::Float(f) => {
if f.is_finite() {
match serde_json::Number::from_f64(f) {
Some(n) => Ok(Value::Number(n)),
None => Err(anyhow!("Cannot represent float in JSON: {}", f)),
}
} else {
Err(anyhow!("JSON cannot represent non-finite float: {}", f))
}
},
toml::Value::Boolean(b) => Ok(Value::Bool(b)),
toml::Value::Datetime(dt) => Ok(Value::String(dt.to_string())),
toml::Value::Array(arr) => {
let mut json_array = Vec::new();
for item in arr {
json_array.push(toml_to_json_value(item)?);
}
Ok(Value::Array(json_array))
},
toml::Value::Table(table) => {
let mut map = serde_json::Map::new();
for (key, value) in table {
map.insert(key, toml_to_json_value(value)?);
}
Ok(Value::Object(map))
}
}
}