Skip to content

Commit

Permalink
[4/6] [openapi-manager] richer extra validation (#6370)
Browse files Browse the repository at this point in the history
With the Nexus external API, the validator generates a `nexus_tags.txt` file
that must be kept track of. Instead of the validation function simply erroring
out if the file is different, it is a better experience for users if it records
what it expects the file to be, and then the OpenAPI manager simply treats it
as an extra file similar to the document itself.

With this pattern, the check and generate functions can both work on the extra
file just like they work on the document.

In order for the there to be a richer protocol for validation, the interface
needs to be split into its own crate. This way, the API crates can depend on
this minimal interface, and the OpenAPI manager itself can depend on the API
crates.

This isn't used yet, but will be in #6373.
  • Loading branch information
sunshowers authored Aug 21, 2024
1 parent ab29a02 commit 6dde3f8
Show file tree
Hide file tree
Showing 12 changed files with 453 additions and 125 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ members = [
"dev-tools/omicron-dev",
"dev-tools/omicron-dev-lib",
"dev-tools/openapi-manager",
"dev-tools/openapi-manager/types",
"dev-tools/oxlog",
"dev-tools/reconfigurator-cli",
"dev-tools/releng",
Expand Down Expand Up @@ -145,6 +146,7 @@ default-members = [
"dev-tools/omicron-dev",
"dev-tools/omicron-dev-lib",
"dev-tools/openapi-manager",
"dev-tools/openapi-manager/types",
"dev-tools/oxlog",
"dev-tools/reconfigurator-cli",
"dev-tools/releng",
Expand Down Expand Up @@ -453,6 +455,7 @@ oxlog = { path = "dev-tools/oxlog" }
oxnet = { git = "https://github.com/oxidecomputer/oxnet" }
once_cell = "1.19.0"
openapi-lint = { git = "https://github.com/oxidecomputer/openapi-lint", branch = "main" }
openapi-manager-types = { path = "dev-tools/openapi-manager/types" }
openapiv3 = "2.0.0"
# must match samael's crate!
openssl = "0.10"
Expand Down
1 change: 1 addition & 0 deletions dev-tools/openapi-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ nexus-internal-api.workspace = true
omicron-workspace-hack.workspace = true
openapiv3.workspace = true
openapi-lint.workspace = true
openapi-manager-types.workspace = true
owo-colors.workspace = true
oximeter-api.workspace = true
serde_json.workspace = true
Expand Down
125 changes: 78 additions & 47 deletions dev-tools/openapi-manager/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@
use std::{io::Write, process::ExitCode};

use anyhow::Result;
use camino::Utf8Path;
use indent_write::io::IndentWriter;
use owo_colors::OwoColorize;
use similar::TextDiff;

use crate::{
output::{
display_api_spec, display_error, display_summary, headers::*, plural,
write_diff, OutputOpts, Styles,
display_api_spec, display_api_spec_file, display_error,
display_summary, headers::*, plural, write_diff, OutputOpts, Styles,
},
spec::{all_apis, CheckStatus},
spec::{all_apis, CheckStale, Environment},
FAILURE_EXIT_CODE, NEEDS_UPDATE_EXIT_CODE,
};

Expand All @@ -37,7 +36,7 @@ impl CheckResult {
}

pub(crate) fn check_impl(
dir: &Utf8Path,
env: &Environment,
output: &OutputOpts,
) -> Result<CheckResult> {
let mut styles = Styles::default();
Expand All @@ -48,6 +47,7 @@ pub(crate) fn check_impl(
let all_apis = all_apis();
let total = all_apis.len();
let count_width = total.to_string().len();
let count_section_indent = count_section_indent(count_width);
let continued_indent = continued_indent(count_width);

eprintln!("{:>HEADER_WIDTH$}", SEPARATOR);
Expand All @@ -58,57 +58,89 @@ pub(crate) fn check_impl(
total.style(styles.bold),
plural::documents(total),
);
let mut num_up_to_date = 0;
let mut num_fresh = 0;
let mut num_stale = 0;
let mut num_missing = 0;
let mut num_failed = 0;

for (ix, spec) in all_apis.iter().enumerate() {
let count = ix + 1;

match spec.check(&dir) {
Ok(status) => match status {
CheckStatus::Ok(summary) => {
eprintln!(
"{:>HEADER_WIDTH$} [{count:>count_width$}/{total}] {}: {}",
UP_TO_DATE.style(styles.success_header),
display_api_spec(spec, &styles),
display_summary(&summary, &styles),
);
match spec.check(env) {
Ok(status) => {
let total_errors = status.total_errors();
let total_errors_width = total_errors.to_string().len();

if total_errors == 0 {
// Success case.
let extra = if status.extra_files_len() > 0 {
format!(
", {} extra files",
status.extra_files_len().style(styles.bold)
)
} else {
"".to_string()
};

num_up_to_date += 1;
}
CheckStatus::Stale { full_path, actual, expected } => {
eprintln!(
"{:>HEADER_WIDTH$} [{count:>count_width$}/{total}] {}",
STALE.style(styles.warning_header),
"{:>HEADER_WIDTH$} [{count:>count_width$}/{total}] {}: {}{extra}",
FRESH.style(styles.success_header),
display_api_spec(spec, &styles),
display_summary(&status.summary, &styles),
);

let diff = TextDiff::from_lines(&actual, &expected);
write_diff(
&diff,
&full_path,
&styles,
// Add an indent to align diff with the status message.
&mut IndentWriter::new(
&continued_indent,
std::io::stderr(),
),
)?;

num_stale += 1;
num_fresh += 1;
continue;
}
CheckStatus::Missing => {
eprintln!(
"{:>HEADER_WIDTH$} [{count:>count_width$}/{total}] {}",
MISSING.style(styles.warning_header),
display_api_spec(spec, &styles),
);

num_missing += 1;
// Out of date: print errors.
eprintln!(
"{:>HEADER_WIDTH$} [{count:>count_width$}/{total}] {}",
STALE.style(styles.warning_header),
display_api_spec(spec, &styles),
);
num_stale += 1;

for (error_ix, (spec_file, error)) in
status.iter_errors().enumerate()
{
let error_count = error_ix + 1;

let display_heading = |heading: &str| {
eprintln!(
"{:>HEADER_WIDTH$}{count_section_indent}\
({error_count:>total_errors_width$}/{total_errors}) {}",
heading.style(styles.warning_header),
display_api_spec_file(spec, spec_file, &styles),
);
};

match error {
CheckStale::Modified {
full_path,
actual,
expected,
} => {
display_heading(MODIFIED);

let diff =
TextDiff::from_lines(&**actual, &**expected);
write_diff(
&diff,
&full_path,
&styles,
// Add an indent to align diff with the status message.
&mut IndentWriter::new(
&continued_indent,
std::io::stderr(),
),
)?;
}
CheckStale::New => {
display_heading(NEW);
}
}
}
},
}
Err(error) => {
eprint!(
"{:>HEADER_WIDTH$} [{count:>count_width$}/{total}] {}",
Expand Down Expand Up @@ -138,13 +170,12 @@ pub(crate) fn check_impl(
};

eprintln!(
"{:>HEADER_WIDTH$} {} {} checked: {} up-to-date, {} stale, {} missing, {} failed",
"{:>HEADER_WIDTH$} {} {} checked: {} fresh, {} stale, {} failed",
status_header,
total.style(styles.bold),
plural::documents(total),
num_up_to_date.style(styles.bold),
num_fresh.style(styles.bold),
num_stale.style(styles.bold),
num_missing.style(styles.bold),
num_failed.style(styles.bold),
);
if num_failed > 0 {
Expand All @@ -170,14 +201,14 @@ pub(crate) fn check_impl(
mod tests {
use std::process::ExitCode;

use crate::spec::find_openapi_dir;
use crate::spec::Environment;

use super::*;

#[test]
fn check_apis_up_to_date() -> Result<ExitCode> {
let output = OutputOpts { color: clap::ColorChoice::Auto };
let dir = find_openapi_dir()?;
let dir = Environment::new(None)?;

let result = check_impl(&dir, &output)?;
Ok(result.to_exit_code())
Expand Down
8 changes: 4 additions & 4 deletions dev-tools/openapi-manager/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use clap::{Args, Parser, Subcommand};

use crate::{
check::check_impl, generate::generate_impl, list::list_impl,
output::OutputOpts, spec::openapi_dir,
output::OutputOpts, spec::Environment,
};

/// Manage OpenAPI specifications.
Expand Down Expand Up @@ -73,7 +73,7 @@ pub struct GenerateArgs {

impl GenerateArgs {
fn exec(self, output: &OutputOpts) -> anyhow::Result<ExitCode> {
let dir = openapi_dir(self.dir)?;
let dir = Environment::new(self.dir)?;
Ok(generate_impl(&dir, output)?.to_exit_code())
}
}
Expand All @@ -87,8 +87,8 @@ pub struct CheckArgs {

impl CheckArgs {
fn exec(self, output: &OutputOpts) -> anyhow::Result<ExitCode> {
let dir = openapi_dir(self.dir)?;
Ok(check_impl(&dir, output)?.to_exit_code())
let env = Environment::new(self.dir)?;
Ok(check_impl(&env, output)?.to_exit_code())
}
}

Expand Down
26 changes: 14 additions & 12 deletions dev-tools/openapi-manager/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use std::{io::Write, process::ExitCode};

use anyhow::Result;
use camino::Utf8Path;
use indent_write::io::IndentWriter;
use owo_colors::OwoColorize;

Expand All @@ -14,7 +13,7 @@ use crate::{
display_api_spec, display_error, display_summary, headers::*, plural,
OutputOpts, Styles,
},
spec::{all_apis, OverwriteStatus},
spec::{all_apis, Environment},
FAILURE_EXIT_CODE,
};

Expand All @@ -34,7 +33,7 @@ impl GenerateResult {
}

pub(crate) fn generate_impl(
dir: &Utf8Path,
env: &Environment,
output: &OutputOpts,
) -> Result<GenerateResult> {
let mut styles = Styles::default();
Expand Down Expand Up @@ -62,27 +61,30 @@ pub(crate) fn generate_impl(
for (ix, spec) in all_apis.iter().enumerate() {
let count = ix + 1;

match spec.overwrite(&dir) {
Ok((status, summary)) => match status {
OverwriteStatus::Updated => {
match spec.overwrite(env) {
Ok(status) => {
let updated_count = status.updated_count();

if updated_count > 0 {
eprintln!(
"{:>HEADER_WIDTH$} [{count:>count_width$}/{total}] {}: {}",
"{:>HEADER_WIDTH$} [{count:>count_width$}/{total}] {}: {} ({} {} updated)",
UPDATED.style(styles.success_header),
display_api_spec(spec, &styles),
display_summary(&summary, &styles),
display_summary(&status.summary, &styles),
updated_count.style(styles.bold),
plural::files(updated_count),
);
num_updated += 1;
}
OverwriteStatus::Unchanged => {
} else {
eprintln!(
"{:>HEADER_WIDTH$} [{count:>count_width$}/{total}] {}: {}",
UNCHANGED.style(styles.unchanged_header),
display_api_spec(spec, &styles),
display_summary(&summary, &styles),
display_summary(&status.summary, &styles),
);
num_unchanged += 1;
}
},
}
Err(err) => {
eprintln!(
"{:>HEADER_WIDTH$} [{count:>count_width$}/{total}] {}",
Expand Down
Loading

0 comments on commit 6dde3f8

Please sign in to comment.