#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
pub mod prelude {
pub use crate::{
BooleanFlag, Flag, FlagNameError, LongFlag, ShortFlag, is_valid_long_flag_name,
is_valid_short_flag,
};
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FlagNameError {
Empty,
InvalidShortFlag,
InvalidLongFlagName,
InvalidToken,
}
impl fmt::Display for FlagNameError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("flag name cannot be empty"),
Self::InvalidShortFlag => {
formatter.write_str("short flag must be one ASCII alphanumeric character")
},
Self::InvalidLongFlagName => formatter.write_str(
"long flag name must be ASCII alphanumeric with optional internal hyphens",
),
Self::InvalidToken => formatter.write_str("flag token must look like -x or --name"),
}
}
}
impl std::error::Error for FlagNameError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ShortFlag {
name: char,
}
impl ShortFlag {
pub const fn new(name: char) -> Result<Self, FlagNameError> {
if is_valid_short_flag(name) {
Ok(Self { name })
} else {
Err(FlagNameError::InvalidShortFlag)
}
}
#[must_use]
pub const fn name(self) -> char {
self.name
}
#[must_use]
pub fn to_token(self) -> String {
format!("-{}", self.name)
}
}
impl fmt::Display for ShortFlag {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.to_token())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct LongFlag {
name: String,
}
impl LongFlag {
pub fn new(name: impl Into<String>) -> Result<Self, FlagNameError> {
let name = name.into();
if is_valid_long_flag_name(&name) {
Ok(Self { name })
} else if name.is_empty() {
Err(FlagNameError::Empty)
} else {
Err(FlagNameError::InvalidLongFlagName)
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.name
}
#[must_use]
pub fn to_token(&self) -> String {
format!("--{}", self.name)
}
}
impl AsRef<str> for LongFlag {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for LongFlag {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.to_token())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Flag {
Short(ShortFlag),
Long(LongFlag),
}
impl Flag {
pub fn try_from_token(token: &str) -> Result<Self, FlagNameError> {
if let Some(name) = token.strip_prefix("--") {
return LongFlag::new(name).map(Self::Long);
}
if let Some(name) = token.strip_prefix('-') {
let mut characters = name.chars();
return match (characters.next(), characters.next()) {
(Some(character), None) => ShortFlag::new(character).map(Self::Short),
_ => Err(FlagNameError::InvalidToken),
};
}
Err(FlagNameError::InvalidToken)
}
#[must_use]
pub fn to_token(&self) -> String {
match self {
Self::Short(flag) => flag.to_token(),
Self::Long(flag) => flag.to_token(),
}
}
}
impl fmt::Display for Flag {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.to_token())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct BooleanFlag {
flag: Flag,
enabled: bool,
}
impl BooleanFlag {
#[must_use]
pub const fn new(flag: Flag, enabled: bool) -> Self {
Self { flag, enabled }
}
#[must_use]
pub const fn enabled(flag: Flag) -> Self {
Self::new(flag, true)
}
#[must_use]
pub const fn disabled(flag: Flag) -> Self {
Self::new(flag, false)
}
#[must_use]
pub const fn flag(&self) -> &Flag {
&self.flag
}
#[must_use]
pub const fn is_enabled(&self) -> bool {
self.enabled
}
}
#[must_use]
pub const fn is_valid_short_flag(name: char) -> bool {
name.is_ascii_alphanumeric()
}
#[must_use]
pub fn is_valid_long_flag_name(name: &str) -> bool {
let bytes = name.as_bytes();
if bytes.is_empty() || bytes[0] == b'-' || bytes[bytes.len() - 1] == b'-' {
return false;
}
bytes
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-')
}
#[cfg(test)]
mod tests {
use super::{
BooleanFlag, Flag, FlagNameError, LongFlag, ShortFlag, is_valid_long_flag_name,
is_valid_short_flag,
};
#[test]
fn validates_short_and_long_names() {
assert!(is_valid_short_flag('v'));
assert!(!is_valid_short_flag('-'));
assert!(is_valid_long_flag_name("dry-run"));
assert!(!is_valid_long_flag_name("-dry"));
assert!(!is_valid_long_flag_name("dry_"));
}
#[test]
fn creates_flag_tokens() -> Result<(), FlagNameError> {
assert_eq!(ShortFlag::new('v')?.to_token(), "-v");
assert_eq!(LongFlag::new("verbose")?.to_token(), "--verbose");
assert_eq!(Flag::try_from_token("--dry-run")?.to_token(), "--dry-run");
assert_eq!(Flag::try_from_token("-q")?.to_token(), "-q");
assert_eq!(
Flag::try_from_token("---"),
Err(FlagNameError::InvalidLongFlagName)
);
Ok(())
}
#[test]
fn stores_boolean_flag_state() -> Result<(), FlagNameError> {
let flag = BooleanFlag::enabled(Flag::try_from_token("--verbose")?);
assert!(flag.is_enabled());
assert_eq!(flag.flag().to_token(), "--verbose");
Ok(())
}
}