Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tree cmd and --format args to CLI #118

Merged
merged 1 commit into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions omnibor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,37 @@ repository = "https://github.com/omnibor/omnibor-rs"
version = "0.3.0"

[dependencies]

# Library dependencies

gitoid = "0.5.0"
tokio = { version = "1.36.0", features = ["io-util"] }
url = "2.5.0"
clap = { version = "4.5.1", features = ["derive"], optional = true }

# Binary-only dependencies

anyhow = { version = "1.0.80", optional = true }
async-recursion = { version = "1.0.5", optional = true }
async-walkdir = { version = "1.0.0", optional = true }
clap = { version = "4.5.1", features = ["derive"], optional = true }
futures-lite = { version = "2.2.0", optional = true }
serde_json = { version = "1.0.114", optional = true }

[dev-dependencies]
tokio = { version = "1.36.0", features = ["io-util", "fs"] }
tokio-test = "0.4.3"

[features]
build-binary = ["anyhow", "clap"]
build-binary = [
"dep:anyhow",
"dep:async-recursion",
"dep:async-walkdir",
"dep:clap",
"dep:futures-lite",
"dep:serde_json",
"tokio/fs",
"tokio/rt-multi-thread"
]

[[bin]]
name = "omnibor"
Expand Down
174 changes: 168 additions & 6 deletions omnibor/src/bin/omnibor.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@
use anyhow::anyhow;
use anyhow::Context as _;
use anyhow::Error;
use anyhow::Result;
use async_recursion::async_recursion;
use async_walkdir::WalkDir;
use clap::Args;
use clap::Parser;
use clap::Subcommand;
use futures_lite::stream::StreamExt as _;
use omnibor::Sha256;
use serde_json::json;
use std::default::Default;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use std::fs::File;
use std::path::Path;
use std::path::PathBuf;
use std::process::ExitCode;
use std::str::FromStr;
use tokio::fs::File as AsyncFile;
use tokio::runtime::Runtime;

fn main() -> ExitCode {
let args = Cli::parse();

let result = match args.command {
Command::Id(args) => run_id(args),
Command::Id(ref args) => run_id(args),
Command::Tree(ref args) => run_tree(args),
};

if let Err(err) = result {
eprintln!("{}", err);
if let Err(e) = result {
if let Some(format) = &args.format() {
print_error(e, *format);
} else {
print_plain_error(e);
}

return ExitCode::FAILURE;
}

ExitCode::SUCCESS
}


/*===========================================================================
* CLI Arguments
*-------------------------------------------------------------------------*/
Expand All @@ -35,18 +54,75 @@ struct Cli {
command: Command,
}

impl Cli {
fn format(&self) -> Option<Format> {
match &self.command {
Command::Id(args) => Some(args.format),
Command::Tree(args) => Some(args.format),
}
}
}

#[derive(Debug, Subcommand)]
enum Command {
/// Print the Artifact ID of the path given.
Id(IdArgs),
/// Print the Artifact IDs of a directory tree.
Tree(TreeArgs),
}

#[derive(Debug, Args)]
struct IdArgs {
/// The path to identify.
path: PathBuf,

/// The format of output
#[arg(short = 'f', long = "format", default_value_t)]
format: Format,
}

#[derive(Debug, Args)]
struct TreeArgs {
/// The root of the tree to identify.
path: PathBuf,

/// The format of output (can be "plain" or "json")
#[arg(short = 'f', long = "format", default_value_t)]
format: Format,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Format {
Plain,
Json,
}

impl Default for Format {
fn default() -> Self {
Format::Plain
}
}

impl Display for Format {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
Format::Plain => write!(f, "plain"),
Format::Json => write!(f, "json"),
}
}
}

impl FromStr for Format {
type Err = Error;

fn from_str(s: &str) -> Result<Format> {
match s {
"plain" => Ok(Format::Plain),
"json" => Ok(Format::Json),
_ => Err(anyhow!("unknown format '{}'", s)),
}
}
}

/*===========================================================================
* Command Implementations
Expand All @@ -58,10 +134,96 @@ type ArtifactId = omnibor::ArtifactId<Sha256>;
/// Run the `id` subcommand.
///
/// This command just produces the `gitoid` URL for the given file.
fn run_id(args: IdArgs) -> Result<()> {
fn run_id(args: &IdArgs) -> Result<()> {
let path = &args.path;
let file = File::open(path).with_context(|| format!("failed to open '{}'", path.display()))?;
let id = ArtifactId::id_reader(&file).context("failed to produce Artifact ID")?;
println!("{}", id.url());

match args.format {
Format::Plain => {
println!("{}", id.url());
}
Format::Json => {
let output = json!({ "id": id.url().to_string() });
println!("{}", output);
}
}

Ok(())
}

/// Run the `tree` subcommand.
///
/// This command produces the `gitoid` URL for all files in a directory tree.
fn run_tree(args: &TreeArgs) -> Result<()> {
#[async_recursion]
async fn process_dir(path: &Path, format: Format) -> Result<()> {
let mut entries = WalkDir::new(path);

loop {
match entries.next().await {
Some(Ok(entry)) => {
let path = &entry.path();

let file_type = entry
.file_type()
.await
.with_context(|| format!("unable to identify file type for '{}'", path.display()))?;

if file_type.is_dir() {
process_dir(path, format).await?;
continue;
}

let mut file = AsyncFile::open(path)
.await
.with_context(|| format!("failed to open file '{}'", path.display()))?;

let id = ArtifactId::id_async_reader(&mut file)
.await
.with_context(|| {
format!("failed to produce Artifact ID for '{}'", path.display())
})?;

match format {
Format::Plain => println!("{} => {}", path.display(), id.url()),
Format::Json => {
let output = json!({
"path": path.display().to_string(),
"id": id.url().to_string()
});

println!("{}", output);
}
}
}
Some(Err(e)) => print_error(Error::from(e), format),
None => break,
}
}

Ok(())
}

let runtime = Runtime::new().context("failed to initialize the async runtime")?;
runtime.block_on(process_dir(&args.path, args.format))
}

/// Print an error, respecting formatting.
fn print_error(error: Error, format: Format) {
match format {
Format::Plain => print_plain_error(error),
Format::Json => {
let output = json!({
"error": error.to_string(),
});

eprintln!("{}", output);
}
}
}

/// Print an error in plain formatting.
fn print_plain_error(error: Error) {
eprintln!("error: {}", error);
}