use crate::const_utf8;
#[derive(Debug, Copy, Clone, PartialEq)]
pub(crate) enum OptType {
Positional,
Flag,
Value,
}
#[derive(Debug, PartialEq)]
pub(crate) enum OptIdentifier {
Single(&'static str),
Multi(&'static[&'static str]),
}
#[derive(Debug, PartialEq)]
pub struct Opt<ID> {
pub(crate) id: ID,
pub(crate) names: OptIdentifier,
pub(crate) value_name: Option<&'static str>,
pub(crate) help_string: Option<&'static str>,
pub(crate) r#type: OptType,
flags: OptFlag,
}
pub enum OptHide {
Short,
Full,
All,
}
#[derive(Debug, PartialEq)]
struct OptFlag(u8);
impl OptFlag {
#[allow(dead_code)]
pub const NONE: Self = Self(0);
pub const REQUIRED: Self = OptFlag(1 << 0);
pub const HELP: Self = OptFlag(1 << 1);
pub const VISIBLE_SHORT: Self = OptFlag(1 << 2);
pub const VISIBLE_FULL: Self = OptFlag(1 << 3);
pub const DEFAULT: Self = Self(Self::VISIBLE_SHORT.0 | Self::VISIBLE_FULL.0);
}
impl<ID> Opt<ID> {
#[inline]
const fn new(id: ID, names: OptIdentifier, value_name: Option<&'static str>, r#type: OptType) -> Self {
assert!(match names {
OptIdentifier::Single(_) => true,
OptIdentifier::Multi(names) => !names.is_empty(),
}, "Option names cannot be an empty slice");
Self { id, names, value_name, help_string: None, r#type, flags: OptFlag::DEFAULT }
}
pub const fn positional(id: ID, name: &'static str) -> Self {
Self::new(id, OptIdentifier::Single(name), None, OptType::Positional)
}
pub const fn help_flag(id: ID, names: &'static[&'static str]) -> Self {
Self::new(id, OptIdentifier::Multi(names), None, OptType::Flag)
.with_help_flag()
}
pub const fn flag(id: ID, names: &'static[&'static str]) -> Self {
Self::new(id, OptIdentifier::Multi(names), None, OptType::Flag)
}
pub const fn value(id: ID, names: &'static[&'static str], value_name: &'static str) -> Self {
Self::new(id, OptIdentifier::Multi(names), Some(value_name), OptType::Value)
}
#[inline]
pub const fn required(mut self) -> Self {
assert!(!self.is_help(), "Help flag cannot be made required");
self.flags.0 |= OptFlag::REQUIRED.0;
self
}
#[inline]
pub const fn help_text(mut self, help_string: &'static str) -> Self {
self.help_string = Some(help_string);
self
}
#[inline]
pub const fn hide_usage(mut self, from: OptHide) -> Self {
self.flags.0 &= !match from {
OptHide::Short => OptFlag::VISIBLE_SHORT.0,
OptHide::Full => OptFlag::VISIBLE_FULL.0,
OptHide::All => OptFlag::VISIBLE_SHORT.0 | OptFlag::VISIBLE_FULL.0,
};
self
}
#[inline]
const fn with_help_flag(mut self) -> Self {
assert!(matches!(self.r#type, OptType::Flag), "Only flags are allowed to be help options");
self.flags.0 |= OptFlag::HELP.0;
self
}
#[inline(always)]
pub const fn is_required(&self) -> bool {
(self.flags.0 & OptFlag::REQUIRED.0) != 0
}
#[inline(always)]
pub const fn is_help(&self) -> bool {
(self.flags.0 & OptFlag::HELP.0) != 0
}
#[inline(always)]
pub(crate) const fn is_short_visible(&self) -> bool {
(self.flags.0 & OptFlag::VISIBLE_SHORT.0) != 0
}
#[inline(always)]
pub(crate) const fn is_full_visible(&self) -> bool {
(self.flags.0 & OptFlag::VISIBLE_FULL.0) != 0
}
}
#[allow(dead_code)]
impl<ID: 'static> Opt<ID> {
pub const fn first_name(&self) -> &str {
match self.names {
OptIdentifier::Single(name) => name,
OptIdentifier::Multi(names) => names.first().unwrap(),
}
}
pub const fn first_long_name(&self) -> Option<&'static str> {
match self.names {
OptIdentifier::Single(name) => if name.len() >= 3 { Some(name) } else { None },
OptIdentifier::Multi(names) => {
let mut i = 0;
while i < names.len() {
if const_utf8::CharIterator::from(names[i]).count() >= 3 {
return Some(names[i]);
}
i += 1;
}
None
}
}
}
pub(crate) const fn first_short_name(&self) -> Option<&'static str> {
const fn predicate(name: &str) -> bool {
let mut chars = const_utf8::CharIterator::from(name);
if let Some(first) = chars.next() {
if let Some(c) = chars.next() {
if c != first && chars.next().is_none() {
return true
}
}
}
false
}
match self.names {
OptIdentifier::Single(name) => if predicate(name) { Some(name) } else { None },
OptIdentifier::Multi(names) => {
let mut i = 0;
while i < names.len() {
if predicate(names[i]) {
return Some(names[i]);
}
i += 1;
}
None
}
}
}
pub(crate) const fn first_short_name_char(&self) -> Option<char> {
const fn predicate(name: &str) -> Option<char> {
let mut chars = const_utf8::CharIterator::from(name);
if let Some(first) = chars.next() {
if let Some(c) = chars.next() {
if c != first && chars.next().is_none() {
return Some(c)
}
}
}
None
}
match self.names {
OptIdentifier::Single(name) => predicate(name),
OptIdentifier::Multi(names) => {
let mut i = 0;
while i < names.len() {
if let Some(c) = predicate(names[i]) {
return Some(c);
}
i += 1;
}
None
}
}
}
pub(crate) fn match_name(&self, string: &str, offset: usize) -> Option<&'static str> {
let rhs = &string[offset..];
if rhs.is_empty() {
return None;
}
match self.names {
OptIdentifier::Single(name) =>
if &name[offset..] == rhs { Some(name) } else { None },
OptIdentifier::Multi(names) =>
names.iter().find(|name| &name[offset..] == rhs).map(|v| &**v),
}
}
}
impl core::ops::BitOr for OptFlag {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output { Self(self.0 | rhs.0) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Option names cannot be an empty slice")]
fn test_new_empty_names_disallowed() {
Opt::new((), OptIdentifier::Multi(&[]), None, OptType::Positional);
}
#[test]
fn test_public_initialisers() {
assert_eq!(Opt::positional((), "name"), Opt { id: (),
names: OptIdentifier::Single("name"), value_name: None, help_string: None,
r#type: OptType::Positional, flags: OptFlag::DEFAULT,
});
assert_eq!(Opt::help_flag((), &["name"]), Opt { id: (),
names: OptIdentifier::Multi(&["name"]), value_name: None, help_string: None,
r#type: OptType::Flag, flags: OptFlag::DEFAULT | OptFlag::HELP,
});
assert_eq!(Opt::flag((), &["name"]), Opt { id: (),
names: OptIdentifier::Multi(&["name"]), value_name: None, help_string: None,
r#type: OptType::Flag, flags: OptFlag::DEFAULT,
});
assert_eq!(Opt::value((), &["name"], "value"), Opt { id: (),
names: OptIdentifier::Multi(&["name"]), value_name: Some("value"), help_string: None,
r#type: OptType::Value, flags: OptFlag::DEFAULT,
});
}
#[test]
fn test_valid_with_chains() {
assert_eq!(Opt::positional((), "").required(), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: None,
r#type: OptType::Positional, flags: OptFlag::DEFAULT | OptFlag::REQUIRED,
});
assert_eq!(Opt::positional((), "").required().help_text("help string"), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: Some("help string"),
r#type: OptType::Positional, flags: OptFlag::DEFAULT | OptFlag::REQUIRED,
});
assert_eq!(Opt::positional((), "").help_text("help string"), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: Some("help string"),
r#type: OptType::Positional, flags: OptFlag::DEFAULT,
});
assert_eq!(Opt::positional((), "").hide_usage(OptHide::Short), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: None,
r#type: OptType::Positional, flags: OptFlag::VISIBLE_FULL,
});
assert_eq!(Opt::positional((), "").hide_usage(OptHide::Full), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: None,
r#type: OptType::Positional, flags: OptFlag::VISIBLE_SHORT,
});
assert_eq!(Opt::positional((), "").hide_usage(OptHide::All), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: None,
r#type: OptType::Positional, flags: OptFlag::NONE,
});
assert_eq!(Opt::positional((), "").required().hide_usage(OptHide::All), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: None,
r#type: OptType::Positional, flags: OptFlag::REQUIRED,
});
}
#[test]
#[should_panic(expected = "Help flag cannot be made required")]
fn test_required_help_disallowed() {
Opt::help_flag((), &["-h", "--help"]).required();
}
#[test]
#[should_panic(expected = "Only flags are allowed to be help options")]
fn test_positional_with_help_flag_disallowed() {
Opt::positional((), "").with_help_flag();
}
#[test]
#[should_panic(expected = "Only flags are allowed to be help options")]
fn test_value_with_help_flag_disallowed() {
Opt::value((), &[""], "").with_help_flag();
}
#[test]
fn test_flag_getters() {
const HELP: Opt<()> = Opt::help_flag((), &[""]);
const REQUIRED: Opt<()> = Opt::positional((), "").required();
assert!(HELP.is_help());
assert!(!HELP.is_required());
assert!(REQUIRED.is_required());
assert!(!REQUIRED.is_help());
}
#[test]
fn test_first_name() {
assert_eq!(Opt::positional((), "first").first_name(), "first");
assert_eq!(Opt::flag((), &["first", "second"]).first_name(), "first");
}
#[test]
fn test_first_long_name() {
assert_eq!(Opt::positional((), "--long").first_long_name(), Some("--long"));
assert_eq!(Opt::positional((), "-long").first_long_name(), Some("-long"));
assert_eq!(Opt::positional((), "--l").first_long_name(), Some("--l"));
assert_eq!(Opt::positional((), "-s").first_long_name(), None);
assert_eq!(Opt::flag((), &["-s", "--long"]).first_long_name(), Some("--long"));
}
#[test]
fn test_first_short_name() {
assert_eq!(Opt::positional((), "-s").first_short_name(), Some("-s"));
assert_eq!(Opt::positional((), "--long").first_short_name(), None);
assert_eq!(Opt::positional((), "--").first_short_name(), None);
assert_eq!(Opt::positional((), "-lo").first_short_name(), None);
assert_eq!(Opt::positional((), "--l").first_short_name(), None);
assert_eq!(Opt::flag((), &["--long", "-s"]).first_short_name(), Some("-s"));
}
#[test]
fn test_first_short_name_char() {
assert_eq!(Opt::positional((), "-s").first_short_name_char(), Some('s'));
assert_eq!(Opt::positional((), "--long").first_short_name_char(), None);
assert_eq!(Opt::positional((), "--").first_short_name_char(), None);
assert_eq!(Opt::positional((), "-lo").first_short_name_char(), None);
assert_eq!(Opt::positional((), "--l").first_short_name_char(), None);
assert_eq!(Opt::flag((), &["--long", "-s"]).first_short_name_char(), Some('s'));
}
#[test]
fn test_match_name() {
assert_eq!(Opt::flag((), &["--one", "--two", "--threee", "--three"])
.match_name("--three", 0), Some("--three"));
assert_eq!(Opt::flag((), &["--one", "--two", "--threee"])
.match_name("--three", 0), None);
assert_eq!(Opt::flag((), &["/one", "/two", "/three", "/four"])
.match_name("-three", 1), Some("/three"));
assert_eq!(Opt::positional((), "-s").match_name("-s", 1), Some("-s"));
assert_eq!(Opt::flag((), &["-x", "-s"]).match_name("-s", 2), None);
assert_eq!(Opt::positional((), "-x").match_name("-s", 2), None);
}
}