use crate::{
apis::ManagedApis,
environment::ErrorAccumulator,
spec_files_generic::{
ApiFiles, ApiLoad, ApiSpecFile, ApiSpecFilesBuilder, AsRawFiles,
GitStubKey, SpecFileInfo, parse_versioned_file_name,
parse_versioned_git_stub_file_name,
},
vcs::{RepoVcs, VcsRevision},
};
use anyhow::{anyhow, bail};
use camino::{Utf8Path, Utf8PathBuf};
use dropshot_api_manager_types::{
ApiIdent, ApiSpecFileName, VersionedApiSpecFileName,
};
use git_stub::{GitCommitHash, GitStub};
use rayon::prelude::*;
use std::{collections::BTreeMap, ops::Deref};
pub struct BlessedApiSpecFile {
inner: ApiSpecFile,
versioned_name: VersionedApiSpecFileName,
}
impl std::fmt::Debug for BlessedApiSpecFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BlessedApiSpecFile")
.field("inner", &self.inner)
.finish()
}
}
impl Deref for BlessedApiSpecFile {
type Target = ApiSpecFile;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl BlessedApiSpecFile {
pub fn new(inner: ApiSpecFile) -> Self {
let versioned_name = inner
.spec_file_name()
.as_versioned()
.unwrap_or_else(|| {
panic!(
"BlessedApiSpecFile requires a versioned API spec, \
got lockstep: {}",
inner.spec_file_name()
)
})
.clone();
Self { inner, versioned_name }
}
pub fn versioned_spec_file_name(&self) -> &VersionedApiSpecFileName {
&self.versioned_name
}
}
impl ApiLoad for BlessedApiSpecFile {
const MISCONFIGURATIONS_ALLOWED: bool = true;
type Unparseable = std::convert::Infallible;
fn make_item(raw: ApiSpecFile) -> Self {
BlessedApiSpecFile::new(raw)
}
fn try_extend(&mut self, item: ApiSpecFile) -> anyhow::Result<()> {
bail!(
"found more than one blessed OpenAPI document for a given \
API version: at least {} and {}",
self.spec_file_name(),
item.spec_file_name()
);
}
fn make_unparseable(
_name: ApiSpecFileName,
_contents: Vec<u8>,
) -> Option<Self::Unparseable> {
None
}
fn unparseable_into_self(unparseable: Self::Unparseable) -> Self {
match unparseable {}
}
fn extend_unparseable(&mut self, unparseable: Self::Unparseable) {
match unparseable {}
}
}
impl AsRawFiles for BlessedApiSpecFile {
fn as_raw_files<'a>(
&'a self,
) -> Box<dyn Iterator<Item = &'a dyn SpecFileInfo> + 'a> {
Box::new(std::iter::once(self.deref() as &dyn SpecFileInfo))
}
}
#[derive(Clone, Debug)]
pub enum BlessedGitStub {
Known {
commit: GitCommitHash,
path: Utf8PathBuf,
},
Lazy {
commit: GitCommitHash,
path: Utf8PathBuf,
},
}
impl BlessedGitStub {
pub fn to_git_stub(
&self,
repo_root: &Utf8Path,
merge_base: Option<GitCommitHash>,
vcs: &RepoVcs,
) -> anyhow::Result<GitStub> {
match self {
BlessedGitStub::Known { commit, path } => {
if let Some(merge_base) = merge_base {
if !vcs.is_ancestor(repo_root, *commit, merge_base)? {
let commit = vcs.first_commit_for_file(
repo_root, merge_base, path,
)?;
return Ok(GitStub::new(commit, path.clone())?);
}
}
Ok(GitStub::new(*commit, path.clone())?)
}
BlessedGitStub::Lazy { commit, path } => {
let commit =
vcs.first_commit_for_file(repo_root, *commit, path)?;
Ok(GitStub::new(commit, path.clone())?)
}
}
}
}
enum BlessedPathKind<'a> {
Lockstep,
GitStubFile { api_dir: &'a str, basename: &'a str },
VersionedFile { api_dir: &'a str, basename: &'a str },
}
struct UnrecognizedPath;
impl<'a> BlessedPathKind<'a> {
fn parse(path: &'a Utf8Path) -> Result<Self, UnrecognizedPath> {
let mut iter = path.iter();
let first = iter.next().ok_or(UnrecognizedPath)?;
let Some(second) = iter.next() else {
return Ok(BlessedPathKind::Lockstep);
};
if iter.next().is_some() {
return Err(UnrecognizedPath);
}
if second.ends_with(".json.gitstub") {
Ok(BlessedPathKind::GitStubFile {
api_dir: first,
basename: second,
})
} else {
Ok(BlessedPathKind::VersionedFile {
api_dir: first,
basename: second,
})
}
}
}
#[derive(Debug)]
pub struct BlessedFiles {
files: BTreeMap<ApiIdent, ApiFiles<BlessedApiSpecFile>>,
git_stubs: BTreeMap<GitStubKey, BlessedGitStub>,
merge_base: Option<GitCommitHash>,
}
impl Deref for BlessedFiles {
type Target = BTreeMap<ApiIdent, ApiFiles<BlessedApiSpecFile>>;
fn deref(&self) -> &Self::Target {
&self.files
}
}
impl BlessedFiles {
pub fn git_stub(
&self,
ident: &ApiIdent,
version: &semver::Version,
) -> Option<&BlessedGitStub> {
self.git_stubs
.get(&GitStubKey { ident: ident.clone(), version: version.clone() })
}
pub fn merge_base(&self) -> Option<GitCommitHash> {
self.merge_base
}
}
enum BlessedFileResult {
Skip,
UnrecognizedPath(Utf8PathBuf),
VersionedDeserialized {
result: Result<ApiSpecFile, anyhow::Error>,
git_path: String,
},
GitStubDeserialized {
result: Result<ApiSpecFile, anyhow::Error>,
git_stub: GitStub,
},
VersionedParseFailed { api_dir: String, basename: String },
GitStubParseFailed { api_dir: String, basename: String },
Error(anyhow::Error),
}
fn process_blessed_entry(
f: &Utf8Path,
repo_root: &Utf8Path,
commit: GitCommitHash,
directory: &Utf8Path,
apis: &ManagedApis,
vcs: &RepoVcs,
) -> BlessedFileResult {
let kind = match BlessedPathKind::parse(f) {
Ok(kind) => kind,
Err(UnrecognizedPath) => {
return BlessedFileResult::UnrecognizedPath(f.to_owned());
}
};
match kind {
BlessedPathKind::Lockstep => BlessedFileResult::Skip,
BlessedPathKind::VersionedFile { api_dir, basename } => {
if ApiIdent::from(api_dir).versioned_api_is_latest_symlink(basename)
{
return BlessedFileResult::Skip;
}
let Some(spec_file_name) =
parse_versioned_file_name(apis, api_dir, basename)
.ok()
.map(ApiSpecFileName::from)
else {
return BlessedFileResult::VersionedParseFailed {
api_dir: api_dir.to_owned(),
basename: basename.to_owned(),
};
};
let git_path = format!("{directory}/{f}");
let contents =
match vcs.show_file(repo_root, commit, git_path.as_ref()) {
Ok(c) => c,
Err(err) => {
return BlessedFileResult::Error(err);
}
};
let result = ApiSpecFile::for_contents(spec_file_name, contents)
.map_err(|(err, _bytes)| {
err
});
BlessedFileResult::VersionedDeserialized { result, git_path }
}
BlessedPathKind::GitStubFile { api_dir, basename } => {
let Some(spec_file_name) =
parse_versioned_git_stub_file_name(apis, api_dir, basename)
.ok()
.map(ApiSpecFileName::from)
else {
return BlessedFileResult::GitStubParseFailed {
api_dir: api_dir.to_owned(),
basename: basename.to_owned(),
};
};
let git_path = format!("{directory}/{f}");
let contents =
match vcs.show_file(repo_root, commit, git_path.as_ref()) {
Ok(c) => c,
Err(err) => {
return BlessedFileResult::Error(err);
}
};
let git_stub_str = match String::from_utf8(contents) {
Ok(s) => s,
Err(err) => {
return BlessedFileResult::Error(anyhow!(err).context(
format!("Git stub {:?} is not valid UTF-8", git_path,),
));
}
};
let git_stub: GitStub =
match git_stub_str.parse() {
Ok(g) => g,
Err(err) => {
return BlessedFileResult::Error(anyhow!(err).context(
format!("parsing Git stub {:?}", git_path),
));
}
};
let json_contents =
match vcs.resolve_stub_contents(&git_stub, repo_root) {
Ok(c) => c,
Err(err) => {
return BlessedFileResult::Error(err.context(format!(
"reading content for Git stub {:?}",
git_path
)));
}
};
let result =
ApiSpecFile::for_contents(spec_file_name, json_contents)
.map_err(|(err, _bytes)| err);
BlessedFileResult::GitStubDeserialized { result, git_stub }
}
}
}
impl BlessedFiles {
pub fn load_from_vcs_parent_branch(
repo_root: &Utf8Path,
branch: &VcsRevision,
directory: &Utf8Path,
apis: &ManagedApis,
error_accumulator: &mut ErrorAccumulator,
vcs: &RepoVcs,
) -> anyhow::Result<BlessedFiles> {
let revision = vcs.merge_base_head(repo_root, branch)?;
Self::load_from_vcs_revision(
repo_root,
revision,
directory,
apis,
error_accumulator,
vcs,
)
}
pub fn load_from_vcs_revision(
repo_root: &Utf8Path,
commit: GitCommitHash,
directory: &Utf8Path,
apis: &ManagedApis,
error_accumulator: &mut ErrorAccumulator,
vcs: &RepoVcs,
) -> anyhow::Result<BlessedFiles> {
let files_found = vcs.list_files(repo_root, commit, directory)?;
let results: Vec<BlessedFileResult> = files_found
.par_iter()
.map(|f| {
process_blessed_entry(
f, repo_root, commit, directory, apis, vcs,
)
})
.collect();
let mut api_files: ApiSpecFilesBuilder<BlessedApiSpecFile> =
ApiSpecFilesBuilder::new(apis, error_accumulator);
let mut git_stubs: BTreeMap<GitStubKey, BlessedGitStub> =
BTreeMap::new();
let mut seen_dirs: BTreeMap<String, Option<ApiIdent>> = BTreeMap::new();
for result in results {
match result {
BlessedFileResult::Skip => {}
BlessedFileResult::UnrecognizedPath(path) => {
api_files.load_warning(anyhow!(
"path {:?}: can't understand this path name",
path
));
}
BlessedFileResult::Error(err) => {
api_files.load_error(err);
}
BlessedFileResult::VersionedDeserialized {
result,
git_path,
} => match result {
Ok(file) => {
let version = file.version().clone();
let ident = file.spec_file_name().ident().clone();
git_stubs.insert(
GitStubKey { ident, version },
BlessedGitStub::Lazy {
commit,
path: Utf8PathBuf::from(&git_path),
},
);
api_files.load_parsed(file);
}
Err(error) => api_files.load_error(error),
},
BlessedFileResult::GitStubDeserialized { result, git_stub } => {
match result {
Ok(file) => {
let version = file.version().clone();
let ident = file.spec_file_name().ident().clone();
git_stubs.insert(
GitStubKey { ident, version },
BlessedGitStub::Known {
commit: git_stub.commit(),
path: git_stub.path().to_owned(),
},
);
api_files.load_parsed(file);
}
Err(error) => api_files.load_error(error),
}
}
BlessedFileResult::VersionedParseFailed {
api_dir,
basename,
} => {
let ident = api_files
.lookup_versioned_dir(&mut seen_dirs, &api_dir);
if let Some(ident) = ident {
api_files.versioned_file_name(&ident, &basename);
}
}
BlessedFileResult::GitStubParseFailed { api_dir, basename } => {
let ident = api_files
.lookup_versioned_dir(&mut seen_dirs, &api_dir);
if let Some(ident) = ident {
api_files
.versioned_git_stub_file_name(&ident, &basename);
}
}
}
}
let files = api_files.into_map();
Ok(BlessedFiles { files, git_stubs, merge_base: Some(commit) })
}
}
impl<'a> From<ApiSpecFilesBuilder<'a, BlessedApiSpecFile>> for BlessedFiles {
fn from(api_files: ApiSpecFilesBuilder<'a, BlessedApiSpecFile>) -> Self {
BlessedFiles {
files: api_files.into_map(),
git_stubs: BTreeMap::new(),
merge_base: None,
}
}
}