use anyhow::{anyhow, Result};
use clap::{ArgAction, Parser, ValueEnum};
use cql2::{Expr, ToSqlAst, Validator};
use std::io::Read;
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[arg(short, long)]
filter: Option<String>,
input: Option<String>,
#[arg(short, long)]
input_format: Option<InputFormat>,
#[arg(short, long)]
output_format: Option<OutputFormat>,
#[arg(long)]
no_validate: bool,
#[arg(long)]
reduce: bool,
#[arg(short, long, action = ArgAction::Count)]
verbose: u8,
}
#[derive(Debug, ValueEnum, Clone)]
pub enum InputFormat {
Json,
Text,
}
#[derive(Debug, ValueEnum, Clone)]
enum OutputFormat {
JsonPretty,
Json,
Text,
Sql,
}
impl Cli {
pub fn run(self) {
if let Err(err) = self.run_inner() {
eprintln!("{}", err);
std::process::exit(1)
}
}
pub fn run_inner(self) -> Result<()> {
if let Some(filter_path) = self.filter.as_ref() {
use std::{
fs::File,
io::{BufRead, BufReader},
};
let expr_str = self.input.as_ref().ok_or_else(|| {
anyhow!("CQL2 expression required as positional argument when using --filter")
})?;
let expr: Expr = if expr_str.trim_start().starts_with('{') {
cql2::parse_json(expr_str)?
} else {
cql2::parse_text(expr_str)?
};
let file = File::open(filter_path)?;
let reader = BufReader::new(file);
reader
.lines()
.map(|line| {
let line = line?;
Ok(serde_json::from_str(&line)?)
})
.collect::<Result<Vec<_>, anyhow::Error>>()?
.into_iter()
.filter_map(|value| {
expr.filter(&[value])
.ok()
.and_then(|mut v| v.pop().cloned())
})
.for_each(|v| println!("{}", serde_json::to_string(&v).unwrap()));
return Ok(());
}
let input = self
.input
.and_then(|input| if input == "-" { None } else { Some(input) })
.map(Ok)
.unwrap_or_else(read_stdin)?;
let input_format = self.input_format.unwrap_or_else(|| {
if input.starts_with('{') {
InputFormat::Json
} else {
InputFormat::Text
}
});
let mut expr: Expr = match input_format {
InputFormat::Json => cql2::parse_json(&input)?,
InputFormat::Text => match cql2::parse_text(&input) {
Ok(expr) => expr,
Err(err) => {
return Err(anyhow!("[ERROR] Parsing error: {input}\n{err}"));
}
},
};
if self.reduce {
expr = expr.reduce(None)?;
}
if !self.no_validate {
let validator = Validator::new().unwrap();
let value = serde_json::to_value(&expr).unwrap();
if let Err(error) = validator.validate(&value) {
return Err(anyhow!(
"[ERROR] Invalid CQL2: {input}\n{}",
match self.verbose {
0 => "For more detailed validation information, use -v".to_string(),
1 => format!("For more detailed validation information, use -vv\n{error}"),
_ => format!("{error:#}"),
}
));
}
}
let output_format = self.output_format.unwrap_or(match input_format {
InputFormat::Json => OutputFormat::Json,
InputFormat::Text => OutputFormat::Text,
});
match output_format {
OutputFormat::JsonPretty => serde_json::to_writer_pretty(std::io::stdout(), &expr)?,
OutputFormat::Json => serde_json::to_writer(std::io::stdout(), &expr)?,
OutputFormat::Text => print!("{}", expr.to_text()?),
OutputFormat::Sql => {
let sql_ast = expr.to_sql_ast()?;
println!("{}", sql_ast);
}
}
println!();
Ok(())
}
}
fn read_stdin() -> Result<String> {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
Ok(buf)
}