Skip to main content

xidl_parser/versioning/
mod.rs

1//! API versioning: one `@version` enum, `@added`/`@removed` lifecycle annotations
2//! and per-version HIR projection.
3//!
4//! Phase 1 is deliberately small. A specification is versioned by marking one
5//! enum with `@version`; its members, in declaration order, are the version
6//! history. `@added(Versions::v1)` and `@removed(Versions::v2)` mark when a
7//! declaration exists; annotation order never matters. Renames are expressed by
8//! declaring a new member with a disjoint interval plus `@rename`, exactly like
9//! the desugaring `versioned idl -> multi idl` describes.
10//!
11//! Projection is a pure HIR rewrite: each version is wrapped in
12//! `module <version-ident> { ... }`, lifecycle annotations and the version enum
13//! are removed, and the document version pragma carries the version value. The
14//! merged form keeps one module per selected version so generators can stay
15//! version-free.
16
17mod error;
18mod lifecycle;
19mod paths;
20mod project;
21mod timeline;
22
23#[cfg(test)]
24mod tests;
25
26pub use error::VersioningError;
27pub(crate) use timeline::VersionSelection;
28pub use timeline::{VersionDef, VersionId, VersionTimeline};
29
30use lifecycle::VersionVisitor as _;
31
32use crate::error::{ParseError, ParserResult};
33use crate::hir::{ParserProperties, Specification};
34use crate::rest_hir::ProjectedHir;
35
36/// Generation property that selects one API version by member ident or value.
37pub const API_VERSION_PROPERTY: &str = "api_version";
38/// Generation property that selects several API versions (comma separated, or `*`).
39pub const API_VERSIONS_PROPERTY: &str = "api_versions";
40/// Generation property that selects the version output layout.
41pub const VERSION_LAYOUT_PROPERTY: &str = "version_layout";
42/// Generation property that lists the API version modules of a projection.
43///
44/// The HIR stage sets it for every versioned contract; targets that cannot
45/// render module-scoped output yet use it to fail loudly instead of emitting
46/// code that does not compile.
47pub const API_VERSION_MODULES_PROPERTY: &str = "api_version_modules";
48/// Layout value: one specification holding one module per version.
49pub const LAYOUT_MERGED: &str = "merged";
50/// Layout value: one projection per version.
51pub const LAYOUT_PER_VERSION: &str = "per_version";
52
53/// One HIR projection produced from a specification.
54#[derive(Debug)]
55pub struct VersionedProjection {
56    /// API versions this projection contains; empty for unversioned contracts.
57    pub versions: Vec<VersionDef>,
58    /// Target version of a single-version projection; `None` for merged and
59    /// unversioned projections.
60    pub version: Option<VersionDef>,
61    /// The projected HIR.
62    pub hir: ProjectedHir,
63}
64
65/// Result of analysing the API versioning annotations of a specification.
66#[derive(Debug, Clone)]
67pub struct VersioningRecord {
68    timeline: VersionTimeline,
69}
70
71impl VersioningRecord {
72    /// Validates every API versioning annotation of a specification.
73    ///
74    /// Validation always runs, even when no version is projected. Returns
75    /// `None` for specifications without a `@version` enum, and fails when
76    /// lifecycle annotations appear without one.
77    pub fn analyze(spec: &Specification) -> ParserResult<Option<VersioningRecord>> {
78        let discovery = lifecycle::discover(&spec.0)?;
79        let Some((ident, members)) = discovery.version_enum else {
80            if let Some((annotation, decl)) = discovery.saw_lifecycle {
81                return Err(VersioningError::MissingVersionEnum { annotation, decl }.into());
82            }
83            return Ok(None);
84        };
85        let timeline = VersionTimeline::from_members(ident, &members)?;
86        let mut validator = lifecycle::LifecycleValidator {
87            timeline: &timeline,
88        };
89        validator.walk(&spec.0, "", lifecycle::Availability::full())?;
90        Ok(Some(VersioningRecord { timeline }))
91    }
92
93    /// The version history declared by the `@version` enum.
94    pub fn timeline(&self) -> &VersionTimeline {
95        &self.timeline
96    }
97
98    /// Projects `version` into an ordinary specification.
99    pub fn project(&self, spec: &Specification, version: VersionId) -> ParserResult<Specification> {
100        project::project_version(spec, &self.timeline, version)
101    }
102
103    /// Projects every selected version into one specification with one module
104    /// per version, in timeline order.
105    pub fn merge(
106        &self,
107        spec: &Specification,
108        versions: &[VersionId],
109    ) -> ParserResult<Specification> {
110        project::merge_versions(spec, &self.timeline, versions)
111    }
112}
113
114/// How versioned output is laid out for the target.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub(crate) enum VersionLayout {
117    /// One specification with one module per selected version.
118    Merged,
119    /// One projection per selected version.
120    PerVersion,
121}
122
123impl VersionLayout {
124    /// Reads [`VERSION_LAYOUT_PROPERTY`] from generation properties.
125    pub(crate) fn from_properties(properties: &ParserProperties) -> ParserResult<Self> {
126        let Some(value) = properties
127            .get(VERSION_LAYOUT_PROPERTY)
128            .and_then(serde_json::Value::as_str)
129        else {
130            return Ok(Self::Merged);
131        };
132        match value {
133            LAYOUT_MERGED => Ok(Self::Merged),
134            LAYOUT_PER_VERSION => Ok(Self::PerVersion),
135            other => Err(VersioningError::UnknownVersionLayout {
136                value: other.to_string(),
137            }
138            .into()),
139        }
140    }
141}
142
143/// Reads the requested version selection from generation properties.
144///
145/// A selection property on a specification without a `@version` enum is an
146/// error, so build flags cannot silently do nothing.
147pub(crate) fn selection_from_properties(
148    properties: &ParserProperties,
149) -> ParserResult<Option<VersionSelection>> {
150    VersionSelection::from_properties(properties)
151}
152
153/// Resolves the requested versions, defaulting to every declared version.
154pub(crate) fn selected_versions(
155    selection: Option<&VersionSelection>,
156    record: &VersioningRecord,
157) -> ParserResult<Vec<VersionId>> {
158    match selection {
159        Some(selection) => selection.resolve(&record.timeline),
160        None => Ok(record.timeline.iter().map(|(id, _)| id).collect()),
161    }
162}
163
164/// Reports a selection requested for an unversioned specification.
165pub(crate) fn unversioned_selection_error(selection: &VersionSelection) -> ParseError {
166    VersioningError::UnversionedSpecification {
167        selection: selection.describe(),
168    }
169    .into()
170}