use crate::{
apis::{ManagedApi, ManagedApis},
environment::ErrorAccumulator,
spec_files_generic::{
ApiFiles, ApiLoad, ApiSpecFile, ApiSpecFilesBuilder, AsRawFiles,
SpecFileInfo, hash_contents,
},
};
use anyhow::{anyhow, bail};
use dropshot_api_manager_types::{
ApiIdent, ApiSpecFileName, LockstepApiSpecFileName,
VersionedApiSpecFileName,
};
use rayon::prelude::*;
use std::{collections::BTreeMap, ops::Deref};
pub struct GeneratedApiSpecFile(ApiSpecFile);
NewtypeDebug! { () pub struct GeneratedApiSpecFile(ApiSpecFile); }
NewtypeDeref! { () pub struct GeneratedApiSpecFile(ApiSpecFile); }
NewtypeDerefMut! { () pub struct GeneratedApiSpecFile(ApiSpecFile); }
NewtypeFrom! { () pub struct GeneratedApiSpecFile(ApiSpecFile); }
impl ApiLoad for GeneratedApiSpecFile {
const MISCONFIGURATIONS_ALLOWED: bool = false;
type Unparseable = std::convert::Infallible;
fn make_item(raw: ApiSpecFile) -> Self {
GeneratedApiSpecFile(raw)
}
fn try_extend(&mut self, item: ApiSpecFile) -> anyhow::Result<()> {
bail!(
"found more than one generated 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 GeneratedApiSpecFile {
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))
}
}
pub struct GeneratedFiles(BTreeMap<ApiIdent, ApiFiles<GeneratedApiSpecFile>>);
NewtypeDeref! {
() pub struct GeneratedFiles(
BTreeMap<ApiIdent, ApiFiles<GeneratedApiSpecFile>>
);
}
enum GeneratedApiResult {
Lockstep {
versions: Vec<Result<ApiSpecFile, anyhow::Error>>,
},
Versioned {
ident: ApiIdent,
versions: Vec<Result<ApiSpecFile, anyhow::Error>>,
latest: Option<VersionedApiSpecFileName>,
},
}
fn generate_api(api: &ManagedApi) -> GeneratedApiResult {
if api.is_lockstep() {
let versions = api
.iter_versions_semver()
.map(|version| {
api.generate_spec_bytes(version)
.and_then(|contents| {
let file_name =
LockstepApiSpecFileName::new(api.ident().clone());
ApiSpecFile::for_contents(file_name.into(), contents)
.map_err(|(e, _buf)| e)
})
.map_err(|error| {
error.context(format!(
"generating OpenAPI document for lockstep \
API {:?}",
api.ident()
))
})
})
.collect();
GeneratedApiResult::Lockstep { versions }
} else {
let supported_versions: Vec<_> = api
.iter_versioned_versions()
.expect(
"iter_versioned_versions() returns `Some` for versioned APIs",
)
.collect();
let versions: Vec<_> = supported_versions
.par_iter()
.map(|supported_version| {
let version = supported_version.semver();
api.generate_spec_bytes(version)
.and_then(|contents| {
let file_name = VersionedApiSpecFileName::new(
api.ident().clone(),
version.clone(),
hash_contents(&contents),
);
ApiSpecFile::for_contents(file_name.into(), contents)
.map_err(|(e, _buf)| e)
})
.map_err(|error| {
error.context(format!(
"generating OpenAPI document for versioned \
API {:?} version {}",
api.ident(),
version
))
})
})
.collect();
let latest = versions.iter().rev().find_map(|r| {
r.as_ref().ok().map(|file| match file.spec_file_name() {
ApiSpecFileName::Versioned(v) => v.clone(),
ApiSpecFileName::Lockstep(_) => {
unreachable!("lockstep file name in versioned API path")
}
})
});
GeneratedApiResult::Versioned {
ident: api.ident().clone(),
versions,
latest,
}
}
}
impl GeneratedFiles {
pub fn generate(
apis: &ManagedApis,
error_accumulator: &mut ErrorAccumulator,
) -> anyhow::Result<GeneratedFiles> {
let results: Vec<GeneratedApiResult> = apis
.iter_apis()
.collect::<Vec<_>>()
.par_iter()
.map(|api| generate_api(api))
.collect();
let mut api_files: ApiSpecFilesBuilder<GeneratedApiSpecFile> =
ApiSpecFilesBuilder::new(apis, error_accumulator);
for result in results {
let (versions, latest_info) = match result {
GeneratedApiResult::Lockstep { versions } => (versions, None),
GeneratedApiResult::Versioned { ident, versions, latest } => {
(versions, Some((ident, latest)))
}
};
for version_result in versions {
match version_result {
Ok(file) => api_files.load_parsed(file),
Err(error) => api_files.load_error(error),
}
}
if let Some((ident, latest)) = latest_info {
match latest {
Some(latest) => api_files.load_latest_link(&ident, latest),
None => api_files.load_error(anyhow!(
"versioned API {:?} symlink: there is no \
working version (fix above error(s) first)",
ident,
)),
}
}
}
Ok(Self::from(api_files))
}
}
impl<'a> From<ApiSpecFilesBuilder<'a, GeneratedApiSpecFile>>
for GeneratedFiles
{
fn from(api_files: ApiSpecFilesBuilder<'a, GeneratedApiSpecFile>) -> Self {
GeneratedFiles(api_files.into_map())
}
}