1use 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}