diff --git a/src/bin/ion/ansi_codes.rs b/src/bin/ion/ansi_codes.rs new file mode 100644 index 0000000..2e70774 --- /dev/null +++ b/src/bin/ion/ansi_codes.rs @@ -0,0 +1,14 @@ +//! Ansi Codes are a convenient way to add styling to text in a terminal. +//! There are libraries that can accomplish the same thing, but when you want to have a large block +//! of static text, sometimes it's simpler to just use `format!()` and include named substitutions +//! (like `{BOLD}`) to turn styling on and off. + +// TODO: Add more constants as needed. + +pub(crate) const NO_STYLE: &str = "\x1B[0m"; +pub(crate) const BOLD: &str = "\x1B[1m"; +pub(crate) const ITALIC: &str = "\x1B[3m"; +pub(crate) const UNDERLINE: &str = "\x1B[4m"; + +pub(crate) const RED: &str = "\x1B[0;31m"; +pub(crate) const GREEN: &str = "\x1B[0;32m"; diff --git a/src/bin/ion/commands/complaint.rs b/src/bin/ion/commands/complaint.rs index 49a749b..c1f2ddf 100644 --- a/src/bin/ion/commands/complaint.rs +++ b/src/bin/ion/commands/complaint.rs @@ -20,7 +20,7 @@ impl IonCliCommand for SucksCommand { command.hide(true) } - fn run(&self, command_path: &mut Vec, args: &ArgMatches) -> anyhow::Result<()> { + fn run(&self, _command_path: &mut Vec, _args: &ArgMatches) -> anyhow::Result<()> { println!( " We're very sorry to hear that! diff --git a/src/bin/ion/commands/schema/validate.rs b/src/bin/ion/commands/schema/validate.rs index 62f7f05..799ccf6 100644 --- a/src/bin/ion/commands/schema/validate.rs +++ b/src/bin/ion/commands/schema/validate.rs @@ -1,7 +1,9 @@ +use crate::ansi_codes::*; use crate::commands::schema::validate::InputGrouping::{FileHandles, Lines, TopLevelValues}; use crate::commands::schema::IonSchemaCommandInput; use crate::commands::{CommandIo, IonCliCommand, WithIonCliArgument}; use crate::input_grouping::InputGrouping; +use crate::output::CommandOutput; use anyhow::Result; use clap::builder::ArgPredicate; use clap::{Arg, ArgAction, ArgMatches, Command}; @@ -9,10 +11,44 @@ use ion_rs::{ion_sexp, AnyEncoding, ElementReader, Reader, SequenceWriter, TextF use ion_rs::{v1_0, Element, IonResult, ValueWriter}; use ion_schema::result::ValidationResult; use ion_schema::AsDocumentHint; -use std::io::BufRead; +use std::io::{BufRead, Write}; +use std::sync::LazyLock; +use termcolor::WriteColor; pub struct ValidateCommand; +static HELP_EPILOGUE: LazyLock = LazyLock::new(|| { + format!( + // '\' at the end of the line indicates that CLAP will handle the line wrapping. + "\ +All Ion Schema types are defined in the context of a schema document, so it is necessary to always \ +have a schema document, even if that schema document is an implicit, empty schema. If a schema is \ +not specified, the default is an implicit, empty Ion Schema 2.0 document. + +{BOLD}{UNDERLINE}Example Usage:{NO_STYLE} + +{UNDERLINE}Validating piped Ion data against an inline type definition{NO_STYLE} + +~$ echo '{{foo:1}} {{bar:2}}' | ion schema -X validate -T '{{fields:{{foo:int,bar:string}}}}' + +(valid ) +(invalid (type_mismatched \"expected type String, found Int\" \"(bar)\" ) ) + +{UNDERLINE}Validating .ion files in a build script{NO_STYLE} + +~$ ion schema -X validate -ER -f my_schema.isl my_type **/*.ion + +a.ion ... ok +b/a.ion ... ok +b/b.ion ... FAILED +b/c.ion ... ok +c.ion ... FAILED + +{ITALIC}NOTE: The output of this command is not intended to be machine-readable.{NO_STYLE} +" + ) +}); + impl IonCliCommand for ValidateCommand { fn name(&self) -> &'static str { "validate" @@ -32,11 +68,7 @@ impl IonCliCommand for ValidateCommand { fn configure_args(&self, command: Command) -> Command { command - .after_help( - "All Ion Schema types are defined in the context of a schema document, so it is necessary to always \ - have a schema document, even if that schema document is an implicit, empty schema. If a schema is \ - not specified, the default is an implicit, empty Ion Schema 2.0 document." - ) + .after_help(HELP_EPILOGUE.as_str()) // Positional args -- It is a breaking change to change the relative order of these args. .arg(IonSchemaCommandInput::type_arg().required(true)) .with_input() @@ -52,15 +84,27 @@ impl IonCliCommand for ValidateCommand { // "quiet" implies "error-on-invalid" so that the command always has some sort of useful output .default_value_if("quiet", ArgPredicate::IsPresent, "true") .action(ArgAction::SetTrue) - .help("Return a non-zero exit code when a value is invalid for the given type.") + .help( + "Return a non-zero exit code when a value is invalid for the given type.", + ), ) .arg( Arg::new("quiet") + .group("output-mode") .short('q') .long("quiet") .help("Suppresses the violations output.") .default_value("false") - .action(ArgAction::SetTrue) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("report") + .group("output-mode") + .short('R') + .long("report") + .help("Prints a human-friendly, test-like report.") + .default_value("false") + .action(ArgAction::SetTrue), ) } @@ -71,32 +115,38 @@ impl IonCliCommand for ValidateCommand { let grouping = InputGrouping::read_from_args(args); let quiet = args.get_flag("quiet"); + let report = args.get_flag("report"); let mut all_valid = true; CommandIo::new(args).for_each_input(|output, input| { + let input_name = input.name().to_string(); // Output always uses 'lines' format so that we can have one output line per grouped input. // If the user wants something different, use 'ion cat' to change it. let mut writer = Writer::new(v1_0::Text.with_format(TextFormat::Lines), output)?; + let mut result_writer = if report { + ResultWriter::Report(input_name) + } else if quiet { + ResultWriter::Quiet + } else { + ResultWriter::Ion + }; + match grouping { FileHandles => { let reader = Reader::new(AnyEncoding, input.into_source())?; let document: Vec<_> = reader.into_elements().collect::>()?; let result = type_ref.validate(document.as_document()); all_valid &= result.is_ok(); - if !quiet { - write_validation_result(result, writer.value_writer())?; - } + result_writer.write_result(&mut writer, result)?; } Lines => { for line in input.into_source().lines() { let document = Element::read_all(line?)?; let result = type_ref.validate(document.as_document()); all_valid &= result.is_ok(); - if !quiet { - write_validation_result(result, writer.value_writer())?; - } + result_writer.write_result(&mut writer, result)?; } } TopLevelValues => { @@ -104,9 +154,7 @@ impl IonCliCommand for ValidateCommand { for value in reader.into_elements() { let result = type_ref.validate(&value?); all_valid &= result.is_ok(); - if !quiet { - write_validation_result(result, writer.value_writer())?; - } + result_writer.write_result(&mut writer, result)?; } } } @@ -124,15 +172,59 @@ impl IonCliCommand for ValidateCommand { } } +enum ResultWriter { + Quiet, + Ion, + Report(String), +} +impl ResultWriter { + fn write_result( + &mut self, + w: &mut Writer>, + result: ValidationResult, + ) -> Result<()> { + match self { + ResultWriter::Quiet => Ok(()), + ResultWriter::Ion => write_validation_result_ion(result, w.value_writer()), + ResultWriter::Report(name) => write_validation_report_line(name, w, result), + } + } +} + +/// Writes a validation result in the "report" style. +/// +/// Format is: ` ... ` +/// +/// This is essentially like the individual lines from `cargo test`. +/// This output format is basically stable, but it is not intended to be machine-readable. +fn write_validation_report_line( + input_name: &str, + w: &mut Writer>, + result: ValidationResult, +) -> Result<()> { + let output = w.output_mut(); + let (color, status) = if result.is_ok() { + (GREEN, "ok") + } else { + (RED, "FAILED") + }; + if output.supports_color() { + output.write_fmt(format_args!("{input_name} ... {color}{status}{NO_STYLE}\n"))?; + } else { + output.write_fmt(format_args!("{input_name} ... {status}\n"))?; + } + Ok(()) +} + /// Writes the validation result /// /// Current format is an s-expression that starts with the symbol 'valid' or 'invalid'. /// If invalid, then it also contains an s-expression describing each violation. /// This output is not (yet?) intended to be stable. -fn write_validation_result( +fn write_validation_result_ion( validation_result: ValidationResult, writer: W, -) -> IonResult<()> { +) -> Result<()> { match validation_result { Ok(_) => writer.write_sexp(vec![&Element::symbol("valid")]), Err(violation) => { @@ -152,7 +244,8 @@ fn write_validation_result( writer.write_sexp(vec_of_refs(&violations)) } - } + }?; + Ok(()) } /// Transposes a borrowed vec of owned elements into an owned vec of borrowed elements. diff --git a/src/bin/ion/main.rs b/src/bin/ion/main.rs index 879ba13..798867b 100644 --- a/src/bin/ion/main.rs +++ b/src/bin/ion/main.rs @@ -1,3 +1,4 @@ +mod ansi_codes; mod auto_decompress; mod commands; mod file_writer;