1use std::fmt;
18
19#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub enum Verbosity {
30 Quiet,
35 #[default]
38 Normal,
39 Verbose,
42 VeryVerbose,
47}
48
49impl Verbosity {
50 pub fn as_str(self) -> &'static str {
53 match self {
54 Verbosity::Quiet => "quiet",
55 Verbosity::Normal => "normal",
56 Verbosity::Verbose => "verbose",
57 Verbosity::VeryVerbose => "very-verbose",
58 }
59 }
60
61 pub fn shows_status(self) -> bool {
63 self >= Verbosity::Normal
64 }
65
66 pub fn shows_verbose(self) -> bool {
68 self >= Verbosity::Verbose
69 }
70
71 pub fn shows_very_verbose(self) -> bool {
73 self >= Verbosity::VeryVerbose
74 }
75
76 pub fn from_verbose_count(count: u8) -> Self {
80 match count {
81 0 => Verbosity::Normal,
82 1 => Verbosity::Verbose,
83 _ => Verbosity::VeryVerbose,
84 }
85 }
86
87 pub fn from_config_pair(
97 verbose: Option<bool>,
98 quiet: Option<bool>,
99 ) -> Result<Option<Self>, InvalidVerbosityCombination> {
100 match (verbose, quiet) {
101 (Some(true), Some(true)) => Err(InvalidVerbosityCombination),
102 (Some(true), _) => Ok(Some(Verbosity::Verbose)),
103 (_, Some(true)) => Ok(Some(Verbosity::Quiet)),
104 _ => Ok(None),
105 }
106 }
107
108 pub fn parse_bool_env(variable: &'static str, raw: &str) -> Result<bool, VerbosityEnvError> {
119 cabin_env::parse_bool(raw).map_err(|_| VerbosityEnvError {
120 variable,
121 value: raw.to_owned(),
122 })
123 }
124}
125
126impl fmt::Display for Verbosity {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 f.write_str(self.as_str())
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct VerbosityEnvError {
137 pub variable: &'static str,
138 pub value: String,
139}
140
141impl fmt::Display for VerbosityEnvError {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 write!(
144 f,
145 "invalid {} value '{}'; expected one of: 1, 0, true, false, yes, no, on, off",
146 self.variable, self.value
147 )
148 }
149}
150
151impl std::error::Error for VerbosityEnvError {}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub struct InvalidVerbosityCombination;
158
159impl fmt::Display for InvalidVerbosityCombination {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 f.write_str("term.verbose and term.quiet cannot both be true")
162 }
163}
164
165impl std::error::Error for InvalidVerbosityCombination {}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn default_is_normal() {
173 assert_eq!(Verbosity::default(), Verbosity::Normal);
174 }
175
176 #[test]
177 fn ordering_matches_intuition() {
178 assert!(Verbosity::Quiet < Verbosity::Normal);
179 assert!(Verbosity::Normal < Verbosity::Verbose);
180 assert!(Verbosity::Verbose < Verbosity::VeryVerbose);
181 }
182
183 #[test]
184 fn shows_predicates_match_thresholds() {
185 assert!(!Verbosity::Quiet.shows_status());
186 assert!(Verbosity::Normal.shows_status());
187 assert!(Verbosity::Verbose.shows_status());
188 assert!(!Verbosity::Normal.shows_verbose());
189 assert!(Verbosity::Verbose.shows_verbose());
190 assert!(Verbosity::VeryVerbose.shows_verbose());
191 assert!(!Verbosity::Verbose.shows_very_verbose());
192 assert!(Verbosity::VeryVerbose.shows_very_verbose());
193 }
194
195 #[test]
196 fn from_verbose_count_clamps_above_two() {
197 assert_eq!(Verbosity::from_verbose_count(0), Verbosity::Normal);
198 assert_eq!(Verbosity::from_verbose_count(1), Verbosity::Verbose);
199 assert_eq!(Verbosity::from_verbose_count(2), Verbosity::VeryVerbose);
200 assert_eq!(Verbosity::from_verbose_count(5), Verbosity::VeryVerbose);
201 assert_eq!(
202 Verbosity::from_verbose_count(u8::MAX),
203 Verbosity::VeryVerbose
204 );
205 }
206
207 #[test]
208 fn from_config_pair_handles_each_combination() {
209 assert_eq!(Verbosity::from_config_pair(None, None).unwrap(), None);
210 assert_eq!(
211 Verbosity::from_config_pair(Some(true), None).unwrap(),
212 Some(Verbosity::Verbose)
213 );
214 assert_eq!(
215 Verbosity::from_config_pair(None, Some(true)).unwrap(),
216 Some(Verbosity::Quiet)
217 );
218 assert_eq!(
219 Verbosity::from_config_pair(Some(false), Some(false)).unwrap(),
220 None
221 );
222 assert!(Verbosity::from_config_pair(Some(true), Some(true)).is_err());
223 }
224
225 #[test]
226 fn parse_bool_env_accepts_documented_values() {
227 for ok in ["1", "true", "yes", "on", "TRUE", "Yes", "ON"] {
228 assert!(
229 Verbosity::parse_bool_env("X", ok).unwrap(),
230 "truthy: {ok:?}"
231 );
232 }
233 for falsy in ["0", "false", "no", "off", "FALSE", "No", "OFF", ""] {
234 assert!(
235 !Verbosity::parse_bool_env("X", falsy).unwrap(),
236 "falsy: {falsy:?}"
237 );
238 }
239 }
240
241 #[test]
242 fn parse_bool_env_rejects_unknown_value() {
243 let err = Verbosity::parse_bool_env("CABIN_TERM_VERBOSE", "loud").unwrap_err();
244 assert_eq!(
245 err.to_string(),
246 "invalid CABIN_TERM_VERBOSE value 'loud'; expected one of: 1, 0, true, false, yes, no, on, off"
247 );
248 }
249
250 #[test]
251 fn invalid_combination_display_is_actionable() {
252 assert_eq!(
253 InvalidVerbosityCombination.to_string(),
254 "term.verbose and term.quiet cannot both be true"
255 );
256 }
257}