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