Skip to content

Commit

Permalink
feat(hugr-cli)!: move mermaid to own sub-command (#1390)
Browse files Browse the repository at this point in the history
drive-by replace `clap_stdin` with `clio` as that's that `clap`
recommends, and it comes with stdout too.


BREAKING CHANGE: Cli validate command no longer has a mermaid option,
use `mermaid` sub-command instead.
  • Loading branch information
ss2165 authored Aug 2, 2024
1 parent f78c0cc commit 77795b9
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 154 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ typetag = "0.2.7"
urlencoding = "2.1.2"
webbrowser = "1.0.0"
clap = { version = "4.5.4" }
clap-stdin = "0.5.0"
clio = "0.3.5"
clap-verbosity-flag = "2.2.0"
assert_cmd = "2.0.14"
assert_fs = "1.1.1"
Expand Down
2 changes: 1 addition & 1 deletion hugr-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ categories = ["compilers"]

[dependencies]
clap = { workspace = true, features = ["derive"] }
clap-stdin.workspace = true
clap-verbosity-flag.workspace = true
hugr-core = { path = "../hugr-core", version = "0.7.0" }
serde_json.workspace = true
serde.workspace = true
thiserror.workspace = true
clio = { workspace = true, features = ["clap-parse"] }

[lints]
workspace = true
Expand Down
79 changes: 74 additions & 5 deletions hugr-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
//! Standard command line tools, used by the hugr binary.

use std::ffi::OsString;
use clap::Parser;
use clap_verbosity_flag::{InfoLevel, Verbosity};
use clio::Input;
use hugr_core::{Extension, Hugr};
use std::{ffi::OsString, path::PathBuf};
use thiserror::Error;
/// We reexport some clap types that are used in the public API.
pub use {clap::Parser, clap_verbosity_flag::Level};

pub mod extensions;
pub mod mermaid;
pub mod validate;

/// CLI arguments.
Expand All @@ -16,9 +19,11 @@ pub mod validate;
#[non_exhaustive]
pub enum CliArgs {
/// Validate and visualize a HUGR file.
Validate(validate::CliArgs),
Validate(validate::ValArgs),
/// Write standard extensions out in serialized form.
GenExtensions(extensions::ExtArgs),
/// Write HUGR as mermaid diagrams.
Mermaid(mermaid::MermaidArgs),
/// External commands
#[command(external_subcommand)]
External(Vec<OsString>),
Expand All @@ -29,6 +34,70 @@ pub enum CliArgs {
#[error(transparent)]
#[non_exhaustive]
pub enum CliError {
/// Error reading input.
#[error("Error reading from path: {0}")]
InputFile(#[from] std::io::Error),
/// Error parsing input.
#[error("Error parsing input: {0}")]
Parse(#[from] serde_json::Error),
/// Errors produced by the `validate` subcommand.
Validate(#[from] validate::CliError),
Validate(#[from] validate::ValError),
}

/// Validate and visualise a HUGR file.
#[derive(Parser, Debug)]
pub struct HugrArgs {
/// Input HUGR file, use '-' for stdin
#[clap(value_parser, default_value = "-")]
pub input: Input,
/// Verbosity.
#[command(flatten)]
pub verbose: Verbosity<InfoLevel>,
/// No standard extensions.
#[arg(
long,
help = "Don't use standard extensions when validating. Prelude is still used."
)]
pub no_std: bool,
/// Extensions paths.
#[arg(
short,
long,
help = "Paths to serialised extensions to validate against."
)]
pub extensions: Vec<PathBuf>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
/// Package of module HUGRs and extensions.
/// The HUGRs are validated against the extensions.
pub struct Package {
/// Module HUGRs included in the package.
pub modules: Vec<Hugr>,
/// Extensions to validate against.
pub extensions: Vec<Extension>,
}

impl Package {
/// Create a new package.
pub fn new(modules: Vec<Hugr>, extensions: Vec<Extension>) -> Self {
Self {
modules,
extensions,
}
}
}

impl HugrArgs {
/// Read either a package or a single hugr from the input.
pub fn get_package(&mut self) -> Result<Package, CliError> {
let val: serde_json::Value = serde_json::from_reader(&mut self.input)?;
// read either a package or a single hugr
if let Ok(p) = serde_json::from_value::<Package>(val.clone()) {
Ok(p)
} else {
let hugr: Hugr = serde_json::from_value(val)?;
Ok(Package::new(vec![hugr], vec![]))
}
}
}
3 changes: 2 additions & 1 deletion hugr-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ fn main() {
match CliArgs::parse() {
CliArgs::Validate(args) => run_validate(args),
CliArgs::GenExtensions(args) => args.run_dump(),
CliArgs::Mermaid(mut args) => args.run_print().unwrap(),
CliArgs::External(_) => {
// TODO: Implement support for external commands.
// Running `hugr COMMAND` would look for `hugr-COMMAND` in the path
Expand All @@ -25,7 +26,7 @@ fn main() {
}

/// Run the `validate` subcommand.
fn run_validate(args: validate::CliArgs) {
fn run_validate(mut args: validate::ValArgs) {
let result = args.run();

if let Err(e) = result {
Expand Down
43 changes: 43 additions & 0 deletions hugr-cli/src/mermaid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//! Render mermaid diagrams.
use std::io::Write;

use clap::Parser;
use clio::Output;
use hugr_core::HugrView;

/// Dump the standard extensions.
#[derive(Parser, Debug)]
#[clap(version = "1.0", long_about = None)]
#[clap(about = "Render mermaid diagrams..")]
#[group(id = "hugr")]
#[non_exhaustive]
pub struct MermaidArgs {
/// Common arguments
#[command(flatten)]
pub hugr_args: crate::HugrArgs,
/// Validate package.
#[arg(
long,
help = "Validate before rendering, includes extension inference."
)]
pub validate: bool,
/// Output file '-' for stdout
#[clap(long, short, value_parser, default_value = "-")]
output: Output,
}

impl MermaidArgs {
/// Write the mermaid diagram to the output.
pub fn run_print(&mut self) -> Result<(), crate::CliError> {
let hugrs = if self.validate {
self.hugr_args.validate()?
} else {
self.hugr_args.get_package()?.modules
};

for hugr in hugrs {
write!(self.output, "{}", hugr.mermaid_string())?;
}
Ok(())
}
}
115 changes: 31 additions & 84 deletions hugr-cli/src/validate.rs
Original file line number Diff line number Diff line change
@@ -1,58 +1,28 @@
//! The `validate` subcommand.
use std::path::PathBuf;

use clap::Parser;
use clap_stdin::FileOrStdin;
use clap_verbosity_flag::{InfoLevel, Level, Verbosity};
use hugr_core::{extension::ExtensionRegistry, Extension, Hugr, HugrView as _};
use clap_verbosity_flag::Level;
use hugr_core::{extension::ExtensionRegistry, Extension, Hugr};
use thiserror::Error;

use crate::{CliError, HugrArgs, Package};

/// Validate and visualise a HUGR file.
#[derive(Parser, Debug)]
#[clap(version = "1.0", long_about = None)]
#[clap(about = "Validate a HUGR.")]
#[group(id = "hugr")]
#[non_exhaustive]
pub struct CliArgs {
/// The input hugr to parse.
pub input: FileOrStdin,
/// Visualise with mermaid.
#[arg(short, long, value_name = "MERMAID", help = "Visualise with mermaid.")]
pub mermaid: bool,
/// Skip validation.
#[arg(short, long, help = "Skip validation.")]
pub no_validate: bool,
/// Verbosity.
pub struct ValArgs {
#[command(flatten)]
pub verbose: Verbosity<InfoLevel>,
/// No standard extensions.
#[arg(
long,
help = "Don't use standard extensions when validating. Prelude is still used."
)]
pub no_std: bool,
/// Extensions paths.
#[arg(
short,
long,
help = "Paths to serialised extensions to validate against."
)]
pub extensions: Vec<PathBuf>,
/// common arguments
pub hugr_args: HugrArgs,
}

/// Error type for the CLI.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum CliError {
/// Error reading input.
#[error("Error reading input: {0}")]
Input(#[from] clap_stdin::StdinError),
/// Error reading input.
#[error("Error reading from path: {0}")]
InputFile(#[from] std::io::Error),
/// Error parsing input.
#[error("Error parsing input: {0}")]
Parse(#[from] serde_json::Error),
pub enum ValError {
/// Error validating HUGR.
#[error("Error validating HUGR: {0}")]
Validate(#[from] hugr_core::hugr::ValidationError),
Expand All @@ -61,45 +31,28 @@ pub enum CliError {
ExtReg(#[from] hugr_core::extension::ExtensionRegistryError),
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
/// Package of module HUGRs and extensions.
/// The HUGRs are validated against the extensions.
pub struct Package {
/// Module HUGRs included in the package.
pub modules: Vec<Hugr>,
/// Extensions to validate against.
pub extensions: Vec<Extension>,
}

impl Package {
/// Create a new package.
pub fn new(modules: Vec<Hugr>, extensions: Vec<Extension>) -> Self {
Self {
modules,
extensions,
}
}
}

/// String to print when validation is successful.
pub const VALID_PRINT: &str = "HUGR valid!";

impl CliArgs {
impl ValArgs {
/// Run the HUGR cli and validate against an extension registry.
pub fn run(&self) -> Result<Vec<Hugr>, CliError> {
let rdr = self.input.clone().into_reader()?;
let val: serde_json::Value = serde_json::from_reader(rdr)?;
// read either a package or a single hugr
let (mut modules, packed_exts) = if let Ok(Package {
modules,
extensions,
}) = serde_json::from_value::<Package>(val.clone())
{
(modules, extensions)
} else {
let hugr: Hugr = serde_json::from_value(val)?;
(vec![hugr], vec![])
};
pub fn run(&mut self) -> Result<Vec<Hugr>, CliError> {
self.hugr_args.validate()
}

/// Test whether a `level` message should be output.
pub fn verbosity(&self, level: Level) -> bool {
self.hugr_args.verbosity(level)
}
}

impl HugrArgs {
/// Load the package and validate against an extension registry.
pub fn validate(&mut self) -> Result<Vec<Hugr>, CliError> {
let Package {
mut modules,
extensions: packed_exts,
} = self.get_package()?;

let mut reg: ExtensionRegistry = if self.no_std {
hugr_core::extension::PRELUDE_REGISTRY.to_owned()
Expand All @@ -109,26 +62,20 @@ impl CliArgs {

// register packed extensions
for ext in packed_exts {
reg.register_updated(ext)?;
reg.register_updated(ext).map_err(ValError::ExtReg)?;
}

// register external extensions
for ext in &self.extensions {
let f = std::fs::File::open(ext)?;
let ext: Extension = serde_json::from_reader(f)?;
reg.register_updated(ext)?;
reg.register_updated(ext).map_err(ValError::ExtReg)?;
}

for hugr in modules.iter_mut() {
if self.mermaid {
println!("{}", hugr.mermaid_string());
}

if !self.no_validate {
hugr.update_validate(&reg)?;
if self.verbosity(Level::Info) {
eprintln!("{}", VALID_PRINT);
}
hugr.update_validate(&reg).map_err(ValError::Validate)?;
if self.verbosity(Level::Info) {
eprintln!("{}", VALID_PRINT);
}
}
Ok(modules)
Expand Down
Loading

0 comments on commit 77795b9

Please sign in to comment.