Skip to main content

use_flag/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6/// Commonly used flag primitives.
7pub mod prelude {
8    pub use crate::{
9        BooleanFlag, Flag, FlagNameError, LongFlag, ShortFlag, is_valid_long_flag_name,
10        is_valid_short_flag,
11    };
12}
13
14/// Validation errors for primitive flag names and tokens.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub enum FlagNameError {
17    /// A flag name was empty.
18    Empty,
19    /// A short flag was not a single ASCII alphanumeric character.
20    InvalidShortFlag,
21    /// A long flag name was not a plain ASCII flag name.
22    InvalidLongFlagName,
23    /// A token did not look like a supported primitive flag token.
24    InvalidToken,
25}
26
27impl fmt::Display for FlagNameError {
28    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::Empty => formatter.write_str("flag name cannot be empty"),
31            Self::InvalidShortFlag => {
32                formatter.write_str("short flag must be one ASCII alphanumeric character")
33            },
34            Self::InvalidLongFlagName => formatter.write_str(
35                "long flag name must be ASCII alphanumeric with optional internal hyphens",
36            ),
37            Self::InvalidToken => formatter.write_str("flag token must look like -x or --name"),
38        }
39    }
40}
41
42impl std::error::Error for FlagNameError {}
43
44/// A one-character short flag such as `-v`.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub struct ShortFlag {
47    name: char,
48}
49
50impl ShortFlag {
51    /// Creates a short flag from a single name character.
52    ///
53    /// # Errors
54    ///
55    /// Returns [`FlagNameError::InvalidShortFlag`] when `name` is not ASCII alphanumeric.
56    pub const fn new(name: char) -> Result<Self, FlagNameError> {
57        if is_valid_short_flag(name) {
58            Ok(Self { name })
59        } else {
60            Err(FlagNameError::InvalidShortFlag)
61        }
62    }
63
64    /// Returns the short flag name character.
65    #[must_use]
66    pub const fn name(self) -> char {
67        self.name
68    }
69
70    /// Returns the token form, such as `-v`.
71    #[must_use]
72    pub fn to_token(self) -> String {
73        format!("-{}", self.name)
74    }
75}
76
77impl fmt::Display for ShortFlag {
78    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79        formatter.write_str(&self.to_token())
80    }
81}
82
83/// A long flag name such as `verbose` for `--verbose`.
84#[derive(Clone, Debug, PartialEq, Eq, Hash)]
85pub struct LongFlag {
86    name: String,
87}
88
89impl LongFlag {
90    /// Creates a long flag from a validated name without the leading `--`.
91    ///
92    /// # Errors
93    ///
94    /// Returns [`FlagNameError::InvalidLongFlagName`] when `name` is not a basic long flag name.
95    pub fn new(name: impl Into<String>) -> Result<Self, FlagNameError> {
96        let name = name.into();
97        if is_valid_long_flag_name(&name) {
98            Ok(Self { name })
99        } else if name.is_empty() {
100            Err(FlagNameError::Empty)
101        } else {
102            Err(FlagNameError::InvalidLongFlagName)
103        }
104    }
105
106    /// Returns the long flag name without the leading `--`.
107    #[must_use]
108    pub fn as_str(&self) -> &str {
109        &self.name
110    }
111
112    /// Returns the token form, such as `--verbose`.
113    #[must_use]
114    pub fn to_token(&self) -> String {
115        format!("--{}", self.name)
116    }
117}
118
119impl AsRef<str> for LongFlag {
120    fn as_ref(&self) -> &str {
121        self.as_str()
122    }
123}
124
125impl fmt::Display for LongFlag {
126    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        formatter.write_str(&self.to_token())
128    }
129}
130
131/// A primitive flag token.
132#[derive(Clone, Debug, PartialEq, Eq, Hash)]
133pub enum Flag {
134    /// A short flag such as `-v`.
135    Short(ShortFlag),
136    /// A long flag such as `--verbose`.
137    Long(LongFlag),
138}
139
140impl Flag {
141    /// Parses a primitive flag token.
142    ///
143    /// # Errors
144    ///
145    /// Returns [`FlagNameError`] when the token is not a supported short or long flag token.
146    pub fn try_from_token(token: &str) -> Result<Self, FlagNameError> {
147        if let Some(name) = token.strip_prefix("--") {
148            return LongFlag::new(name).map(Self::Long);
149        }
150
151        if let Some(name) = token.strip_prefix('-') {
152            let mut characters = name.chars();
153            return match (characters.next(), characters.next()) {
154                (Some(character), None) => ShortFlag::new(character).map(Self::Short),
155                _ => Err(FlagNameError::InvalidToken),
156            };
157        }
158
159        Err(FlagNameError::InvalidToken)
160    }
161
162    /// Returns the token form for this flag.
163    #[must_use]
164    pub fn to_token(&self) -> String {
165        match self {
166            Self::Short(flag) => flag.to_token(),
167            Self::Long(flag) => flag.to_token(),
168        }
169    }
170}
171
172impl fmt::Display for Flag {
173    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
174        formatter.write_str(&self.to_token())
175    }
176}
177
178/// A flag paired with a boolean state.
179#[derive(Clone, Debug, PartialEq, Eq, Hash)]
180pub struct BooleanFlag {
181    flag: Flag,
182    enabled: bool,
183}
184
185impl BooleanFlag {
186    /// Creates a boolean flag primitive.
187    #[must_use]
188    pub const fn new(flag: Flag, enabled: bool) -> Self {
189        Self { flag, enabled }
190    }
191
192    /// Creates an enabled boolean flag.
193    #[must_use]
194    pub const fn enabled(flag: Flag) -> Self {
195        Self::new(flag, true)
196    }
197
198    /// Creates a disabled boolean flag.
199    #[must_use]
200    pub const fn disabled(flag: Flag) -> Self {
201        Self::new(flag, false)
202    }
203
204    /// Returns the underlying flag.
205    #[must_use]
206    pub const fn flag(&self) -> &Flag {
207        &self.flag
208    }
209
210    /// Returns whether the flag is enabled.
211    #[must_use]
212    pub const fn is_enabled(&self) -> bool {
213        self.enabled
214    }
215}
216
217/// Returns whether `name` is valid for a short flag.
218#[must_use]
219pub const fn is_valid_short_flag(name: char) -> bool {
220    name.is_ascii_alphanumeric()
221}
222
223/// Returns whether `name` is valid for a long flag without the leading `--`.
224#[must_use]
225pub fn is_valid_long_flag_name(name: &str) -> bool {
226    let bytes = name.as_bytes();
227    if bytes.is_empty() || bytes[0] == b'-' || bytes[bytes.len() - 1] == b'-' {
228        return false;
229    }
230
231    bytes
232        .iter()
233        .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-')
234}
235
236#[cfg(test)]
237mod tests {
238    use super::{
239        BooleanFlag, Flag, FlagNameError, LongFlag, ShortFlag, is_valid_long_flag_name,
240        is_valid_short_flag,
241    };
242
243    #[test]
244    fn validates_short_and_long_names() {
245        assert!(is_valid_short_flag('v'));
246        assert!(!is_valid_short_flag('-'));
247        assert!(is_valid_long_flag_name("dry-run"));
248        assert!(!is_valid_long_flag_name("-dry"));
249        assert!(!is_valid_long_flag_name("dry_"));
250    }
251
252    #[test]
253    fn creates_flag_tokens() -> Result<(), FlagNameError> {
254        assert_eq!(ShortFlag::new('v')?.to_token(), "-v");
255        assert_eq!(LongFlag::new("verbose")?.to_token(), "--verbose");
256        assert_eq!(Flag::try_from_token("--dry-run")?.to_token(), "--dry-run");
257        assert_eq!(Flag::try_from_token("-q")?.to_token(), "-q");
258        assert_eq!(
259            Flag::try_from_token("---"),
260            Err(FlagNameError::InvalidLongFlagName)
261        );
262        Ok(())
263    }
264
265    #[test]
266    fn stores_boolean_flag_state() -> Result<(), FlagNameError> {
267        let flag = BooleanFlag::enabled(Flag::try_from_token("--verbose")?);
268
269        assert!(flag.is_enabled());
270        assert_eq!(flag.flag().to_token(), "--verbose");
271        Ok(())
272    }
273}