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}