Skip to main content

rtb_app/
features.rs

1//! Runtime feature gating for built-in subcommands.
2//!
3//! Compile-time selection is handled by Cargo features on the `rtb`
4//! umbrella crate. Runtime gating — "this tool compiled the `update`
5//! command in, but this particular invocation wants to hide it" — lives
6//! here.
7
8use std::collections::HashSet;
9
10/// Built-in feature identifiers that can be toggled at runtime.
11///
12/// `#[non_exhaustive]` keeps variant additions a minor-version change
13/// for downstream matchers.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum Feature {
17    /// The `init` bootstrap command.
18    Init,
19    /// The `version` command.
20    Version,
21    /// Self-update (`update` subcommand and the pre-run version check).
22    Update,
23    /// TUI documentation browser (`docs` subcommand).
24    Docs,
25    /// MCP server (`mcp` subcommand).
26    Mcp,
27    /// Health-check diagnostics (`doctor` subcommand).
28    Doctor,
29    /// AI-powered features (`docs ask`, agentic flows).
30    Ai,
31    /// Opt-in anonymous telemetry.
32    Telemetry,
33    /// Runtime config get/set (`config` subcommand).
34    Config,
35    /// Structured release-notes display (`changelog` subcommand).
36    Changelog,
37    /// Credential management (`credentials` subcommand subtree).
38    /// Default-on; gates the v0.4 `credentials list / add / remove
39    /// / test / doctor` subcommands.
40    Credentials,
41}
42
43impl Feature {
44    /// Features enabled by default when no explicit overrides are supplied.
45    /// Mirrors [`Features::default`].
46    #[must_use]
47    pub fn defaults() -> Features {
48        Features::builder().build()
49    }
50
51    /// Every defined variant. Useful for debug introspection.
52    ///
53    /// Returns a slice rather than a fixed-size array because
54    /// [`Feature`] is `#[non_exhaustive]` — adding a variant is a
55    /// minor-version change that must not break downstream callers'
56    /// type signatures. A slice length is a value, not part of the
57    /// type.
58    #[must_use]
59    pub const fn all() -> &'static [Self] {
60        &[
61            Self::Init,
62            Self::Version,
63            Self::Update,
64            Self::Docs,
65            Self::Mcp,
66            Self::Doctor,
67            Self::Ai,
68            Self::Telemetry,
69            Self::Config,
70            Self::Changelog,
71            Self::Credentials,
72        ]
73    }
74}
75
76/// Immutable set of runtime-enabled features.
77///
78/// Construct via [`Features::default`] for the documented defaults, or
79/// via [`Features::builder`] to override explicitly.
80///
81/// The default set — `Init`, `Version`, `Update`, `Docs`, `Mcp`,
82/// `Doctor`, `Credentials`, `Telemetry`, `Config` — matches the
83/// Cargo default feature set of the `rtb` umbrella crate; runtime
84/// gating only hides commands that are already compiled in.
85#[derive(Debug, Clone)]
86pub struct Features {
87    enabled: HashSet<Feature>,
88}
89
90impl Default for Features {
91    fn default() -> Self {
92        Self::builder().build()
93    }
94}
95
96impl Features {
97    /// `true` iff `feature` is in the enabled set.
98    #[must_use]
99    pub fn is_enabled(&self, feature: Feature) -> bool {
100        self.enabled.contains(&feature)
101    }
102
103    /// Start a builder pre-populated with the default feature set.
104    #[must_use]
105    pub fn builder() -> FeaturesBuilder {
106        FeaturesBuilder::new()
107    }
108
109    /// Iterate the enabled features in arbitrary order.
110    pub fn iter(&self) -> impl Iterator<Item = Feature> + '_ {
111        self.enabled.iter().copied()
112    }
113}
114
115/// Builder for [`Features`]. Pre-populated with the default feature set
116/// via [`FeaturesBuilder::new`]; start empty with [`FeaturesBuilder::none`].
117#[derive(Debug, Default)]
118pub struct FeaturesBuilder {
119    enabled: HashSet<Feature>,
120}
121
122impl FeaturesBuilder {
123    /// Start a builder pre-populated with the default feature set:
124    /// `Init`, `Version`, `Update`, `Docs`, `Mcp`, `Doctor`,
125    /// `Credentials`, `Telemetry`, `Config`.
126    #[must_use]
127    pub fn new() -> Self {
128        Self {
129            enabled: [
130                Feature::Init,
131                Feature::Version,
132                Feature::Update,
133                Feature::Docs,
134                Feature::Mcp,
135                Feature::Doctor,
136                Feature::Credentials,
137                Feature::Telemetry,
138                Feature::Config,
139            ]
140            .into_iter()
141            .collect(),
142        }
143    }
144
145    /// Start with an empty enabled set.
146    #[must_use]
147    pub fn none() -> Self {
148        Self { enabled: HashSet::new() }
149    }
150
151    /// Add `feature` to the enabled set.
152    #[must_use]
153    pub fn enable(mut self, feature: Feature) -> Self {
154        self.enabled.insert(feature);
155        self
156    }
157
158    /// Remove `feature` from the enabled set.
159    #[must_use]
160    pub fn disable(mut self, feature: Feature) -> Self {
161        self.enabled.remove(&feature);
162        self
163    }
164
165    /// Finalise the builder.
166    #[must_use]
167    pub fn build(self) -> Features {
168        Features { enabled: self.enabled }
169    }
170}