mod commands;
mod convert;
mod helpers;
mod types;
use std::fs;
use std::io::{self, Read, Write};
use serde::Serialize;
use types::Error;
pub(crate) const API_VERSION: &str = "1.0";
pub(crate) mod exit_codes {
pub const SUCCESS: i32 = 0;
pub const USER_ERROR: i32 = 1;
#[allow(dead_code)]
pub const INTERNAL_ERROR: i32 = 2;
}
pub(crate) fn output_json<T: Serialize>(value: &T) -> i32 {
match serde_json::to_string(value) {
Ok(json) => {
if writeln!(io::stdout(), "{json}").is_err() {
return exit_codes::SUCCESS;
}
exit_codes::SUCCESS
}
Err(e) => {
eprintln!("Error serializing JSON: {e}");
exit_codes::INTERNAL_ERROR
}
}
}
pub(crate) fn parse_json_error(e: &serde_json::Error) -> Error {
let mut err = Error::new(format!("JSON parse error: {e}"));
if e.line() > 0 {
err.line = Some(e.line() as u32);
err.column = Some(e.column() as u32);
}
let msg = e.to_string();
if msg.contains("missing field") || msg.contains("unknown field") {
if let Some(start) = msg.find('`') {
if let Some(end) = msg[start + 1..].find('`') {
err.field = Some(msg[start + 1..start + 1 + end].to_string());
}
}
}
err
}
fn read_source(file_path: Option<&str>) -> Result<String, String> {
if let Some(path) = file_path {
fs::read_to_string(path).map_err(|e| format!("Error reading file '{path}': {e}"))
} else {
let mut source = String::new();
io::stdin()
.read_to_string(&mut source)
.map_err(|e| format!("Error reading stdin: {e}"))?;
Ok(source)
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
commands::util::cmd_help();
std::process::exit(exit_codes::USER_ERROR);
}
let command = &args[1];
let exit_code = match command.as_str() {
"version" => commands::util::cmd_version(),
"help" | "--help" | "-h" => {
commands::util::cmd_help();
exit_codes::SUCCESS
}
"load-file" => {
if args.len() < 3 {
eprintln!("Error: load-file command requires file path argument");
exit_codes::USER_ERROR
} else {
let filename = &args[2];
match read_source(Some(filename)) {
Ok(source) => commands::load::cmd_load(&source, filename),
Err(e) => {
eprintln!("{e}");
exit_codes::USER_ERROR
}
}
}
}
"load-full" => {
if args.len() < 3 {
eprintln!("Error: load-full command requires file path argument");
exit_codes::USER_ERROR
} else {
let path = &args[2];
let plugins: Vec<&str> = args[3..].iter().map(String::as_str).collect();
commands::load::cmd_load_full(path, &plugins)
}
}
"validate-file" => {
if args.len() < 3 {
eprintln!("Error: validate-file command requires file path argument");
exit_codes::USER_ERROR
} else {
match read_source(Some(&args[2])) {
Ok(source) => commands::validate::cmd_validate(&source),
Err(e) => {
eprintln!("{e}");
exit_codes::USER_ERROR
}
}
}
}
"query-file" => {
if args.len() < 4 {
eprintln!("Error: query-file command requires file path and BQL arguments");
exit_codes::USER_ERROR
} else {
match read_source(Some(&args[2])) {
Ok(source) => commands::query::cmd_query(&source, &args[3]),
Err(e) => {
eprintln!("{e}");
exit_codes::USER_ERROR
}
}
}
}
"batch-file" => {
if args.len() < 4 {
eprintln!("Error: batch-file command requires file path and at least one query");
exit_codes::USER_ERROR
} else {
let filename = &args[2];
let queries: Vec<String> = args.iter().skip(3).cloned().collect();
match read_source(Some(filename)) {
Ok(source) => commands::query::cmd_batch(&source, filename, &queries),
Err(e) => {
eprintln!("{e}");
exit_codes::USER_ERROR
}
}
}
}
"format-file" => {
if args.len() < 3 {
eprintln!("Error: format-file command requires file path argument");
exit_codes::USER_ERROR
} else {
match read_source(Some(&args[2])) {
Ok(source) => commands::format::cmd_format(&source),
Err(e) => {
eprintln!("{e}");
exit_codes::USER_ERROR
}
}
}
}
"clamp-file" => {
if args.len() < 3 {
eprintln!("Error: clamp-file command requires file path argument");
exit_codes::USER_ERROR
} else {
let filename = &args[2];
let begin_date = args.get(3).map(String::as_str);
let end_date = args.get(4).map(String::as_str);
match read_source(Some(filename)) {
Ok(source) => {
commands::clamp::cmd_clamp(&source, filename, begin_date, end_date)
}
Err(e) => {
eprintln!("{e}");
exit_codes::USER_ERROR
}
}
}
}
"is-encrypted" => {
if args.len() < 3 {
eprintln!("Error: is-encrypted command requires file path argument");
exit_codes::USER_ERROR
} else {
commands::util::cmd_is_encrypted(&args[2])
}
}
"get-account-type" => {
if args.len() < 3 {
eprintln!("Error: get-account-type command requires account name argument");
exit_codes::USER_ERROR
} else {
commands::util::cmd_get_account_type(&args[2])
}
}
"types" => commands::util::cmd_types(),
"schema" => commands::util::cmd_schema(),
"format-entry" | "format-entries" | "create-entry" | "create-entries"
| "filter-entries" | "clamp-entries" => {
let json_str = match read_source(None) {
Ok(s) => s,
Err(e) => {
eprintln!("{e}");
std::process::exit(exit_codes::USER_ERROR);
}
};
match command.as_str() {
"format-entry" => commands::format::cmd_format_entry(&json_str),
"format-entries" => commands::format::cmd_format_entries(&json_str),
"create-entry" => commands::entry::cmd_create_entry(&json_str),
"create-entries" => commands::entry::cmd_create_entries(&json_str),
"filter-entries" => commands::clamp::cmd_filter_entries(&json_str),
"clamp-entries" => commands::clamp::cmd_clamp_entries(&json_str),
_ => unreachable!(),
}
}
"load" | "validate" | "query" | "batch" | "format" | "clamp" => {
let source = match read_source(None) {
Ok(s) => s,
Err(e) => {
eprintln!("{e}");
std::process::exit(exit_codes::USER_ERROR);
}
};
match command.as_str() {
"load" => {
let filename = args.get(2).map_or("<stdin>", std::string::String::as_str);
commands::load::cmd_load(&source, filename)
}
"validate" => commands::validate::cmd_validate(&source),
"query" => {
if args.len() < 3 {
eprintln!("Error: query command requires BQL argument");
exit_codes::USER_ERROR
} else {
commands::query::cmd_query(&source, &args[2])
}
}
"batch" => {
let filename = args.get(2).map_or("<stdin>", std::string::String::as_str);
let queries: Vec<String> = args.iter().skip(3).cloned().collect();
commands::query::cmd_batch(&source, filename, &queries)
}
"format" => commands::format::cmd_format(&source),
"clamp" => {
let filename = args.get(2).map_or("<stdin>", std::string::String::as_str);
let begin_date = args.get(3).map(String::as_str);
let end_date = args.get(4).map(String::as_str);
commands::clamp::cmd_clamp(&source, filename, begin_date, end_date)
}
_ => unreachable!(),
}
}
_ => {
eprintln!("Unknown command: {command}");
commands::util::cmd_help();
exit_codes::USER_ERROR
}
};
std::process::exit(exit_code);
}