use crate::{
apis::ManagedApis, environment::ErrorAccumulator, output::InlineErrorChain,
};
use anyhow::anyhow;
use camino::Utf8Path;
use debug_ignore::DebugIgnore;
use dropshot_api_manager_types::{
ApiDocFileName, ApiIdent, LockstepApiDocFileName, VersionedApiDocFileName,
VersionedApiDocKind,
};
use git_stub::{GitCommitHash, GitStubParseError};
use openapiv3::OpenAPI;
use sha2::{Digest, Sha256};
use std::{
collections::{BTreeMap, btree_map::Entry},
fmt::Debug,
};
use thiserror::Error;
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
pub(crate) struct GitStubKey {
pub(crate) ident: ApiIdent,
pub(crate) version: semver::Version,
}
pub(crate) fn parse_versioned_file_name(
apis: &ManagedApis,
ident: &str,
basename: &str,
) -> Result<VersionedApiDocFileName, BadVersionedFileName> {
let ident = ApiIdent::from(ident.to_string());
let Some(api) = apis.api(&ident) else {
return Err(BadVersionedFileName::NoSuchApi);
};
if !api.is_versioned() {
return Err(BadVersionedFileName::NotVersioned);
}
let expected_prefix = format!("{}-", ident);
let suffix = basename.strip_prefix(&expected_prefix).ok_or_else(|| {
BadVersionedFileName::UnexpectedName {
ident: ident.clone(),
source: anyhow!("unexpected prefix"),
}
})?;
let middle = suffix.strip_suffix(".json").ok_or_else(|| {
BadVersionedFileName::UnexpectedName {
ident: ident.clone(),
source: anyhow!("bad suffix"),
}
})?;
let (version_str, hash) = middle.rsplit_once("-").ok_or_else(|| {
BadVersionedFileName::UnexpectedName {
ident: ident.clone(),
source: anyhow!("cannot extract version and hash"),
}
})?;
let version: semver::Version =
version_str.parse().map_err(|e: semver::Error| {
BadVersionedFileName::UnexpectedName {
ident: ident.clone(),
source: anyhow!(e).context(format!(
"version string is not a semver: {:?}",
version_str
)),
}
})?;
if !version.pre.is_empty() {
return Err(BadVersionedFileName::UnexpectedName {
ident,
source: anyhow!(
"version string has a prerelease field \
(not supported): {:?}",
version_str
),
});
}
if !version.build.is_empty() {
return Err(BadVersionedFileName::UnexpectedName {
ident,
source: anyhow!(
"version string has a build field (not supported): {:?}",
version_str
),
});
}
Ok(VersionedApiDocFileName::new(ident, version, hash.to_string()))
}
pub(crate) fn parse_versioned_git_stub_file_name(
apis: &ManagedApis,
ident: &str,
basename: &str,
) -> Result<VersionedApiDocFileName, BadVersionedFileName> {
let json_basename = basename.strip_suffix(".gitstub").ok_or_else(|| {
BadVersionedFileName::UnexpectedName {
ident: ApiIdent::from(ident.to_string()),
source: anyhow!("expected .json.gitstub suffix"),
}
})?;
let versioned = parse_versioned_file_name(apis, ident, json_basename)?;
Ok(versioned.to_git_stub())
}
pub(crate) fn parse_lockstep_file_name(
apis: &ManagedApis,
basename: &str,
) -> Result<LockstepApiDocFileName, BadLockstepFileName> {
let ident = ApiIdent::from(
basename
.strip_suffix(".json")
.ok_or(BadLockstepFileName::MissingJsonSuffix)?
.to_owned(),
);
let api = apis.api(&ident).ok_or_else(|| {
BadLockstepFileName::NoSuchApi { ident: ident.clone() }
})?;
if !api.is_lockstep() {
return Err(BadLockstepFileName::NotLockstep);
}
Ok(LockstepApiDocFileName::new(ident))
}
#[derive(Debug, Error)]
pub(crate) enum BadLockstepFileName {
#[error("expected lockstep API file name to end in \".json\"")]
MissingJsonSuffix,
#[error("does not match a known API")]
NoSuchApi {
ident: ApiIdent,
},
#[error("this API is not a lockstep API")]
NotLockstep,
}
#[derive(Debug, Error)]
pub(crate) enum BadVersionedFileName {
#[error("does not match a known API")]
NoSuchApi,
#[error("this API is not a versioned API")]
NotVersioned,
#[error(
"expected a versioned API document filename for API {ident:?} to look \
like \"{ident:?}-SEMVER-HASH.json\""
)]
UnexpectedName { ident: ApiIdent, source: anyhow::Error },
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ApiDocFileParseError {
#[error("parsing as JSON")]
JsonParse { source: serde_json::Error },
#[error("parsing OpenAPI document")]
OpenApiParse { source: serde_json::Error },
#[error("parsing version from generated document")]
VersionParse { source: semver::Error },
#[error(
"version in the file ({file_version}) differs from the one in the \
filename"
)]
VersionMismatch { file_version: semver::Version },
#[error(
"version in the file ({file_version}) differs from the declared \
lockstep version ({declared_version})"
)]
LockstepVersionMismatch {
file_version: semver::Version,
declared_version: semver::Version,
},
#[error(
"computed hash {expected:?}, but file name has different hash \
{actual:?}"
)]
HashMismatch { expected: String, actual: String },
#[error(
"resolved contents have hash {expected:?}, but file name has \
different hash {actual:?}"
)]
GitStubHashMismatch { expected: String, actual: String },
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum UnparseableReason {
#[error(transparent)]
Parse(#[from] ApiDocFileParseError),
#[error("parsing as a Git stub")]
GitStubInvalid {
#[source]
source: GitStubParseError,
},
#[error(
"needs to be rewritten to canonical Git stub format with forward \
slashes and a trailing newline"
)]
GitStubNonCanonical,
#[error("resolving Git stub contents")]
GitStubUnresolvable {
#[source]
source: anyhow::Error,
},
}
#[derive(Debug)]
pub struct ApiDocFile {
name: ApiDocFileName,
value: DebugIgnore<serde_json::Value>,
contents: DebugIgnore<OpenAPI>,
contents_buf: DebugIgnore<Vec<u8>>,
version: semver::Version,
}
impl ApiDocFile {
pub fn for_contents(
doc_file_name: ApiDocFileName,
contents_buf: Vec<u8>,
) -> Result<ApiDocFile, (ApiDocFileParseError, Vec<u8>)> {
let value: serde_json::Value =
match serde_json::from_slice(&contents_buf) {
Ok(v) => v,
Err(e) => {
return Err((
ApiDocFileParseError::JsonParse { source: e },
contents_buf,
));
}
};
let openapi: OpenAPI = match serde_json::from_slice(&contents_buf) {
Ok(o) => o,
Err(e) => {
return Err((
ApiDocFileParseError::OpenApiParse { source: e },
contents_buf,
));
}
};
let parsed_version: semver::Version = match openapi.info.version.parse()
{
Ok(v) => v,
Err(e) => {
return Err((
ApiDocFileParseError::VersionParse { source: e },
contents_buf,
));
}
};
match &doc_file_name {
ApiDocFileName::Versioned(v) => {
if *v.version() != parsed_version {
return Err((
ApiDocFileParseError::VersionMismatch {
file_version: parsed_version,
},
contents_buf,
));
}
let expected_hash = hash_contents(&contents_buf);
if expected_hash != v.hash() {
let error = match v.kind() {
VersionedApiDocKind::Json => {
ApiDocFileParseError::HashMismatch {
expected: expected_hash,
actual: v.hash().to_owned(),
}
}
VersionedApiDocKind::GitStub => {
ApiDocFileParseError::GitStubHashMismatch {
expected: expected_hash,
actual: v.hash().to_owned(),
}
}
};
return Err((error, contents_buf));
}
}
ApiDocFileName::Lockstep(_) => {}
}
Ok(ApiDocFile {
name: doc_file_name,
value: DebugIgnore(value),
contents: DebugIgnore(openapi),
contents_buf: DebugIgnore(contents_buf),
version: parsed_version,
})
}
pub fn doc_file_name(&self) -> &ApiDocFileName {
&self.name
}
pub fn version(&self) -> &semver::Version {
&self.version
}
pub fn value(&self) -> &serde_json::Value {
&self.value
}
pub fn openapi(&self) -> &OpenAPI {
&self.contents
}
pub fn contents(&self) -> &[u8] {
&self.contents_buf
}
pub(crate) fn into_name_and_contents(self) -> (ApiDocFileName, Vec<u8>) {
(self.name, self.contents_buf.0)
}
}
pub struct ApiDocFilesBuilder<'a, T> {
apis: &'a ManagedApis,
doc_files: BTreeMap<ApiIdent, ApiFiles<T>>,
error_accumulator: &'a mut ErrorAccumulator,
}
impl<'a, T: ApiLoad + AsRawFiles> ApiDocFilesBuilder<'a, T> {
pub fn new(
apis: &'a ManagedApis,
error_accumulator: &'a mut ErrorAccumulator,
) -> ApiDocFilesBuilder<'a, T> {
ApiDocFilesBuilder {
apis,
doc_files: BTreeMap::new(),
error_accumulator,
}
}
pub fn load_error(&mut self, error: anyhow::Error) {
self.error_accumulator.error(error);
}
pub fn load_warning(&mut self, error: anyhow::Error) {
self.error_accumulator.warning(error);
}
pub fn lockstep_file_name(
&mut self,
basename: &str,
) -> Option<ApiDocFileName> {
match parse_lockstep_file_name(self.apis, basename) {
Err(BadLockstepFileName::NoSuchApi { ident })
if T::MISCONFIGURATIONS_ALLOWED =>
{
if !self.apis.unknown_apis().contains(&ident) {
let warning = anyhow!(
"skipping file {basename:?}: {} \
(this is expected if you are deleting an API)",
BadLockstepFileName::NoSuchApi { ident },
);
self.load_warning(warning);
}
None
}
Err(warning @ BadLockstepFileName::NotLockstep)
if T::MISCONFIGURATIONS_ALLOWED =>
{
let warning = anyhow!(
"skipping file {basename:?}: {warning} \
(this is expected if you are converting \
a lockstep API to a versioned one)"
);
self.load_warning(warning);
None
}
Err(warning @ BadLockstepFileName::MissingJsonSuffix) => {
let warning = anyhow!(warning)
.context(format!("skipping file {:?}", basename));
self.load_warning(warning);
None
}
Err(BadLockstepFileName::NoSuchApi { ident })
if self.apis.unknown_apis().contains(&ident) =>
{
let warning = anyhow!(BadLockstepFileName::NoSuchApi { ident })
.context(format!("skipping file {:?}", basename));
self.load_warning(warning);
None
}
Err(error) => {
self.load_error(
anyhow!(error).context(format!("file {:?}", basename)),
);
None
}
Ok(file_name) => Some(file_name.into()),
}
}
pub fn versioned_directory(&mut self, basename: &str) -> Option<ApiIdent> {
let ident = ApiIdent::from(basename.to_owned());
match self.apis.api(&ident) {
Some(api) if api.is_versioned() => Some(ident),
Some(_) => {
let error = anyhow!(
"skipping directory for lockstep API: {:?}",
basename,
);
if T::MISCONFIGURATIONS_ALLOWED {
self.load_warning(error);
} else {
self.load_error(error);
}
None
}
None => {
let error = anyhow!(
"skipping directory for unknown API: {:?}",
basename,
);
if T::MISCONFIGURATIONS_ALLOWED {
self.load_warning(error);
} else {
self.load_error(error);
}
None
}
}
}
pub fn versioned_file_name(
&mut self,
ident: &ApiIdent,
basename: &str,
) -> Option<ApiDocFileName> {
self.handle_versioned_parse(
parse_versioned_file_name(self.apis, ident, basename),
&format!("file {basename}"),
&format!("skipping file {basename}"),
)
.map(Into::into)
}
pub fn versioned_git_stub_file_name(
&mut self,
ident: &ApiIdent,
basename: &str,
) -> Option<ApiDocFileName> {
self.handle_versioned_parse(
parse_versioned_git_stub_file_name(self.apis, ident, basename),
&format!("Git stub {basename}"),
&format!("skipping Git stub {basename}"),
)
.map(Into::into)
}
pub fn symlink_contents(
&mut self,
symlink_path: &Utf8Path,
ident: &ApiIdent,
basename: &str,
) -> Option<VersionedApiDocFileName> {
self.handle_versioned_parse(
parse_versioned_file_name(self.apis, ident, basename),
&format!("bad symlink {symlink_path} pointing to {basename}"),
&format!("ignoring symlink {symlink_path} pointing to {basename}"),
)
}
fn handle_versioned_parse(
&mut self,
result: Result<VersionedApiDocFileName, BadVersionedFileName>,
error_label: &str,
warning_label: &str,
) -> Option<VersionedApiDocFileName> {
match result {
Ok(file_name) => Some(file_name),
Err(
warning @ (BadVersionedFileName::NoSuchApi
| BadVersionedFileName::NotVersioned),
) if T::MISCONFIGURATIONS_ALLOWED => {
self.load_warning(
anyhow!(warning).context(warning_label.to_owned()),
);
None
}
Err(warning @ BadVersionedFileName::UnexpectedName { .. }) => {
self.load_warning(
anyhow!(warning).context(warning_label.to_owned()),
);
None
}
Err(error) => {
self.load_error(anyhow!(error).context(error_label.to_owned()));
None
}
}
}
pub fn load_parsed(&mut self, file: ApiDocFile) {
let version_mismatch = match file.doc_file_name() {
ApiDocFileName::Lockstep(_) => {
let api = self
.apis
.api(file.doc_file_name().ident())
.expect("parsed lockstep file name implies a known API");
let declared_version = api
.iter_versions_semver()
.next()
.expect("lockstep API has exactly one version");
if file.version() != declared_version {
Some(ApiDocFileParseError::LockstepVersionMismatch {
file_version: file.version().clone(),
declared_version: declared_version.clone(),
})
} else {
None
}
}
ApiDocFileName::Versioned(_) => None,
};
match version_mismatch {
Some(reason) => {
let (name, contents) = file.into_name_and_contents();
self.insert_unparseable(name, contents, reason.into());
}
None => {
let ident = file.doc_file_name().ident();
let api_version = file.version();
let entry = self
.doc_files
.entry(ident.clone())
.or_insert_with(ApiFiles::new)
.doc_files
.entry(api_version.clone());
match entry {
Entry::Vacant(vacant_entry) => {
vacant_entry.insert(T::make_item(file));
}
Entry::Occupied(mut occupied_entry) => {
match occupied_entry.get_mut().try_extend(file) {
Ok(()) => (),
Err(error) => self.load_error(error),
};
}
};
}
}
}
pub fn load_maybe_unparseable(
&mut self,
file_name: ApiDocFileName,
result: Result<ApiDocFile, (UnparseableReason, Vec<u8>)>,
) {
match result {
Ok(file) => {
self.load_parsed(file);
}
Err((error, contents)) => {
self.insert_unparseable(file_name, contents, error);
}
}
}
pub fn load_unparseable(
&mut self,
file_name: ApiDocFileName,
contents: Vec<u8>,
reason: UnparseableReason,
) {
self.insert_unparseable(file_name, contents, reason);
}
fn insert_unparseable(
&mut self,
file_name: ApiDocFileName,
contents: Vec<u8>,
reason: UnparseableReason,
) {
let rendered = InlineErrorChain::new(&reason).to_string();
let path = file_name.path();
match T::make_unparseable(file_name.clone(), contents, reason) {
Some(unparseable) => {
let version = match file_name.version() {
Some(version) => version.clone(),
None => {
let api = self.apis.api(file_name.ident()).expect(
"parsed lockstep file name implies a known API",
);
api.iter_versions_semver()
.next()
.expect("lockstep API has exactly one version")
.clone()
}
};
let ident = file_name.ident().clone();
let entry = self
.doc_files
.entry(ident)
.or_insert_with(ApiFiles::new)
.doc_files
.entry(version);
match entry {
Entry::Vacant(vacant_entry) => {
vacant_entry
.insert(T::unparseable_into_self(unparseable));
}
Entry::Occupied(mut occupied_entry) => {
occupied_entry
.get_mut()
.extend_unparseable(unparseable);
}
}
}
None => {
self.load_error(anyhow!(
"unparseable file {path:?}: {rendered}"
));
}
}
}
pub fn load_latest_link(
&mut self,
ident: &ApiIdent,
links_to: VersionedApiDocFileName,
) {
let Some(api) = self.apis.api(ident) else {
let error =
anyhow!("link for unknown API {:?} ({})", ident, links_to);
if T::MISCONFIGURATIONS_ALLOWED {
self.load_warning(error);
} else {
self.load_error(error);
}
return;
};
if !api.is_versioned() {
let error = anyhow!(
"link for non-versioned API {:?} ({})",
ident,
links_to
);
if T::MISCONFIGURATIONS_ALLOWED {
self.load_warning(error);
} else {
self.load_error(error);
}
return;
}
let api_files =
self.doc_files.entry(ident.clone()).or_insert_with(ApiFiles::new);
if let Some(previous) = api_files.latest_link.replace(links_to) {
let new_link = api_files.latest_link.as_ref().unwrap().to_string();
self.load_error(anyhow!(
"API {:?}: multiple \"latest\" links (at least {}, {})",
ident,
previous,
new_link,
));
}
}
pub fn set_git_stub_commit(
&mut self,
ident: &ApiIdent,
version: &semver::Version,
commit: GitCommitHash,
) {
if let Some(api_files) = self.doc_files.get_mut(ident)
&& let Some(item) = api_files.doc_files.get_mut(version)
{
item.set_git_stub_commit(commit);
}
}
pub(crate) fn lookup_versioned_dir(
&mut self,
seen_dirs: &mut BTreeMap<String, Option<ApiIdent>>,
dir_basename: &str,
) -> Option<ApiIdent> {
seen_dirs
.entry(dir_basename.to_owned())
.or_insert_with(|| self.versioned_directory(dir_basename))
.clone()
}
pub fn into_map(self) -> BTreeMap<ApiIdent, ApiFiles<T>> {
self.doc_files
}
}
#[derive(Debug)]
pub struct ApiFiles<T> {
doc_files: BTreeMap<semver::Version, T>,
latest_link: Option<VersionedApiDocFileName>,
}
impl<T: AsRawFiles> ApiFiles<T> {
fn new() -> ApiFiles<T> {
ApiFiles { doc_files: BTreeMap::new(), latest_link: None }
}
pub fn versions(&self) -> &BTreeMap<semver::Version, T> {
&self.doc_files
}
pub fn latest_link(&self) -> Option<&VersionedApiDocFileName> {
self.latest_link.as_ref()
}
}
pub trait DocFileInfo {
fn doc_file_name(&self) -> &ApiDocFileName;
fn version(&self) -> Option<&semver::Version>;
}
impl DocFileInfo for ApiDocFile {
fn doc_file_name(&self) -> &ApiDocFileName {
&self.name
}
fn version(&self) -> Option<&semver::Version> {
Some(&self.version)
}
}
pub trait AsRawFiles: Debug {
fn as_raw_files<'a>(
&'a self,
) -> Box<dyn Iterator<Item = &'a dyn DocFileInfo> + 'a>;
}
impl AsRawFiles for Vec<ApiDocFile> {
fn as_raw_files<'a>(
&'a self,
) -> Box<dyn Iterator<Item = &'a dyn DocFileInfo> + 'a> {
Box::new(self.iter().map(|f| f as &dyn DocFileInfo))
}
}
pub trait ApiLoad {
const MISCONFIGURATIONS_ALLOWED: bool;
type Unparseable;
fn make_item(raw: ApiDocFile) -> Self;
fn try_extend(&mut self, raw: ApiDocFile) -> anyhow::Result<()>;
fn make_unparseable(
name: ApiDocFileName,
contents: Vec<u8>,
reason: UnparseableReason,
) -> Option<Self::Unparseable>;
fn unparseable_into_self(unparseable: Self::Unparseable) -> Self
where
Self: Sized;
fn extend_unparseable(&mut self, unparseable: Self::Unparseable);
fn set_git_stub_commit(&mut self, _commit: GitCommitHash) {}
}
pub(crate) fn hash_contents(contents: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(contents);
let computed_hash = hasher.finalize();
hex::encode(&computed_hash.as_slice()[0..3])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ManagedApiConfig;
use anyhow::Context;
use assert_matches::assert_matches;
use dropshot::{ApiDescription, ApiDescriptionBuildErrors, StubContext};
use dropshot_api_manager_types::{
ManagedApiMetadata, SupportedVersion, SupportedVersions, Versions,
};
use semver::Version;
#[test]
fn test_parse_name_lockstep() {
let apis = all_apis().unwrap();
let name = parse_lockstep_file_name(&apis, "lockstep.json").unwrap();
assert_eq!(
name,
LockstepApiDocFileName::new(ApiIdent::from("lockstep".to_owned()))
);
}
#[test]
fn test_parse_name_versioned() {
let apis = all_apis().unwrap();
let name = parse_versioned_file_name(
&apis,
"versioned",
"versioned-1.2.3-feedface.json",
)
.unwrap();
assert_eq!(
name,
VersionedApiDocFileName::new(
ApiIdent::from("versioned".to_owned()),
Version::new(1, 2, 3),
"feedface".to_owned(),
)
);
}
#[test]
fn test_parse_name_lockstep_fail() {
let apis = all_apis().unwrap();
let error = parse_lockstep_file_name(&apis, "lockstep").unwrap_err();
assert_matches!(error, BadLockstepFileName::MissingJsonSuffix);
let error =
parse_lockstep_file_name(&apis, "bart-simpson.json").unwrap_err();
assert_matches!(
error,
BadLockstepFileName::NoSuchApi { ident } if ident == "bart-simpson".into()
);
let error =
parse_lockstep_file_name(&apis, "versioned.json").unwrap_err();
assert_matches!(error, BadLockstepFileName::NotLockstep);
}
#[test]
fn test_parse_name_versioned_fail() {
let apis = all_apis().unwrap();
let error = parse_versioned_file_name(
&apis,
"bart-simpson",
"bart-simpson-1.2.3-hash.json",
)
.unwrap_err();
assert_matches!(error, BadVersionedFileName::NoSuchApi);
let error = parse_versioned_file_name(
&apis,
"lockstep",
"lockstep-1.2.3-hash.json",
)
.unwrap_err();
assert_matches!(error, BadVersionedFileName::NotVersioned);
let error =
parse_versioned_file_name(&apis, "versioned", "1.2.3-hash.json")
.unwrap_err();
assert_matches!(error, BadVersionedFileName::UnexpectedName { .. });
let error = parse_versioned_file_name(
&apis,
"versioned",
"versioned-1.2.3.json",
)
.unwrap_err();
assert_matches!(error, BadVersionedFileName::UnexpectedName { .. });
let error = parse_versioned_file_name(
&apis,
"versioned",
"versioned-hash.json",
)
.unwrap_err();
assert_matches!(error, BadVersionedFileName::UnexpectedName { .. });
let error = parse_versioned_file_name(
&apis,
"versioned",
"versioned-1.2.3-hash",
)
.unwrap_err();
assert_matches!(error, BadVersionedFileName::UnexpectedName { .. });
let error = parse_versioned_file_name(
&apis,
"versioned",
"versioned-bogus-hash",
)
.unwrap_err();
assert_matches!(error, BadVersionedFileName::UnexpectedName { .. });
}
#[test]
fn test_parse_name_versioned_git_stub_valid() {
let apis = all_apis().unwrap();
let name = parse_versioned_git_stub_file_name(
&apis,
"versioned",
"versioned-1.2.3-feedface.json.gitstub",
)
.unwrap();
assert_eq!(
name,
VersionedApiDocFileName::new_git_stub(
ApiIdent::from("versioned".to_owned()),
Version::new(1, 2, 3),
"feedface".to_owned(),
)
);
}
#[test]
fn test_parse_name_versioned_git_stub_invalid() {
let apis = all_apis().unwrap();
let error = parse_versioned_git_stub_file_name(
&apis,
"versioned",
"versioned-1.2.3-feedface.json",
)
.unwrap_err();
assert_matches!(error, BadVersionedFileName::UnexpectedName { .. });
let error = parse_versioned_git_stub_file_name(
&apis,
"unknown",
"unknown-1.2.3-feedface.json.gitstub",
)
.unwrap_err();
assert_matches!(error, BadVersionedFileName::NoSuchApi);
let error = parse_versioned_git_stub_file_name(
&apis,
"lockstep",
"lockstep-1.2.3-feedface.json.gitstub",
)
.unwrap_err();
assert_matches!(error, BadVersionedFileName::NotVersioned);
let error = parse_versioned_git_stub_file_name(
&apis,
"versioned",
"versioned-badversion-feedface.json.gitstub",
)
.unwrap_err();
assert_matches!(error, BadVersionedFileName::UnexpectedName { .. });
}
fn all_apis() -> anyhow::Result<ManagedApis> {
let apis = vec![
ManagedApiConfig {
ident: "lockstep",
versions: Versions::Lockstep {
version: "1.0.0".parse().unwrap(),
},
title: "Lockstep API",
metadata: ManagedApiMetadata {
description: Some("A simple lockstep-versioned API"),
..ManagedApiMetadata::default()
},
api_description: unimplemented_fn,
},
ManagedApiConfig {
ident: "versioned",
versions: Versions::Versioned {
supported_versions: SupportedVersions::new(vec![
SupportedVersion::new(Version::new(1, 0, 0), "initial"),
]),
},
title: "Versioned API",
metadata: ManagedApiMetadata {
description: Some("A simple lockstep-versioned API"),
..ManagedApiMetadata::default()
},
api_description: unimplemented_fn,
},
];
let apis =
ManagedApis::new(apis).context("error creating ManagedApis")?;
Ok(apis)
}
fn unimplemented_fn()
-> Result<ApiDescription<StubContext>, ApiDescriptionBuildErrors> {
unimplemented!("this shouldn't be called, not part of test")
}
}