use crate::{
apis::ManagedApis,
output::{
Styles,
headers::{GENERATING, HEADER_WIDTH},
},
spec_files_blessed::{BlessedApiSpecFile, BlessedFiles},
spec_files_generated::GeneratedFiles,
spec_files_generic::ApiSpecFilesBuilder,
spec_files_local::{LocalFiles, walk_local_directory},
vcs::{RepoVcs, RepoVcsKind, VcsRevision},
};
use anyhow::Context;
use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
use owo_colors::OwoColorize;
const DEFAULT_GIT_BRANCH: &str = "origin/main";
const DEFAULT_JJ_REVSET: &str = "trunk()";
#[derive(Clone, Debug)]
pub struct Environment {
pub(crate) command: String,
pub(crate) repo_root: Utf8PathBuf,
pub(crate) default_openapi_dir: Utf8PathBuf,
pub(crate) default_git_branch: String,
pub(crate) default_jj_revset: String,
pub(crate) vcs: RepoVcs,
}
impl Environment {
pub fn new(
command: impl Into<String>,
repo_root: impl Into<Utf8PathBuf>,
default_openapi_dir: impl Into<Utf8PathBuf>,
) -> anyhow::Result<Self> {
let command = command.into();
let repo_root = repo_root.into();
let default_openapi_dir = default_openapi_dir.into();
validate_paths(&repo_root, &default_openapi_dir)?;
let vcs = RepoVcs::detect(&repo_root)?;
Ok(Self {
repo_root,
default_openapi_dir,
default_git_branch: DEFAULT_GIT_BRANCH.to_owned(),
default_jj_revset: DEFAULT_JJ_REVSET.to_owned(),
command,
vcs,
})
}
pub fn with_default_git_branch(
mut self,
branch: impl Into<String>,
) -> Self {
self.default_git_branch = branch.into();
self
}
pub fn with_default_jj_revset(mut self, revset: impl Into<String>) -> Self {
self.default_jj_revset = revset.into();
self
}
#[cfg(test)]
pub(crate) fn new_for_test(
command: impl Into<String>,
repo_root: impl Into<Utf8PathBuf>,
default_openapi_dir: impl Into<Utf8PathBuf>,
) -> anyhow::Result<Self> {
let command = command.into();
let repo_root = repo_root.into();
let default_openapi_dir = default_openapi_dir.into();
validate_paths(&repo_root, &default_openapi_dir)?;
let vcs = RepoVcs::git()?;
Ok(Self {
repo_root,
default_openapi_dir,
default_git_branch: DEFAULT_GIT_BRANCH.to_owned(),
default_jj_revset: DEFAULT_JJ_REVSET.to_owned(),
command,
vcs,
})
}
pub(crate) fn resolve(
&self,
openapi_dir: Option<Utf8PathBuf>,
) -> anyhow::Result<ResolvedEnv> {
let (abs_dir, rel_dir) = match &openapi_dir {
Some(provided_dir) => {
let abs_dir = camino::absolute_utf8(provided_dir)
.with_context(|| {
format!(
"error making provided OpenAPI directory \
absolute: {}",
provided_dir
)
})?;
let rel_dir = abs_dir
.strip_prefix(&self.repo_root)
.with_context(|| {
format!(
"provided OpenAPI directory {} is not a \
subdirectory of repository root {}",
abs_dir, self.repo_root
)
})?
.to_path_buf();
(abs_dir, rel_dir)
}
None => {
let rel_dir = self.default_openapi_dir.clone();
let abs_dir = self.repo_root.join(&rel_dir);
(abs_dir, rel_dir)
}
};
let default_blessed_branch = match self.vcs.kind() {
RepoVcsKind::Git => self.default_git_branch.clone(),
RepoVcsKind::Jj => self.default_jj_revset.clone(),
};
Ok(ResolvedEnv {
command: self.command.clone(),
repo_root: self.repo_root.clone(),
local_source: LocalSource::Directory { abs_dir, rel_dir },
default_blessed_branch,
vcs: self.vcs.clone(),
})
}
}
fn validate_paths(
repo_root: &Utf8Path,
default_openapi_dir: &Utf8Path,
) -> anyhow::Result<()> {
if !repo_root.is_absolute() {
return Err(anyhow::anyhow!(
"repo_root must be an absolute path, found: {}",
repo_root
));
}
if !is_normal_relative(default_openapi_dir) {
return Err(anyhow::anyhow!(
"default_openapi_dir must be a relative path with \
normal components, found: {}",
default_openapi_dir
));
}
Ok(())
}
fn is_normal_relative(default_openapi_dir: &Utf8Path) -> bool {
default_openapi_dir
.components()
.all(|c| matches!(c, Utf8Component::Normal(_) | Utf8Component::CurDir))
}
#[derive(Debug)]
pub(crate) struct ResolvedEnv {
pub(crate) command: String,
pub(crate) repo_root: Utf8PathBuf,
pub(crate) local_source: LocalSource,
pub(crate) default_blessed_branch: String,
pub(crate) vcs: RepoVcs,
}
impl ResolvedEnv {
pub(crate) fn openapi_abs_dir(&self) -> &Utf8Path {
match &self.local_source {
LocalSource::Directory { abs_dir, .. } => abs_dir,
}
}
pub(crate) fn openapi_rel_dir(&self) -> &Utf8Path {
match &self.local_source {
LocalSource::Directory { rel_dir, .. } => rel_dir,
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum BlessedSource {
VcsRevisionMergeBase { revision: VcsRevision, directory: Utf8PathBuf },
Directory { local_directory: Utf8PathBuf },
}
impl BlessedSource {
pub fn load(
&self,
repo_root: &Utf8Path,
apis: &ManagedApis,
styles: &Styles,
vcs: &RepoVcs,
) -> anyhow::Result<(BlessedFiles, ErrorAccumulator)> {
let mut errors = ErrorAccumulator::new();
match self {
BlessedSource::Directory { local_directory } => {
eprintln!(
"{:>HEADER_WIDTH$} blessed OpenAPI documents from {:?}",
"Loading".style(styles.success_header),
local_directory,
);
let api_files: ApiSpecFilesBuilder<'_, BlessedApiSpecFile> =
walk_local_directory(
local_directory,
apis,
&mut errors,
repo_root,
vcs,
)?;
Ok((BlessedFiles::from(api_files), errors))
}
BlessedSource::VcsRevisionMergeBase { revision, directory } => {
eprintln!(
"{:>HEADER_WIDTH$} blessed OpenAPI documents from VCS \
revision {:?} path {:?}",
"Loading".style(styles.success_header),
revision,
directory
);
Ok((
BlessedFiles::load_from_vcs_parent_branch(
repo_root,
revision,
directory,
apis,
&mut errors,
vcs,
)?,
errors,
))
}
}
}
}
#[derive(Debug)]
pub enum GeneratedSource {
Generated,
Directory { local_directory: Utf8PathBuf },
}
impl GeneratedSource {
pub fn load(
&self,
apis: &ManagedApis,
styles: &Styles,
repo_root: &Utf8Path,
vcs: &RepoVcs,
) -> anyhow::Result<(GeneratedFiles, ErrorAccumulator)> {
let mut errors = ErrorAccumulator::new();
match self {
GeneratedSource::Generated => {
eprintln!(
"{:>HEADER_WIDTH$} OpenAPI documents from API \
definitions ... ",
GENERATING.style(styles.success_header)
);
Ok((GeneratedFiles::generate(apis, &mut errors)?, errors))
}
GeneratedSource::Directory { local_directory } => {
eprintln!(
"{:>HEADER_WIDTH$} \"generated\" OpenAPI documents from \
{:?} ... ",
"Loading".style(styles.success_header),
local_directory,
);
let api_files = walk_local_directory(
local_directory,
apis,
&mut errors,
repo_root,
vcs,
)?;
Ok((GeneratedFiles::from(api_files), errors))
}
}
}
}
#[derive(Debug)]
pub enum LocalSource {
Directory {
abs_dir: Utf8PathBuf,
rel_dir: Utf8PathBuf,
},
}
impl LocalSource {
pub fn load(
&self,
apis: &ManagedApis,
styles: &Styles,
repo_root: &Utf8Path,
vcs: &RepoVcs,
) -> anyhow::Result<(LocalFiles, ErrorAccumulator)> {
let mut errors = ErrorAccumulator::new();
let any_uses_git_stub =
apis.iter_apis().any(|a| apis.uses_git_stub_storage(a));
if any_uses_git_stub && vcs.is_shallow_clone(repo_root) {
errors.error(anyhow::anyhow!(
"this repository is a shallow clone, but Git stub storage is \
enabled for some APIs. Git stubs cannot be resolved in a \
shallow clone because the referenced commits may not be \
available. To fix this, fetch complete history (e.g. \
`git fetch --unshallow`) or make a fresh clone without \
--depth."
));
return Ok((LocalFiles::default(), errors));
}
match self {
LocalSource::Directory { abs_dir, .. } => {
eprintln!(
"{:>HEADER_WIDTH$} local OpenAPI documents from \
{:?} ... ",
"Loading".style(styles.success_header),
abs_dir,
);
Ok((
LocalFiles::load_from_directory(
abs_dir,
apis,
&mut errors,
repo_root,
vcs,
)?,
errors,
))
}
}
}
}
pub struct ErrorAccumulator {
errors: Vec<anyhow::Error>,
warnings: Vec<anyhow::Error>,
}
impl ErrorAccumulator {
pub fn new() -> ErrorAccumulator {
ErrorAccumulator { errors: Vec::new(), warnings: Vec::new() }
}
pub fn error(&mut self, error: anyhow::Error) {
self.errors.push(error);
}
pub fn warning(&mut self, error: anyhow::Error) {
self.warnings.push(error);
}
pub fn iter_errors(&self) -> impl Iterator<Item = &'_ anyhow::Error> + '_ {
self.errors.iter()
}
pub fn iter_warnings(
&self,
) -> impl Iterator<Item = &'_ anyhow::Error> + '_ {
self.warnings.iter()
}
}