Skip to main content

oxiphysics_core/
stability.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! API stability markers for OxiPhysics.
5//!
6//! This module provides types and traits to document the stability level
7//! of public APIs. Consumers can query stability at runtime via the
8//! [`HasStability`] trait, and library authors annotate items with the
9//! marker structs ([`Stable`], [`Unstable`], [`Experimental`],
10//! [`Deprecated`]) in documentation or associated types.
11//!
12//! # Stability Policy
13//!
14//! | Level          | Guarantee |
15//! |----------------|-----------|
16//! | **Stable**     | Follows semver strictly. Breaking changes only in major versions. |
17//! | **Unstable**   | May change in minor versions with a deprecation notice. |
18//! | **Experimental** | May change or be removed at any time without notice. |
19//! | **Deprecated** | Scheduled for removal in a future version. |
20//!
21//! # Example
22//!
23//! ```no_run
24//! use oxiphysics_core::stability::{StabilityLevel, HasStability};
25//!
26//! struct MyApi;
27//!
28//! impl HasStability for MyApi {
29//!     fn stability() -> StabilityLevel {
30//!         StabilityLevel::Stable
31//!     }
32//! }
33//!
34//! assert_eq!(MyApi::stability(), StabilityLevel::Stable);
35//! assert_eq!(MyApi::stability().to_string(), "stable");
36//! ```
37
38/// Marker type for stable APIs.
39///
40/// Stable APIs follow semver strictly: breaking changes happen only
41/// across major version boundaries.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct Stable;
44
45/// Marker type for unstable APIs.
46///
47/// Unstable APIs may change in minor releases. A deprecation notice
48/// will be issued before removal whenever possible.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct Unstable;
51
52/// Marker type for experimental APIs.
53///
54/// Experimental APIs may change or be removed at any time. They are
55/// provided for early feedback and should not be relied upon in
56/// production code.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct Experimental;
59
60/// Marker type for deprecated APIs.
61///
62/// Deprecated APIs are scheduled for removal in a future version.
63/// Migration guidance is provided in the item-level documentation.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct Deprecated;
66
67/// Runtime-queryable API stability level.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub enum StabilityLevel {
70    /// The API is stable and follows semver.
71    Stable,
72    /// The API is unstable and may change in minor versions.
73    Unstable,
74    /// The API is experimental and may change or be removed at any time.
75    Experimental,
76    /// The API is deprecated and will be removed in a future version.
77    Deprecated,
78}
79
80impl StabilityLevel {
81    /// Returns `true` if the API is considered safe for production use.
82    ///
83    /// Only [`StabilityLevel::Stable`] qualifies.
84    pub fn is_production_ready(self) -> bool {
85        matches!(self, Self::Stable)
86    }
87
88    /// Returns `true` if the API may change without a major version bump.
89    pub fn may_change(self) -> bool {
90        matches!(self, Self::Unstable | Self::Experimental)
91    }
92
93    /// Returns `true` if the API is deprecated.
94    pub fn is_deprecated(self) -> bool {
95        matches!(self, Self::Deprecated)
96    }
97
98    /// Returns a human-readable label suitable for documentation badges.
99    pub fn badge_label(self) -> &'static str {
100        match self {
101            Self::Stable => "stability: stable",
102            Self::Unstable => "stability: unstable",
103            Self::Experimental => "stability: experimental",
104            Self::Deprecated => "stability: deprecated",
105        }
106    }
107}
108
109impl std::fmt::Display for StabilityLevel {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        let label = match self {
112            Self::Stable => "stable",
113            Self::Unstable => "unstable",
114            Self::Experimental => "experimental",
115            Self::Deprecated => "deprecated",
116        };
117        f.write_str(label)
118    }
119}
120
121impl std::str::FromStr for StabilityLevel {
122    type Err = StabilityParseError;
123
124    fn from_str(s: &str) -> Result<Self, Self::Err> {
125        match s.to_ascii_lowercase().as_str() {
126            "stable" => Ok(Self::Stable),
127            "unstable" => Ok(Self::Unstable),
128            "experimental" => Ok(Self::Experimental),
129            "deprecated" => Ok(Self::Deprecated),
130            _ => Err(StabilityParseError {
131                input: s.to_owned(),
132            }),
133        }
134    }
135}
136
137/// Error returned when parsing an invalid stability level string.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct StabilityParseError {
140    /// The invalid input string.
141    pub input: String,
142}
143
144impl std::fmt::Display for StabilityParseError {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        write!(
147            f,
148            "unknown stability level '{}': expected one of stable, unstable, experimental, deprecated",
149            self.input
150        )
151    }
152}
153
154impl std::error::Error for StabilityParseError {}
155
156/// Trait for types that declare their API stability level.
157///
158/// Implement this on public API entry-point types so consumers can
159/// programmatically query how stable an API is.
160///
161/// ```no_run
162/// use oxiphysics_core::stability::{StabilityLevel, HasStability};
163///
164/// struct RigidBodySolver;
165///
166/// impl HasStability for RigidBodySolver {
167///     fn stability() -> StabilityLevel {
168///         StabilityLevel::Stable
169///     }
170/// }
171/// ```
172pub trait HasStability {
173    /// Returns the stability level of this API.
174    fn stability() -> StabilityLevel;
175
176    /// Returns `true` if this API is production-ready.
177    fn is_production_ready() -> bool {
178        Self::stability().is_production_ready()
179    }
180
181    /// Returns `true` if this API may change without a major version bump.
182    fn may_change() -> bool {
183        Self::stability().may_change()
184    }
185}
186
187/// Returns the version of the `oxiphysics-core` crate at build time.
188pub fn version() -> &'static str {
189    env!("CARGO_PKG_VERSION")
190}
191
192/// Returns the minimum supported Rust version (MSRV) for OxiPhysics.
193pub fn msrv() -> &'static str {
194    "nightly (edition 2024)"
195}
196
197/// Returns a summary of the stability policy as a static string slice.
198pub fn stability_policy_summary() -> &'static str {
199    "OxiPhysics follows semver for Stable APIs. \
200     Unstable APIs may change in minor versions. \
201     Experimental APIs may change at any time. \
202     Deprecated APIs will be removed in a future major version."
203}
204
205// ---------------------------------------------------------------------------
206// Tests
207// ---------------------------------------------------------------------------
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn display_stable() {
214        assert_eq!(StabilityLevel::Stable.to_string(), "stable");
215    }
216
217    #[test]
218    fn display_unstable() {
219        assert_eq!(StabilityLevel::Unstable.to_string(), "unstable");
220    }
221
222    #[test]
223    fn display_experimental() {
224        assert_eq!(StabilityLevel::Experimental.to_string(), "experimental");
225    }
226
227    #[test]
228    fn display_deprecated() {
229        assert_eq!(StabilityLevel::Deprecated.to_string(), "deprecated");
230    }
231
232    #[test]
233    fn parse_round_trip() {
234        for level in [
235            StabilityLevel::Stable,
236            StabilityLevel::Unstable,
237            StabilityLevel::Experimental,
238            StabilityLevel::Deprecated,
239        ] {
240            let parsed: StabilityLevel = level.to_string().parse().expect("round-trip parse");
241            assert_eq!(parsed, level);
242        }
243    }
244
245    #[test]
246    fn parse_case_insensitive() {
247        assert_eq!(
248            "STABLE".parse::<StabilityLevel>().expect("upper case"),
249            StabilityLevel::Stable
250        );
251        assert_eq!(
252            "Experimental"
253                .parse::<StabilityLevel>()
254                .expect("mixed case"),
255            StabilityLevel::Experimental
256        );
257    }
258
259    #[test]
260    fn parse_invalid() {
261        let err = "bogus".parse::<StabilityLevel>().expect_err("should fail");
262        assert_eq!(err.input, "bogus");
263        assert!(err.to_string().contains("bogus"));
264    }
265
266    #[test]
267    fn production_ready() {
268        assert!(StabilityLevel::Stable.is_production_ready());
269        assert!(!StabilityLevel::Unstable.is_production_ready());
270        assert!(!StabilityLevel::Experimental.is_production_ready());
271        assert!(!StabilityLevel::Deprecated.is_production_ready());
272    }
273
274    #[test]
275    fn may_change() {
276        assert!(!StabilityLevel::Stable.may_change());
277        assert!(StabilityLevel::Unstable.may_change());
278        assert!(StabilityLevel::Experimental.may_change());
279        assert!(!StabilityLevel::Deprecated.may_change());
280    }
281
282    #[test]
283    fn is_deprecated() {
284        assert!(StabilityLevel::Deprecated.is_deprecated());
285        assert!(!StabilityLevel::Stable.is_deprecated());
286    }
287
288    #[test]
289    fn badge_labels() {
290        assert_eq!(StabilityLevel::Stable.badge_label(), "stability: stable");
291        assert_eq!(
292            StabilityLevel::Experimental.badge_label(),
293            "stability: experimental"
294        );
295    }
296
297    #[test]
298    fn version_not_empty() {
299        assert!(!version().is_empty());
300    }
301
302    #[test]
303    fn msrv_contains_nightly() {
304        assert!(msrv().contains("nightly"));
305    }
306
307    #[test]
308    fn policy_summary_not_empty() {
309        assert!(!stability_policy_summary().is_empty());
310    }
311
312    #[test]
313    fn has_stability_trait() {
314        struct TestApi;
315        impl HasStability for TestApi {
316            fn stability() -> StabilityLevel {
317                StabilityLevel::Unstable
318            }
319        }
320        assert_eq!(TestApi::stability(), StabilityLevel::Unstable);
321        assert!(!TestApi::is_production_ready());
322        assert!(TestApi::may_change());
323    }
324
325    #[test]
326    fn marker_structs_are_eq() {
327        assert_eq!(Stable, Stable);
328        assert_eq!(Unstable, Unstable);
329        assert_eq!(Experimental, Experimental);
330        assert_eq!(Deprecated, Deprecated);
331    }
332}