Skip to main content

cabin_core/
experimental.rs

1//! Typed model for Cabin's experimental (unstable) feature gates.
2//!
3//! Experimental features are opt-in behaviors that may change or
4//! disappear between releases.  The user enables one per invocation
5//! with the global `-Z <feature>` CLI flag; nothing here persists
6//! into manifests, lockfiles, or config files.  The enum lives in
7//! `cabin-core` so the CLI parser and every crate that gates a pass
8//! on a feature share one name list and one error wording.
9//!
10//! The registry is currently empty - no feature is gated behind
11//! `-Z` today, so every `-Z <value>` is rejected as unknown.  Adding
12//! a feature means adding a variant, its `as_str` spelling, and an
13//! `ALL` entry; the parser and its error message follow from those.
14
15use std::fmt;
16
17/// One recognized experimental feature, as accepted by `-Z`.
18///
19/// Uninhabited while no experimental feature is registered.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub enum ExperimentalFeature {}
22
23impl ExperimentalFeature {
24    /// Every recognized feature, in the order the parse error
25    /// lists them.
26    pub const ALL: [Self; 0] = [];
27
28    /// Stable kebab-case spelling, exactly what `-Z` accepts.
29    #[must_use]
30    pub const fn as_str(self) -> &'static str {
31        match self {}
32    }
33}
34
35impl fmt::Display for ExperimentalFeature {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        f.write_str(self.as_str())
38    }
39}
40
41impl std::str::FromStr for ExperimentalFeature {
42    type Err = UnknownExperimentalFeature;
43
44    // Exact-match parsing, mirroring the other typed CLI/env value
45    // parsers in this crate: no case folding, no trimming.
46    fn from_str(raw: &str) -> Result<Self, Self::Err> {
47        Self::ALL
48            .into_iter()
49            .find(|feature| feature.as_str() == raw)
50            .ok_or_else(|| UnknownExperimentalFeature {
51                value: raw.to_owned(),
52            })
53    }
54}
55
56/// Error returned when a `-Z` value names no recognized
57/// experimental feature.  The `Display` impl is the user-visible
58/// wording; with no feature registered it reads:
59///
60/// ```text
61/// unknown experimental feature 'frobnicate'; no experimental features are currently recognized
62/// ```
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct UnknownExperimentalFeature {
65    /// The raw, invalid value as the user provided it.
66    pub value: String,
67}
68
69impl fmt::Display for UnknownExperimentalFeature {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        let recognized = ExperimentalFeature::ALL
72            .map(ExperimentalFeature::as_str)
73            .join(", ");
74        if recognized.is_empty() {
75            write!(
76                f,
77                "unknown experimental feature '{}'; no experimental features are currently \
78                 recognized",
79                self.value,
80            )
81        } else {
82            write!(
83                f,
84                "unknown experimental feature '{}'; expected one of: {recognized}",
85                self.value,
86            )
87        }
88    }
89}
90
91impl std::error::Error for UnknownExperimentalFeature {}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn unknown_value_reports_no_recognized_features() {
99        // The registry is empty, so every value - including the
100        // removed `standard-compat` name - is unknown and reports
101        // the same no-features wording.
102        for value in ["frobnicate", "standard-compat"] {
103            let err = value.parse::<ExperimentalFeature>().unwrap_err();
104            assert_eq!(
105                err.to_string(),
106                format!(
107                    "unknown experimental feature '{value}'; no experimental features are \
108                     currently recognized"
109                ),
110            );
111        }
112    }
113}