use std::collections::HashMap;
#[derive(Debug)]
pub enum Error {
ValidationError {
name: String,
value: String,
type_: Type,
},
WrongType {
name: String,
value: Value,
expected_type: Type,
},
Missing {
name: String,
type_: Type,
},
Extra(Emitted),
Duplicate(Emitted),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug)]
pub enum Value {
Bool(bool),
String(String),
Int(i64),
Float(f64),
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Type {
Bool,
String,
Int,
Float,
Optional(&'static Type),
}
#[derive(Clone, Debug)]
pub enum Emitted {
Positional(usize, String),
Named(String, Value),
}
#[derive(Clone, Debug)]
pub enum EmittedRef<'a> {
Positional(usize, &'a str),
Named(&'a str, &'a Value),
}
pub type TypeDefs = HashMap<String, Type>;
pub struct Parser<I: Iterator<Item = String>> {
input: I,
options: ParserOptions,
positional_index: usize,
the_rest_are_positional: bool,
}
pub struct ParserOptions {
definitions: TypeDefs,
default_type: Type,
single_shorthand: bool,
the_rest_are_positional: bool,
equals_assignment: bool,
reject_undefined: bool,
reject_positional: bool,
}
impl Value {
pub fn type_(&self) -> Type {
match self {
Value::Int(_) => Type::Int,
Value::Float(_) => Type::Float,
Value::Bool(_) => Type::Bool,
Value::String(_) => Type::String,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Value::Bool(value) => Some(*value),
_ => None,
}
}
pub fn as_float(&self) -> Option<f64> {
match self {
Value::Int(value) => Some(*value as f64),
Value::Float(value) => Some(*value),
_ => None,
}
}
pub fn as_int(&self) -> Option<i64> {
match self {
Value::Int(value) => Some(*value),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
Value::String(value) => Some(value.as_str()),
_ => None,
}
}
pub fn as_string(self) -> Option<String> {
match self {
Value::String(value) => Some(value),
_ => None,
}
}
}
impl Type {
pub fn optional(self) -> Self {
match self {
value @ Type::Optional(_) => value,
Type::Bool => Type::Bool,
Type::Int => {
static TYPE: Type = Type::Int;
Type::Optional(&TYPE)
}
Type::Float => {
static TYPE: Type = Type::Float;
Type::Optional(&TYPE)
}
Type::String => {
static TYPE: Type = Type::String;
Type::Optional(&TYPE)
}
}
}
pub fn inner(self) -> Self {
match self {
Type::Optional(inner) => *inner,
value => value,
}
}
pub fn parse(&self, s: &str) -> Option<Value> {
match self {
Type::Bool => s.parse().ok().map(Value::Bool),
Type::String => Some(Value::String(s.into())),
Type::Int => s.parse().ok().map(Value::Int),
Type::Float => s.parse().ok().map(Value::Float),
_ => None,
}
}
pub fn was_missing<S: ToString>(self, name: S) -> Error {
Error::Missing {
name: name.to_string(),
type_: self,
}
}
}
impl Emitted {
pub fn into_value(self) -> Value {
match self {
Emitted::Positional(_, v) => Value::String(v),
Emitted::Named(_, v) => v,
}
}
pub fn as_ref<'a>(&'a self) -> EmittedRef<'a> {
match self {
Emitted::Positional(i, v) => EmittedRef::Positional(*i, v.as_str()),
Emitted::Named(n, v) => EmittedRef::Named(n.as_str(), &v),
}
}
pub fn was_duplicate(self) -> Error {
match self {
Emitted::Named(_, _) => Error::Duplicate(self),
_ => unreachable!("A positional argument cannot be a duplicate"),
}
}
pub fn was_extra(self) -> Error {
Error::Extra(self)
}
}
pub mod generic {
use super::*;
#[derive(Default, Debug)]
pub struct Generic {
tagged: HashMap<String, Value>,
remaining: Vec<Value>,
}
use core::ops::Index;
pub trait Key {
fn index_into<'a>(&self, value: &'a Generic) -> Option<&'a Value>;
}
impl Key for &str {
fn index_into<'a>(&self, value: &'a Generic) -> Option<&'a Value> {
value.tagged.get(&self.to_string())
}
}
impl Key for usize {
fn index_into<'a>(&self, value: &'a Generic) -> Option<&'a Value> {
value.remaining.get(*self)
}
}
impl<K: Key> Index<K> for Generic {
type Output = Value;
fn index(&self, key: K) -> &Value {
key.index_into(self).unwrap()
}
}
use std::fmt::Display;
impl Generic {
pub fn get<K: Key>(&self, key: K) -> Option<&Value> {
key.index_into(self)
}
pub fn check<K: Key + Display>(&self, key: K, type_: Type) -> Result<&Value> {
key.index_into(self)
.ok_or_else(|| Error::Missing {
name: key.to_string(),
type_,
})
.and_then(|value| {
if value.type_().optional() != type_.optional() {
Err(Error::WrongType {
name: key.to_string(),
value: value.clone(),
expected_type: type_,
})
} else {
Ok(value)
}
})
}
pub fn check_bool<K: Key + Display>(&self, key: K) -> Result<bool> {
match key.index_into(self) {
Some(Value::Bool(value)) => Ok(*value),
Some(value) => Err(Error::WrongType {
name: key.to_string(),
value: value.clone(),
expected_type: Type::Bool,
}),
None => Ok(false),
}
}
}
impl std::iter::FromIterator<Emitted> for Generic {
fn from_iter<T: IntoIterator<Item = Emitted>>(iter: T) -> Self {
let mut result = Generic::default();
for value in iter.into_iter() {
match value {
Emitted::Positional(_i, value) => {
result.remaining.push(Value::String(value));
}
Emitted::Named(name, value) => {
result.tagged.insert(name, value);
}
}
}
result
}
}
}
impl Default for ParserOptions {
fn default() -> Self {
Self {
definitions: Default::default(),
default_type: Type::Bool,
single_shorthand: Default::default(),
the_rest_are_positional: Default::default(),
equals_assignment: Default::default(),
reject_undefined: Default::default(),
reject_positional: Default::default(),
}
}
}
macro_rules! setter {
($name:ident: $type:ty) => (
pub fn $name(mut self, value: $type) -> Self {
self.$name = value;
self
}
)
}
impl ParserOptions {
setter!(definitions: TypeDefs);
setter!(default_type: Type);
setter!(single_shorthand: bool);
setter!(the_rest_are_positional: bool);
setter!(equals_assignment: bool);
setter!(reject_undefined: bool);
setter!(reject_positional: bool);
pub fn need<S: ToString>(mut self, name: S, type_: Type) -> Self {
self.definitions.insert(name.to_string(), type_);
self
}
pub fn want<S: ToString>(self, name: S, type_: Type) -> Self {
self.need(name, type_.optional())
}
pub fn build<I: Iterator<Item = String>>(self, input: I) -> Parser<I> {
Parser {
input,
positional_index: 0,
options: self,
the_rest_are_positional: false,
}
}
pub fn build_from_args(self) -> Parser<std::iter::Skip<std::env::Args>> {
self.build(std::env::args().skip(1))
}
}
pub fn parse_args() -> impl Iterator<Item = Result<Emitted>> {
ParserOptions::default().build_from_args()
}
macro_rules! post_increment {
($e:expr) => {{
let v = &mut $e;
let ret = *v;
*v += 1;
ret
}};
}
impl<I: Iterator<Item = String>> Iterator for Parser<I> {
type Item = Result<Emitted>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(arg) = self.input.next() {
if self.the_rest_are_positional {
return Some(Ok(Emitted::Positional(
post_increment!(self.positional_index),
arg.into(),
)));
}
let name: String = if arg == "--" {
self.the_rest_are_positional = true;
continue;
} else if arg.starts_with("--") {
arg.chars().skip(2).collect()
} else if arg.starts_with("-") {
arg.chars().skip(1).collect()
} else {
return Some(Ok(Emitted::Positional(
post_increment!(self.positional_index),
arg.into(),
)));
};
let type_ = self
.options
.definitions
.get(&name)
.unwrap_or_else(|| &self.options.default_type);
let value = match type_.inner() {
Type::Bool => Value::Bool(true),
type_ => match self.input.next() {
None => return Some(Err(Error::Missing { name, type_ })),
Some(value) => match type_.parse(&value) {
Some(result) => result,
None => return Some(Err(Error::ValidationError { name, type_, value })),
},
},
};
return Some(Ok(Emitted::Named(name, value)));
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! assert_match {
($e:expr, $p:pat) => {
match $e {
$p => (),
v => assert!(false, "Expected {}, got {:?}", std::stringify!($p), v),
}
};
}
#[test]
fn it_works() {
#[derive(Default, PartialEq, Eq, Debug)]
struct Opt {
a: bool,
}
fn parse<'a, I: IntoIterator<Item = &'a str>>(input: I) -> Result<Opt> {
let mut opt = Opt::default();
for arg in ParserOptions::default().build(input.into_iter().map(|v| v.to_string())) {
let arg = arg?;
match arg.as_ref() {
EmittedRef::Named("a", Value::Bool(v))
| EmittedRef::Named("homogeneous-arrays", Value::Bool(v)) => {
if opt.a {
return Err(Error::Duplicate(arg));
}
opt.a = *v;
}
_ => return Err(Error::Extra(arg)),
}
}
Ok(opt)
}
macro_rules! assert_match {
($e:expr, $p:pat) => {
match $e {
$p => (),
_ => assert!(false),
}
};
}
assert_match!(parse(vec![]), Ok(Opt { a: false }));
assert_match!(parse(vec!["-a"]), Ok(Opt { a: true }));
assert_match!(parse(vec!["-a", "-a"]), Err(_));
}
#[test]
fn it_works2() {
#[derive(Default, PartialEq, Eq, Debug)]
struct Opt {
a: String,
}
fn parse<'a, I: IntoIterator<Item = &'a str>>(input: I) -> Result<Opt> {
use EmittedRef::*;
let mut a = None;
let input = input.into_iter().map(|v| v.to_string());
for arg in ParserOptions::default()
.need("a", Type::String)
.need("homogeneous-arrays", Type::String)
.build(input)
{
let arg = arg?;
match arg.as_ref() {
Named("a", _) | Named("homogeneous-arrays", _) => {
if a.is_some() {
return Err(arg.was_duplicate());
}
a = arg.into_value().as_string();
}
_ => return Err(arg.was_extra()),
}
}
Ok(Opt {
a: a.ok_or_else(|| Type::String.was_missing("a"))?,
})
}
assert_match!(parse(vec![]), Err(_));
assert_match!(parse(vec!["-a"]), Err(_));
assert_match!(parse(vec!["-a", "-a"]), Ok(Opt { .. }));
assert_eq!(parse(vec!["-a", "-a"]).unwrap().a, "-a");
assert_match!(parse(vec!["--homogeneous-arrays", "-a"]), Ok(Opt { .. }));
assert_eq!(parse(vec!["--homogeneous-arrays", "-a"]).unwrap().a, "-a");
assert_match!(
parse(vec!["--homogeneus-arrays", "-a"]),
Err(Error::Extra(_))
);
{
let e = dbg!(parse(vec!["--homogeneus-arrays", "-a"]));
assert_match!(&e, Err(Error::Extra(_)));
if let Err(Error::Extra(e)) = e {
assert_match!(e.as_ref(), EmittedRef::Named("homogeneus-arrays", _));
if let EmittedRef::Named("homogeneus-arrays", e) = e.as_ref() {
assert_eq!(e.type_(), Type::Bool);
assert_eq!(e.as_str(), None);
}
}
}
}
#[test]
fn it_works3() {
fn parse<'a, I: IntoIterator<Item = &'a str>>(input: I) -> Result<()> {
let input = input.into_iter().map(|v| v.to_string());
for arg in ParserOptions::default()
.default_type(Type::String)
.build(input)
.flatten()
{
return Err(arg.was_extra());
}
Ok(())
}
{
let e = dbg!(parse(vec!["--homogeneus-arrays", "-a"]));
assert_match!(&e, Err(Error::Extra(_)));
if let Err(Error::Extra(e)) = e {
assert_match!(e.as_ref(), EmittedRef::Named("homogeneus-arrays", _));
if let EmittedRef::Named("homogeneus-arrays", e) = e.as_ref() {
assert_eq!(e.as_str(), Some("-a"));
}
}
}
}
}