1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6pub 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#[derive(Clone, Debug, PartialEq, Eq)]
16pub enum FlagNameError {
17 Empty,
19 InvalidShortFlag,
21 InvalidLongFlagName,
23 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub struct ShortFlag {
47 name: char,
48}
49
50impl ShortFlag {
51 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 #[must_use]
66 pub const fn name(self) -> char {
67 self.name
68 }
69
70 #[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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
85pub struct LongFlag {
86 name: String,
87}
88
89impl LongFlag {
90 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 #[must_use]
108 pub fn as_str(&self) -> &str {
109 &self.name
110 }
111
112 #[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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
133pub enum Flag {
134 Short(ShortFlag),
136 Long(LongFlag),
138}
139
140impl Flag {
141 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 #[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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
180pub struct BooleanFlag {
181 flag: Flag,
182 enabled: bool,
183}
184
185impl BooleanFlag {
186 #[must_use]
188 pub const fn new(flag: Flag, enabled: bool) -> Self {
189 Self { flag, enabled }
190 }
191
192 #[must_use]
194 pub const fn enabled(flag: Flag) -> Self {
195 Self::new(flag, true)
196 }
197
198 #[must_use]
200 pub const fn disabled(flag: Flag) -> Self {
201 Self::new(flag, false)
202 }
203
204 #[must_use]
206 pub const fn flag(&self) -> &Flag {
207 &self.flag
208 }
209
210 #[must_use]
212 pub const fn is_enabled(&self) -> bool {
213 self.enabled
214 }
215}
216
217#[must_use]
219pub const fn is_valid_short_flag(name: char) -> bool {
220 name.is_ascii_alphanumeric()
221}
222
223#[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}