use crate::{
apis::ManagedApis,
cmd::{
check::check_impl, debug::debug_impl, generate::generate_impl,
list::list_impl,
},
environment::{BlessedSource, Environment, GeneratedSource, ResolvedEnv},
output::OutputOpts,
vcs::VcsRevision,
};
use anyhow::Result;
use camino::Utf8PathBuf;
use clap::{Args, Parser, Subcommand};
use std::process::ExitCode;
#[derive(Debug, Parser)]
pub struct App {
#[clap(flatten)]
output_opts: OutputOpts,
#[clap(subcommand)]
command: Command,
}
impl App {
pub fn exec(self, env: &Environment, apis: &ManagedApis) -> ExitCode {
let result = match self.command {
Command::Debug(args) => args.exec(env, apis, &self.output_opts),
Command::List(args) => args.exec(apis, &self.output_opts),
Command::Generate(args) => args.exec(env, apis, &self.output_opts),
Command::Check(args) => args.exec(env, apis, &self.output_opts),
};
match result {
Ok(exit_code) => exit_code,
Err(error) => {
eprintln!("failure: {:#}", error);
ExitCode::FAILURE
}
}
}
}
#[derive(Debug, Subcommand)]
pub enum Command {
Debug(DebugArgs),
List(ListArgs),
Generate(GenerateArgs),
Check(CheckArgs),
}
#[derive(Debug, Args)]
pub struct BlessedSourceArgs {
#[clap(
long = "blessed-from-vcs",
alias = "blessed-from-git",
env(BLESSED_FROM_VCS_ENV),
value_name("REVISION")
)]
pub blessed_from_vcs: Option<String>,
#[clap(long, env(BLESSED_FROM_VCS_PATH_ENV), value_name("PATH"))]
pub blessed_from_vcs_path: Option<Utf8PathBuf>,
#[clap(
long,
conflicts_with("blessed_from_vcs"),
env("OPENAPI_MGR_BLESSED_FROM_DIR"),
value_name("DIRECTORY")
)]
pub blessed_from_dir: Option<Utf8PathBuf>,
}
const BLESSED_FROM_VCS_ENV: &str = "OPENAPI_MGR_BLESSED_FROM_VCS";
const BLESSED_FROM_VCS_PATH_ENV: &str = "OPENAPI_MGR_BLESSED_FROM_VCS_PATH";
const BLESSED_FROM_GIT_ENV: &str = "OPENAPI_MGR_BLESSED_FROM_GIT";
impl BlessedSourceArgs {
pub(crate) fn to_blessed_source(
&self,
env: &ResolvedEnv,
) -> Result<BlessedSource, anyhow::Error> {
assert!(
self.blessed_from_dir.is_none() || self.blessed_from_vcs.is_none()
);
if let Some(local_directory) = &self.blessed_from_dir {
return Ok(BlessedSource::Directory {
local_directory: local_directory.clone(),
});
}
let resolved =
resolve_blessed_from_vcs(self.blessed_from_vcs.as_deref());
let revision_str = match &resolved {
Some(revision) => revision.as_str(),
None => env.default_blessed_branch.as_str(),
};
let revision = VcsRevision::from(String::from(revision_str));
let directory = match &self.blessed_from_vcs_path {
Some(path) => path.clone(),
None => Utf8PathBuf::from(env.openapi_rel_dir()),
};
Ok(BlessedSource::VcsRevisionMergeBase { revision, directory })
}
}
fn resolve_blessed_from_vcs(cli_value: Option<&str>) -> Option<String> {
if let Some(v) = cli_value {
return Some(v.to_owned());
}
if let Ok(v) = std::env::var(BLESSED_FROM_GIT_ENV) {
return Some(v);
}
None
}
#[derive(Debug, Args)]
pub struct GeneratedSourceArgs {
#[clap(long, value_name("DIRECTORY"))]
pub generated_from_dir: Option<Utf8PathBuf>,
}
impl From<GeneratedSourceArgs> for GeneratedSource {
fn from(value: GeneratedSourceArgs) -> Self {
match value.generated_from_dir {
Some(local_directory) => {
GeneratedSource::Directory { local_directory }
}
None => GeneratedSource::Generated,
}
}
}
#[derive(Debug, Args)]
pub struct LocalSourceArgs {
#[clap(long, env("OPENAPI_MGR_DIR"), value_name("DIRECTORY"))]
dir: Option<Utf8PathBuf>,
}
#[derive(Debug, Args)]
pub struct DebugArgs {
#[clap(flatten)]
local: LocalSourceArgs,
#[clap(flatten)]
blessed: BlessedSourceArgs,
#[clap(flatten)]
generated: GeneratedSourceArgs,
}
impl DebugArgs {
fn exec(
self,
env: &Environment,
apis: &ManagedApis,
output: &OutputOpts,
) -> anyhow::Result<ExitCode> {
let env = env.resolve(self.local.dir)?;
let blessed_source = self.blessed.to_blessed_source(&env)?;
let generated_source = GeneratedSource::from(self.generated);
debug_impl(apis, &env, &blessed_source, &generated_source, output)?;
Ok(ExitCode::SUCCESS)
}
}
#[derive(Debug, Args)]
pub struct ListArgs {
#[clap(long, short)]
verbose: bool,
}
impl ListArgs {
fn exec(
self,
apis: &ManagedApis,
output: &OutputOpts,
) -> anyhow::Result<ExitCode> {
list_impl(apis, self.verbose, output)?;
Ok(ExitCode::SUCCESS)
}
}
#[derive(Debug, Args)]
pub struct GenerateArgs {
#[clap(flatten)]
local: LocalSourceArgs,
#[clap(flatten)]
blessed: BlessedSourceArgs,
#[clap(flatten)]
generated: GeneratedSourceArgs,
}
impl GenerateArgs {
fn exec(
self,
env: &Environment,
apis: &ManagedApis,
output: &OutputOpts,
) -> anyhow::Result<ExitCode> {
let env = env.resolve(self.local.dir)?;
let blessed_source = self.blessed.to_blessed_source(&env)?;
let generated_source = GeneratedSource::from(self.generated);
Ok(generate_impl(
apis,
&env,
&blessed_source,
&generated_source,
output,
)?
.to_exit_code())
}
}
#[derive(Debug, Args)]
pub struct CheckArgs {
#[clap(flatten)]
local: LocalSourceArgs,
#[clap(flatten)]
blessed: BlessedSourceArgs,
#[clap(flatten)]
generated: GeneratedSourceArgs,
}
impl CheckArgs {
fn exec(
self,
env: &Environment,
apis: &ManagedApis,
output: &OutputOpts,
) -> anyhow::Result<ExitCode> {
let env = env.resolve(self.local.dir)?;
let blessed_source = self.blessed.to_blessed_source(&env)?;
let generated_source = GeneratedSource::from(self.generated);
Ok(check_impl(apis, &env, &blessed_source, &generated_source, output)?
.to_exit_code())
}
}
pub const NEEDS_UPDATE_EXIT_CODE: u8 = 4;
pub const FAILURE_EXIT_CODE: u8 = 100;
#[cfg(test)]
mod test {
use super::*;
use crate::{
environment::{
BlessedSource, Environment, GeneratedSource, ResolvedEnv,
},
vcs::VcsRevision,
};
use assert_matches::assert_matches;
use camino::{Utf8Path, Utf8PathBuf};
use clap::Parser;
#[test]
fn test_arg_parsing() {
let app = App::parse_from(["dummy", "check"]);
assert_matches!(
app.command,
Command::Check(CheckArgs {
local: LocalSourceArgs { dir: None },
blessed: BlessedSourceArgs {
blessed_from_vcs: None,
blessed_from_vcs_path: None,
blessed_from_dir: None
},
generated: GeneratedSourceArgs { generated_from_dir: None },
})
);
let app = App::parse_from(["dummy", "check", "--dir", "foo"]);
assert_matches!(app.command, Command::Check(CheckArgs {
local: LocalSourceArgs { dir: Some(local_dir) },
blessed:
BlessedSourceArgs { blessed_from_vcs: None, blessed_from_vcs_path: None, blessed_from_dir: None },
generated: GeneratedSourceArgs { generated_from_dir: None },
}) if local_dir == "foo");
let app = App::parse_from([
"dummy",
"check",
"--dir",
"foo",
"--generated-from-dir",
"bar",
]);
assert_matches!(app.command, Command::Check(CheckArgs {
local: LocalSourceArgs { dir: Some(local_dir) },
blessed:
BlessedSourceArgs { blessed_from_vcs: None, blessed_from_vcs_path: None, blessed_from_dir: None },
generated: GeneratedSourceArgs { generated_from_dir: Some(generated_dir) },
}) if local_dir == "foo" && generated_dir == "bar");
let app = App::parse_from([
"dummy",
"check",
"--dir",
"foo",
"--generated-from-dir",
"bar",
"--blessed-from-dir",
"baz",
]);
assert_matches!(app.command, Command::Check(CheckArgs {
local: LocalSourceArgs { dir: Some(local_dir) },
blessed:
BlessedSourceArgs { blessed_from_vcs: None, blessed_from_vcs_path: None, blessed_from_dir: Some(blessed_dir) },
generated: GeneratedSourceArgs { generated_from_dir: Some(generated_dir) },
}) if local_dir == "foo" && generated_dir == "bar" && blessed_dir == "baz");
let app = App::parse_from([
"dummy",
"check",
"--blessed-from-git",
"some/other/upstream",
]);
assert_matches!(app.command, Command::Check(CheckArgs {
local: LocalSourceArgs { dir: None },
blessed:
BlessedSourceArgs { blessed_from_vcs: Some(git), blessed_from_vcs_path: None, blessed_from_dir: None },
generated: GeneratedSourceArgs { generated_from_dir: None },
}) if git == "some/other/upstream");
let error = App::try_parse_from([
"dummy",
"check",
"--blessed-from-vcs",
"vcs_revision",
"--blessed-from-dir",
"dir",
])
.unwrap_err();
assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict);
assert!(error.to_string().contains(
"error: the argument '--blessed-from-vcs <REVISION>' \
cannot be used with '--blessed-from-dir <DIRECTORY>"
));
}
#[test]
fn test_local_args() {
#[cfg(unix)]
const ABS_DIR: &str = "/tmp";
#[cfg(windows)]
const ABS_DIR: &str = "C:\\tmp";
{
let env = Environment::new_for_test(
"cargo openapi".to_owned(),
Utf8PathBuf::from(ABS_DIR),
Utf8PathBuf::from("foo"),
)
.expect("loading environment");
let env = env.resolve(None).expect("resolving environment");
assert_eq!(
env.openapi_abs_dir(),
Utf8Path::new(ABS_DIR).join("foo")
);
}
{
let error = Environment::new_for_test(
"cargo openapi".to_owned(),
Utf8PathBuf::from(ABS_DIR),
Utf8PathBuf::from(ABS_DIR),
)
.unwrap_err();
assert_eq!(
error.to_string(),
format!(
"default_openapi_dir must be a relative path with \
normal components, found: {}",
ABS_DIR
)
);
}
{
let current_dir =
Utf8PathBuf::try_from(std::env::current_dir().unwrap())
.unwrap();
let env = Environment::new_for_test(
"cargo openapi".to_owned(),
current_dir.clone(),
Utf8PathBuf::from("foo"),
)
.expect("loading environment");
let env = env
.resolve(Some(Utf8PathBuf::from("bar")))
.expect("resolving environment");
assert_eq!(env.openapi_abs_dir(), current_dir.join("bar"));
}
}
#[test]
fn test_generated_args() {
let source = GeneratedSource::from(GeneratedSourceArgs {
generated_from_dir: None,
});
assert_matches!(source, GeneratedSource::Generated);
let source = GeneratedSource::from(GeneratedSourceArgs {
generated_from_dir: Some(Utf8PathBuf::from("/tmp")),
});
assert_matches!(
source,
GeneratedSource::Directory { local_directory }
if local_directory == "/tmp"
);
}
#[test]
fn test_blessed_args() {
#[cfg(unix)]
const ABS_DIR: &str = "/tmp";
#[cfg(windows)]
const ABS_DIR: &str = "C:\\tmp";
unsafe {
std::env::remove_var(BLESSED_FROM_VCS_ENV);
std::env::remove_var(BLESSED_FROM_VCS_PATH_ENV);
std::env::remove_var(BLESSED_FROM_GIT_ENV);
}
let env =
Environment::new_for_test("cargo openapi", ABS_DIR, "foo-openapi")
.unwrap()
.with_default_git_branch("upstream/dev".to_owned());
let env = env.resolve(None).unwrap();
let source = BlessedSourceArgs {
blessed_from_vcs: None,
blessed_from_vcs_path: None,
blessed_from_dir: None,
}
.to_blessed_source(&env)
.unwrap();
assert_matches!(
source,
BlessedSource::VcsRevisionMergeBase { revision, directory }
if *revision == "upstream/dev" && directory == "foo-openapi"
);
let source = BlessedSourceArgs {
blessed_from_vcs: Some(String::from("my/other/main")),
blessed_from_vcs_path: None,
blessed_from_dir: None,
}
.to_blessed_source(&env)
.unwrap();
assert_matches!(
source,
BlessedSource::VcsRevisionMergeBase { revision, directory}
if *revision == "my/other/main" && directory == "foo-openapi"
);
let source = BlessedSourceArgs {
blessed_from_vcs: Some(String::from("my/other/main")),
blessed_from_vcs_path: Some(Utf8PathBuf::from("other_openapi/bar")),
blessed_from_dir: None,
}
.to_blessed_source(&env)
.unwrap();
assert_matches!(
source,
BlessedSource::VcsRevisionMergeBase { revision, directory}
if *revision == "my/other/main" &&
directory == "other_openapi/bar"
);
let source = BlessedSourceArgs {
blessed_from_vcs: None,
blessed_from_vcs_path: None,
blessed_from_dir: Some(Utf8PathBuf::from("/tmp")),
}
.to_blessed_source(&env)
.unwrap();
assert_matches!(
source,
BlessedSource::Directory { local_directory }
if local_directory == "/tmp"
);
}
fn parse_blessed_source(
env: &ResolvedEnv,
extra_args: &[&str],
) -> BlessedSource {
let mut args = vec!["dummy", "check"];
args.extend_from_slice(extra_args);
let app = App::parse_from(args);
match app.command {
Command::Check(check_args) => {
check_args.blessed.to_blessed_source(env).unwrap()
}
_ => panic!("expected Check command"),
}
}
#[test]
fn test_blessed_args_from_env_vars() {
#[cfg(unix)]
const ABS_DIR: &str = "/tmp";
#[cfg(windows)]
const ABS_DIR: &str = "C:\\tmp";
let env =
Environment::new_for_test("cargo openapi", ABS_DIR, "foo-openapi")
.unwrap()
.with_default_git_branch("upstream/dev".to_owned());
let env = env.resolve(None).unwrap();
unsafe {
std::env::remove_var(BLESSED_FROM_VCS_ENV);
std::env::remove_var(BLESSED_FROM_VCS_PATH_ENV);
std::env::remove_var(BLESSED_FROM_GIT_ENV);
}
unsafe {
std::env::set_var(BLESSED_FROM_VCS_ENV, "env-trunk");
}
assert_eq!(
parse_blessed_source(&env, &[]),
BlessedSource::VcsRevisionMergeBase {
revision: VcsRevision::from("env-trunk".to_owned()),
directory: Utf8PathBuf::from("foo-openapi"),
},
);
unsafe {
std::env::set_var(BLESSED_FROM_VCS_ENV, "env-trunk");
std::env::set_var(BLESSED_FROM_VCS_PATH_ENV, "custom-dir");
}
assert_eq!(
parse_blessed_source(&env, &[]),
BlessedSource::VcsRevisionMergeBase {
revision: VcsRevision::from("env-trunk".to_owned()),
directory: Utf8PathBuf::from("custom-dir"),
},
);
unsafe {
std::env::remove_var(BLESSED_FROM_VCS_PATH_ENV);
}
unsafe {
std::env::remove_var(BLESSED_FROM_VCS_ENV);
std::env::set_var(BLESSED_FROM_GIT_ENV, "origin/dev");
}
assert_eq!(
parse_blessed_source(&env, &[]),
BlessedSource::VcsRevisionMergeBase {
revision: VcsRevision::from("origin/dev".to_owned()),
directory: Utf8PathBuf::from("foo-openapi"),
},
);
unsafe {
std::env::set_var(BLESSED_FROM_VCS_ENV, "env-vcs");
std::env::set_var(BLESSED_FROM_GIT_ENV, "env-git");
}
assert_eq!(
parse_blessed_source(&env, &[]),
BlessedSource::VcsRevisionMergeBase {
revision: VcsRevision::from("env-vcs".to_owned()),
directory: Utf8PathBuf::from("foo-openapi"),
},
);
assert_eq!(
parse_blessed_source(&env, &["--blessed-from-vcs", "cli-override"]),
BlessedSource::VcsRevisionMergeBase {
revision: VcsRevision::from("cli-override".to_owned()),
directory: Utf8PathBuf::from("foo-openapi"),
},
);
}
}