vanth/crates/cli/src/cli.rs
Markus Scully af6bfadff8
🐛♻️ cli, vanth, vanth tests: Refactor store initialization and add trace logging
- Fixed CLI commands to prevent creating databases unnecessarily by using `StoreParams` with `create_if_not_exists: false` for `get`, `get_all`, `delete`, and `delete_all` commands.
- Refactored `Store::sqlite_from_path` to accept `StoreParams` and dynamically generate SQLite open flags.
- Added trace logging for received JSON content in `handle_write` command.
2025-08-07 16:27:39 +03:00

235 lines
6.4 KiB
Rust

use clap::{Args, Parser, Subcommand};
use tracing::trace;
use std::io::{self, Read};
use std::path::PathBuf;
use std::process;
use vanth::hash as vanth_hash;
use vanth::{
ContentHash, Ty,
store::{Store, StoreParams},
};
#[derive(Parser, Debug)]
#[command(name = "vanth")]
#[command(about = "Vanth CLI tool")]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(about = "Write a value to the store")]
Write(WriteArgs),
#[command(about = "Get a value from the store")]
Get(GetArgs),
#[command(about = "Get all values of a type from the store")]
GetAll(GetAllArgs),
#[command(about = "Delete a value by hash from all types")]
Delete(DeleteArgs),
#[command(about = "Delete all values of a type")]
DeleteAll(DeleteAllArgs),
}
#[derive(Args, Debug)]
pub struct WriteArgs {
#[arg(long, help = "Database file path")]
db: PathBuf,
#[arg(long, help = "Type name, e.g., path::to::Type")]
ty: String,
#[arg(long, help = "JSON value to write, optional (read from stdin if omitted)")]
value: Option<String>,
}
#[derive(Args, Debug)]
pub struct GetArgs {
#[arg(long, help = "Database file path")]
db: PathBuf,
#[arg(long, help = "Type name, e.g., path::to::Type")]
ty: String,
#[arg(help = "Content hash as 64-character hex string")]
content_hash: String,
}
#[derive(Args, Debug)]
pub struct GetAllArgs {
#[arg(long, help = "Database file path")]
db: PathBuf,
#[arg(long, help = "Type name, e.g., path::to::Type")]
ty: String,
}
#[derive(Args, Debug)]
pub struct DeleteArgs {
#[arg(long, help = "Database file path")]
db: PathBuf,
#[arg(help = "Content hash as 64-character hex string")]
content_hash: String,
}
#[derive(Args, Debug)]
pub struct DeleteAllArgs {
#[arg(long, help = "Database file path")]
db: PathBuf,
#[arg(long, help = "Type name, e.g., path::to::Type")]
ty: String,
}
pub fn execute(cli: Cli) {
match cli.command {
Commands::Write(args) => handle_write(&args),
Commands::Get(args) => handle_get(&args),
Commands::GetAll(args) => handle_get_all(&args),
Commands::Delete(args) => handle_delete(&args),
Commands::DeleteAll(args) => handle_delete_all(&args),
}
}
fn parse_ty(s: &str) -> Ty {
Ty {
path: s.split("::").map(|p| p.to_string()).collect(),
}
}
fn parse_hash(s: &str) -> ContentHash {
if s.len() != 64 {
eprintln!("Hash must be exactly 64 hexadecimal characters");
process::exit(1);
}
let mut hash = [0u8; 32];
for (i, byte) in hash.iter_mut().enumerate() {
let hex_slice = &s[i * 2..i * 2 + 2];
*byte = u8::from_str_radix(hex_slice, 16).unwrap_or_else(|_| {
eprintln!("Invalid hexadecimal in hash: {}", hex_slice);
process::exit(1);
});
}
ContentHash { hash }
}
fn handle_write(args: &WriteArgs) {
let mut store = Store::sqlite_from_path(args.db.clone(), StoreParams::default()).unwrap_or_else(|e| {
eprintln!("Error opening store: {:?}", e);
process::exit(1);
});
let ty = parse_ty(&args.ty);
let mut content = String::new();
if let Some(val) = &args.value {
content = val.clone();
} else {
io::stdin().read_to_string(&mut content).unwrap_or_else(|e| {
eprintln!("Error reading from stdin: {}", e);
process::exit(1);
});
}
trace!("Received JSON content: {}", content);
let value: serde_json::Value = serde_json::from_str(&content).unwrap_or_else(|e| {
eprintln!("Invalid JSON: {}", e);
process::exit(1);
});
let data = serde_json::to_vec(&value).unwrap();
let content_hash = vanth_hash(&value);
store.write_raw(ty, content_hash, data).unwrap_or_else(|e| {
eprintln!("Error writing to store: {:?}", e);
process::exit(1);
});
}
fn handle_get(args: &GetArgs) {
let mut store = Store::sqlite_from_path(
args.db.clone(),
StoreParams {
create_if_not_exists: false,
..Default::default()
},
)
.unwrap_or_else(|e| {
eprintln!("Error opening store: {:?}", e);
process::exit(1);
});
let ty = parse_ty(&args.ty);
let content_hash = parse_hash(&args.content_hash);
let raw = store.get_from_hash_raw(ty, content_hash).unwrap_or_else(|e| {
eprintln!("Error getting from store: {:?}", e);
process::exit(1);
});
match raw {
Some(data) => {
let output = String::from_utf8(data).unwrap_or_else(|e| {
eprintln!("Invalid UTF-8 in data: {}", e);
process::exit(1);
});
println!("{}", output);
}
None => {
process::exit(1);
}
}
}
fn handle_get_all(args: &GetAllArgs) {
let mut store = Store::sqlite_from_path(
args.db.clone(),
StoreParams {
create_if_not_exists: false,
..Default::default()
},
)
.unwrap_or_else(|e| {
eprintln!("Error opening store: {:?}", e);
process::exit(1);
});
let ty = parse_ty(&args.ty);
let items = store.get_all_of_type_raw(ty).unwrap_or_else(|e| {
eprintln!("Error getting all from store: {:?}", e);
process::exit(1);
});
for (_, data) in items {
let output = String::from_utf8(data).unwrap_or_else(|e| {
eprintln!("Invalid UTF-8 in data: {}", e);
process::exit(1);
});
println!("{}", output);
}
}
fn handle_delete(args: &DeleteArgs) {
let mut store = Store::sqlite_from_path(
args.db.clone(),
StoreParams {
create_if_not_exists: false,
..Default::default()
},
)
.unwrap_or_else(|e| {
eprintln!("Error opening store: {:?}", e);
process::exit(1);
});
let content_hash = parse_hash(&args.content_hash);
}
fn handle_delete_all(args: &DeleteAllArgs) {
let mut store = Store::sqlite_from_path(
args.db.clone(),
StoreParams {
create_if_not_exists: false,
..Default::default()
},
)
.unwrap_or_else(|e| {
eprintln!("Error opening store: {:?}", e);
process::exit(1);
});
let ty = parse_ty(&args.ty);
store.delete_all_raw(ty).unwrap_or_else(|e| {
eprintln!("Error deleting all from store: {:?}", e);
process::exit(1);
});
}