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

Adds 'report' output for ion schema validate #160

Merged
merged 1 commit into from
Oct 24, 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
14 changes: 14 additions & 0 deletions src/bin/ion/ansi_codes.rs
Original file line number Diff line number Diff line change
@@ -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";
2 changes: 1 addition & 1 deletion src/bin/ion/commands/complaint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ impl IonCliCommand for SucksCommand {
command.hide(true)
}

fn run(&self, command_path: &mut Vec<String>, args: &ArgMatches) -> anyhow::Result<()> {
fn run(&self, _command_path: &mut Vec<String>, _args: &ArgMatches) -> anyhow::Result<()> {
println!(
"
We're very sorry to hear that!
Expand Down
133 changes: 113 additions & 20 deletions src/bin/ion/commands/schema/validate.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,54 @@
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};
use ion_rs::{ion_sexp, AnyEncoding, ElementReader, Reader, SequenceWriter, TextFormat, Writer};
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<String> = 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"
Expand All @@ -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()
Expand All @@ -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),
)
}

Expand All @@ -71,42 +115,46 @@ 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::<IonResult<_>>()?;
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 => {
let reader = Reader::new(AnyEncoding, input.into_source())?;
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)?;
}
}
}
Expand All @@ -124,15 +172,59 @@ impl IonCliCommand for ValidateCommand {
}
}

enum ResultWriter {
Quiet,
Ion,
Report(String),
}
impl ResultWriter {
fn write_result(
&mut self,
w: &mut Writer<v1_0::Text, &mut CommandOutput<'_>>,
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: `<input name> ... <ok|FAILED>`
///
/// 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<v1_0::Text, &mut CommandOutput<'_>>,
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<W: ValueWriter>(
fn write_validation_result_ion<W: ValueWriter>(
validation_result: ValidationResult,
writer: W,
) -> IonResult<()> {
) -> Result<()> {
match validation_result {
Ok(_) => writer.write_sexp(vec![&Element::symbol("valid")]),
Err(violation) => {
Expand All @@ -152,7 +244,8 @@ fn write_validation_result<W: ValueWriter>(

writer.write_sexp(vec_of_refs(&violations))
}
}
}?;
Ok(())
}

/// Transposes a borrowed vec of owned elements into an owned vec of borrowed elements.
Expand Down
1 change: 1 addition & 0 deletions src/bin/ion/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod ansi_codes;
mod auto_decompress;
mod commands;
mod file_writer;
Expand Down
Loading