automerge/rust/automerge-cli/src/main.rs
2023-01-10 12:51:56 +00:00

240 lines
6.8 KiB
Rust

use std::{fs::File, path::PathBuf, str::FromStr};
use anyhow::{anyhow, Result};
use clap::{
builder::{BoolishValueParser, TypedValueParser, ValueParserFactory},
Parser,
};
use is_terminal::IsTerminal;
mod color_json;
mod examine;
mod examine_sync;
mod export;
mod import;
mod merge;
#[derive(Parser, Debug)]
#[clap(about = "Automerge CLI")]
struct Opts {
#[clap(subcommand)]
cmd: Command,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum ExportFormat {
Json,
Toml,
}
#[derive(Copy, Clone, Default, Debug)]
pub(crate) struct SkipVerifyFlag(bool);
impl SkipVerifyFlag {
fn load(&self, buf: &[u8]) -> Result<automerge::Automerge, automerge::AutomergeError> {
if self.0 {
automerge::Automerge::load(buf)
} else {
automerge::Automerge::load_unverified_heads(buf)
}
}
}
#[derive(Clone)]
struct SkipVerifyFlagParser;
impl ValueParserFactory for SkipVerifyFlag {
type Parser = SkipVerifyFlagParser;
fn value_parser() -> Self::Parser {
SkipVerifyFlagParser
}
}
impl TypedValueParser for SkipVerifyFlagParser {
type Value = SkipVerifyFlag;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
BoolishValueParser::new()
.parse_ref(cmd, arg, value)
.map(SkipVerifyFlag)
}
}
impl FromStr for ExportFormat {
type Err = anyhow::Error;
fn from_str(input: &str) -> Result<ExportFormat> {
match input {
"json" => Ok(ExportFormat::Json),
"toml" => Ok(ExportFormat::Toml),
_ => Err(anyhow!("Invalid export format: {}", input)),
}
}
}
#[derive(Debug, Parser)]
enum Command {
/// Output current state of an Automerge document in a specified format
Export {
/// Format for output: json, toml
#[clap(long, short, default_value = "json")]
format: ExportFormat,
/// Path that contains Automerge changes
changes_file: Option<PathBuf>,
/// The file to write to. If omitted assumes stdout
#[clap(long("out"), short('o'))]
output_file: Option<PathBuf>,
/// Whether to verify the head hashes of a compressed document
#[clap(long, action = clap::ArgAction::SetFalse)]
skip_verifying_heads: SkipVerifyFlag,
},
Import {
/// Format for input: json, toml
#[clap(long, short, default_value = "json")]
format: ExportFormat,
input_file: Option<PathBuf>,
/// Path to write Automerge changes to
#[clap(long("out"), short('o'))]
changes_file: Option<PathBuf>,
},
/// Read an automerge document and print a JSON representation of the changes in it to stdout
Examine {
input_file: Option<PathBuf>,
skip_verifying_heads: SkipVerifyFlag,
},
/// Read an automerge sync messaage and print a JSON representation of it
ExamineSync { input_file: Option<PathBuf> },
/// Read one or more automerge documents and output a merged, compacted version of them
Merge {
/// The file to write to. If omitted assumes stdout
#[clap(long("out"), short('o'))]
output_file: Option<PathBuf>,
/// The file(s) to compact. If empty assumes stdin
input: Vec<PathBuf>,
},
}
fn open_file_or_stdin(maybe_path: Option<PathBuf>) -> Result<Box<dyn std::io::Read>> {
if std::io::stdin().is_terminal() {
if let Some(path) = maybe_path {
Ok(Box::new(File::open(path).unwrap()))
} else {
Err(anyhow!(
"Must provide file path if not providing input via stdin"
))
}
} else {
Ok(Box::new(std::io::stdin()))
}
}
fn create_file_or_stdout(maybe_path: Option<PathBuf>) -> Result<Box<dyn std::io::Write>> {
if std::io::stdout().is_terminal() {
if let Some(path) = maybe_path {
Ok(Box::new(File::create(path).unwrap()))
} else {
Err(anyhow!("Must provide file path if not piping to stdout"))
}
} else {
Ok(Box::new(std::io::stdout()))
}
}
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let opts = Opts::parse();
match opts.cmd {
Command::Export {
changes_file,
format,
output_file,
skip_verifying_heads,
} => {
let output: Box<dyn std::io::Write> = if let Some(output_file) = output_file {
Box::new(File::create(output_file)?)
} else {
Box::new(std::io::stdout())
};
match format {
ExportFormat::Json => {
let mut in_buffer = open_file_or_stdin(changes_file)?;
export::export_json(
&mut in_buffer,
output,
skip_verifying_heads,
std::io::stdout().is_terminal(),
)
}
ExportFormat::Toml => unimplemented!(),
}
}
Command::Import {
format,
input_file,
changes_file,
} => match format {
ExportFormat::Json => {
let mut out_buffer = create_file_or_stdout(changes_file)?;
let mut in_buffer = open_file_or_stdin(input_file)?;
import::import_json(&mut in_buffer, &mut out_buffer)
}
ExportFormat::Toml => unimplemented!(),
},
Command::Examine {
input_file,
skip_verifying_heads,
} => {
let in_buffer = open_file_or_stdin(input_file)?;
let out_buffer = std::io::stdout();
match examine::examine(
in_buffer,
out_buffer,
skip_verifying_heads,
std::io::stdout().is_terminal(),
) {
Ok(()) => {}
Err(e) => {
eprintln!("Error: {:?}", e);
}
}
Ok(())
}
Command::ExamineSync { input_file } => {
let in_buffer = open_file_or_stdin(input_file)?;
let out_buffer = std::io::stdout();
match examine_sync::examine_sync(in_buffer, out_buffer, std::io::stdout().is_terminal())
{
Ok(()) => {}
Err(e) => {
eprintln!("Error: {:?}", e);
}
}
Ok(())
}
Command::Merge { input, output_file } => {
let out_buffer = create_file_or_stdout(output_file)?;
match merge::merge(input.into(), out_buffer) {
Ok(()) => {}
Err(e) => {
eprintln!("Failed to merge: {}", e);
}
};
Ok(())
}
}
}