mod claims;
mod error;
mod lifecycle;
pub mod map;
mod paths;
mod project;
mod timeline;
#[cfg(test)]
mod tests;
pub use error::VersioningError;
pub(crate) use timeline::VersionSelection;
pub use timeline::{VersionDef, VersionId, VersionTimeline};
use lifecycle::VersionVisitor as _;
use crate::error::{ParseError, ParserResult};
use crate::hir::{ParserProperties, Specification, VersionParameter};
use crate::rest_hir::ProjectedHir;
pub const API_VERSION_PROPERTY: &str = "api_version";
pub const API_VERSIONS_PROPERTY: &str = "api_versions";
pub const VERSION_LAYOUT_PROPERTY: &str = "version_layout";
pub const API_VERSION_MODULES_PROPERTY: &str = "api_version_modules";
pub const LAYOUT_MERGED: &str = "merged";
pub const LAYOUT_PER_VERSION: &str = "per_version";
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum IntervalKind {
Type,
Member,
Enumerator,
BitmaskValue,
Operation,
Parameter,
Attribute,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MemberInterval {
pub path: String,
pub kind: IntervalKind,
pub from: usize,
pub until: usize,
}
#[derive(Debug)]
pub struct VersionedProjection {
pub versions: Vec<VersionDef>,
pub version: Option<VersionDef>,
pub hir: ProjectedHir,
}
#[derive(Debug, Clone)]
pub struct VersioningRecord {
timeline: VersionTimeline,
parameter: Option<VersionParameter>,
}
impl VersioningRecord {
pub fn analyze(spec: &Specification) -> ParserResult<Option<VersioningRecord>> {
let discovery = lifecycle::discover(&spec.0)?;
let Some((ident, members)) = discovery.version_enum else {
if let Some((annotation, decl)) = discovery.saw_lifecycle {
return Err(VersioningError::MissingVersionEnum { annotation, decl }.into());
}
claims::check(spec, None)?;
return Ok(None);
};
let timeline = VersionTimeline::from_members(ident.clone(), &members)?;
if let Some(parameter) = &discovery.version_parameter {
for (_, version) in timeline.iter() {
parameter.validate_value(&version.value).map_err(|reason| {
VersioningError::MalformedVersionParameter {
decl: format!("{ident}::{}", version.ident),
reason,
}
})?;
}
}
let mut validator = lifecycle::LifecycleValidator {
timeline: &timeline,
};
validator.walk(&spec.0, "", lifecycle::Availability::full())?;
claims::check(spec, Some(&timeline))?;
Ok(Some(VersioningRecord {
timeline,
parameter: discovery.version_parameter,
}))
}
pub fn parameter(&self) -> Option<&VersionParameter> {
self.parameter.as_ref()
}
pub fn timeline(&self) -> &VersionTimeline {
&self.timeline
}
pub fn member_intervals(&self, spec: &Specification) -> ParserResult<Vec<MemberInterval>> {
lifecycle::collect_intervals(&spec.0, &self.timeline)
}
pub fn resolve_version(&self, raw: &str) -> ParserResult<VersionId> {
self.timeline.find(raw).map_err(Into::into)
}
pub fn project(&self, spec: &Specification, version: VersionId) -> ParserResult<Specification> {
project::project_version(spec, &self.timeline, version)
}
pub fn merge(
&self,
spec: &Specification,
versions: &[VersionId],
) -> ParserResult<Specification> {
project::merge_versions(spec, &self.timeline, versions)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum VersionLayout {
Merged,
PerVersion,
}
impl VersionLayout {
pub(crate) fn from_properties(properties: &ParserProperties) -> ParserResult<Self> {
let Some(value) = properties
.get(VERSION_LAYOUT_PROPERTY)
.and_then(serde_json::Value::as_str)
else {
return Ok(Self::Merged);
};
match value {
LAYOUT_MERGED => Ok(Self::Merged),
LAYOUT_PER_VERSION => Ok(Self::PerVersion),
other => Err(VersioningError::UnknownVersionLayout {
value: other.to_string(),
}
.into()),
}
}
}
pub(crate) fn selection_from_properties(
properties: &ParserProperties,
) -> ParserResult<Option<VersionSelection>> {
VersionSelection::from_properties(properties)
}
pub(crate) fn selected_versions(
selection: Option<&VersionSelection>,
record: &VersioningRecord,
) -> ParserResult<Vec<VersionId>> {
match selection {
Some(selection) => selection.resolve(&record.timeline),
None => Ok(record.timeline.iter().map(|(id, _)| id).collect()),
}
}
pub(crate) fn unversioned_selection_error(selection: &VersionSelection) -> ParseError {
VersioningError::UnversionedSpecification {
selection: selection.describe(),
}
.into()
}