Skip to main content

operad/core/
versioning.rs

1//! Public API stability and feature-versioning markers.
2
3use std::marker::PhantomData;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum ApiStability {
7    Stable,
8    Experimental,
9    BackendSpecific,
10    MigrationOnly,
11}
12
13impl ApiStability {
14    pub const fn label(self) -> &'static str {
15        match self {
16            Self::Stable => "stable",
17            Self::Experimental => "experimental",
18            Self::BackendSpecific => "backend-specific",
19            Self::MigrationOnly => "migration-only",
20        }
21    }
22
23    pub const fn is_semver_protected(self) -> bool {
24        matches!(self, Self::Stable)
25    }
26
27    pub const fn may_change_without_major(self) -> bool {
28        !self.is_semver_protected()
29    }
30}
31
32pub trait ApiStabilityMarker: Copy + Clone + Default + Eq + PartialEq {
33    const STABILITY: ApiStability;
34
35    fn stability(self) -> ApiStability {
36        Self::STABILITY
37    }
38}
39
40#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
41pub struct Stable;
42
43impl ApiStabilityMarker for Stable {
44    const STABILITY: ApiStability = ApiStability::Stable;
45}
46
47#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
48pub struct Experimental;
49
50impl ApiStabilityMarker for Experimental {
51    const STABILITY: ApiStability = ApiStability::Experimental;
52}
53
54#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
55pub struct BackendSpecific;
56
57impl ApiStabilityMarker for BackendSpecific {
58    const STABILITY: ApiStability = ApiStability::BackendSpecific;
59}
60
61#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
62pub struct MigrationOnly;
63
64impl ApiStabilityMarker for MigrationOnly {
65    const STABILITY: ApiStability = ApiStability::MigrationOnly;
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub struct StabilityNote {
70    pub stability: ApiStability,
71    pub since: Option<&'static str>,
72    pub note: &'static str,
73}
74
75impl StabilityNote {
76    pub const fn new(
77        stability: ApiStability,
78        since: Option<&'static str>,
79        note: &'static str,
80    ) -> Self {
81        Self {
82            stability,
83            since,
84            note,
85        }
86    }
87
88    pub const fn stable(since: &'static str, note: &'static str) -> Self {
89        Self::new(ApiStability::Stable, Some(since), note)
90    }
91
92    pub const fn experimental(since: &'static str, note: &'static str) -> Self {
93        Self::new(ApiStability::Experimental, Some(since), note)
94    }
95
96    pub const fn backend_specific(feature: &'static str) -> Self {
97        Self::new(ApiStability::BackendSpecific, None, feature)
98    }
99
100    pub const fn migration_only(note: &'static str) -> Self {
101        Self::new(ApiStability::MigrationOnly, None, note)
102    }
103
104    pub const fn is_semver_protected(self) -> bool {
105        self.stability.is_semver_protected()
106    }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub struct FeatureStability {
111    pub feature: &'static str,
112    pub stability: ApiStability,
113    pub since: Option<&'static str>,
114    pub note: &'static str,
115}
116
117impl FeatureStability {
118    pub const fn new(
119        feature: &'static str,
120        stability: ApiStability,
121        since: Option<&'static str>,
122        note: &'static str,
123    ) -> Self {
124        Self {
125            feature,
126            stability,
127            since,
128            note,
129        }
130    }
131
132    pub const fn stable(feature: &'static str, since: &'static str, note: &'static str) -> Self {
133        Self::new(feature, ApiStability::Stable, Some(since), note)
134    }
135
136    pub const fn experimental(
137        feature: &'static str,
138        since: &'static str,
139        note: &'static str,
140    ) -> Self {
141        Self::new(feature, ApiStability::Experimental, Some(since), note)
142    }
143
144    pub const fn backend_specific(feature: &'static str, note: &'static str) -> Self {
145        Self::new(feature, ApiStability::BackendSpecific, None, note)
146    }
147
148    pub const fn migration_only(feature: &'static str, note: &'static str) -> Self {
149        Self::new(feature, ApiStability::MigrationOnly, None, note)
150    }
151
152    pub const fn is_semver_protected(self) -> bool {
153        self.stability.is_semver_protected()
154    }
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
158pub struct ApiStatus<S: ApiStabilityMarker> {
159    pub since: Option<&'static str>,
160    pub note: &'static str,
161    marker: PhantomData<S>,
162}
163
164impl<S: ApiStabilityMarker> ApiStatus<S> {
165    pub const fn new(since: Option<&'static str>, note: &'static str) -> Self {
166        Self {
167            since,
168            note,
169            marker: PhantomData,
170        }
171    }
172
173    pub const fn stability(&self) -> ApiStability {
174        S::STABILITY
175    }
176
177    pub const fn note(&self) -> StabilityNote {
178        StabilityNote::new(S::STABILITY, self.since, self.note)
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn marker_types_classify_api_stability() {
188        assert_eq!(Stable.stability(), ApiStability::Stable);
189        assert_eq!(Experimental.stability(), ApiStability::Experimental);
190        assert_eq!(BackendSpecific.stability(), ApiStability::BackendSpecific);
191        assert_eq!(MigrationOnly.stability(), ApiStability::MigrationOnly);
192    }
193
194    #[test]
195    fn stability_notes_encode_semver_expectations() {
196        let stable = StabilityNote::stable("5.0.0", "Public layout primitives");
197        let experimental = StabilityNote::experimental("5.0.0", "Early host runtime policy");
198
199        assert!(stable.is_semver_protected());
200        assert!(experimental.stability.may_change_without_major());
201        assert_eq!(ApiStability::MigrationOnly.label(), "migration-only");
202    }
203
204    #[test]
205    fn feature_stability_records_feature_scope() {
206        let wgpu =
207            FeatureStability::backend_specific("wgpu", "Renderer availability depends on backend");
208        let status = ApiStatus::<Stable>::new(Some("5.0.0"), "Core document tree");
209
210        assert_eq!(wgpu.feature, "wgpu");
211        assert!(!wgpu.is_semver_protected());
212        assert_eq!(status.stability(), ApiStability::Stable);
213        assert_eq!(status.note().since, Some("5.0.0"));
214    }
215}