Skip to content

Instantly share code, notes, and snippets.

@kangalio
Created March 28, 2021 15:45
Show Gist options
  • Save kangalio/56385cfed9de08b5b2b45eb89cb4a851 to your computer and use it in GitHub Desktop.
Save kangalio/56385cfed9de08b5b2b45eb89cb4a851 to your computer and use it in GitHub Desktop.
#![allow(clippy::or_fun_call)]
// deliberately not copy with the intention to prevent accidental copies and confusion
#[derive(Clone, Debug)]
pub struct Arguments<'a> {
pub args: &'a str,
}
impl<'a> Arguments<'a> {
/// Pop a whitespace-separated word from the front of the arguments.
///
/// Leading whitespace will be trimmed; trailing whitespace is not consumed.
///
/// ```rust
/// assert_eq!(Arguments { args: "test" }.pop_word(), Some("test") };
/// assert_eq!(Arguments { args: " test" }.pop_word(), Some("test") };
/// assert_eq!(Arguments { args: " test " }.pop_word(), Some("test") };
/// ```
pub fn with_popped_word(&self) -> Option<(Self, &'a str)> {
let args = self.args.trim_start();
// Split " a b c" into "a" and " b c"
let (word, remnant) = args.split_at(args.find(char::is_whitespace).unwrap_or(args.len()));
match word.is_empty() {
true => None,
false => Some((Self { args: remnant }, word)),
}
}
}
/// Parse a value out of a string by popping off the front of the string.
pub trait ParseConsuming<'a>: Sized {
fn pop_from(args: &Arguments<'a>) -> Option<(Arguments<'a>, Self)>;
}
impl<'a> ParseConsuming<'a> for &'a str {
fn pop_from(args: &Arguments<'a>) -> Option<(Arguments<'a>, Self)> {
args.with_popped_word()
}
}
// TODO: wrap serenity::utils::Parse instead of FromStr
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Wrapper<T>(pub T);
impl<'a, T: std::str::FromStr> ParseConsuming<'a> for Wrapper<T> {
fn pop_from(args: &Arguments<'a>) -> Option<(Arguments<'a>, Self)> {
let (args, word) = args.with_popped_word()?;
Some((args, Self(word.parse().ok()?)))
}
}
#[derive(Debug, PartialEq)]
struct CodeBlock<'a> {
code: &'a str,
language: Option<&'a str>,
}
impl<'a> ParseConsuming<'a> for CodeBlock<'a> {
fn pop_from(args: &Arguments<'a>) -> Option<(Arguments<'a>, Self)> {
// TODO: support ``` codeblocks and language annotations
let args_after_start_backtick = args.args.trim_start().strip_prefix('`')?;
let end_backtick_pos = args_after_start_backtick.find('`')?;
let args = Arguments {
args: &args_after_start_backtick[(end_backtick_pos + 1)..],
};
let codeblock = Self {
code: &args_after_start_backtick[..end_backtick_pos],
language: None,
};
Some((args, codeblock))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParseError;
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Failed to parse arguments")
}
}
impl std::error::Error for ParseError {}
#[doc(hidden)]
#[macro_export]
macro_rules! _parse {
// All arguments have been consumed
( $args:ident => [ $( $name:ident )* ] ) => {
if $args.with_popped_word().is_none() {
return Ok(( $( $name ),* ));
}
};
// Consume Option<T> greedy-first
( $args:ident => [ $($preamble:tt)* ]
(Option<$type:ty>)
$( $rest:tt )*
) => {
if let Some(($args, token)) = <$type>::pop_from(&$args) {
let token: Option<$type> = Some(token);
$crate::_parse!($args => [ $($preamble)* token ] $($rest)* );
}
let token: Option<$type> = None;
$crate::_parse!($args => [ $($preamble)* token ] $($rest)* );
};
// Consume Option<T> lazy-first
( $args:ident => [ $($preamble:tt)* ]
(lazy Option<$type:ty>)
$( $rest:tt )*
) => {
let token: Option<$type> = None;
$crate::_parse!($args => [ $($preamble)* token ] $($rest)* );
if let Some(($args, token)) = <$type>::pop_from(&$args) {
let token: Option<$type> = Some(token);
$crate::_parse!($args => [ $($preamble)* token ] $($rest)* );
}
};
// Consume Vec<T> greedy-first
( $args:ident => [ $($preamble:tt)* ]
(Vec<$type:ty>)
$( $rest:tt )*
) => {
let mut tokens = Vec::new();
let mut token_rest_args = Vec::new();
token_rest_args.push($args.clone());
let mut running_args = $args.clone();
while let Some((popped_args, token)) = <$type>::pop_from(&running_args) {
tokens.push(token);
token_rest_args.push(popped_args.clone());
running_args = popped_args;
}
while let Some(token_rest_args) = token_rest_args.pop() {
$crate::_parse!(token_rest_args => [ $($preamble)* tokens ] $($rest)* );
tokens.pop();
}
};
// Consume T
( $args:ident => [ $( $preamble:ident )* ]
($type:ty)
$( $rest:tt )*
) => {
if let Some(($args, token)) = <$type>::pop_from(&$args) {
$crate::_parse!($args => [ $($preamble)* token ] $($rest)* );
}
};
}
#[macro_export]
macro_rules! parse_args {
($args:expr => $(
$( #[$attr:ident] )?
( $($type:tt)* )
),* $(,)? ) => {
move || -> Result<( $($($type)*),* ), $crate::ParseError> {
use $crate::ParseConsuming as _;
let args = $crate::Arguments { args: $args };
$crate::_parse!(
args => []
$(
($($attr)? $($type)*)
)*
);
Err($crate::ParseError)
}()
};
}
#[test]
fn test_parse_args() {
assert_eq!(
parse_args!("hello" => (Option<&str>), (&str)),
Ok((None, "hello")),
);
assert_eq!(
parse_args!("a b c" => (Vec<&str>), (&str)),
Ok((vec!["a", "b"], "c")),
);
assert_eq!(
parse_args!("a b c" => (Vec<&str>), (Vec<&str>)),
Ok((vec!["a", "b", "c"], vec![])),
);
assert_eq!(
parse_args!("a b 8 c" => (Vec<&str>), (Wrapper<u32>), (Vec<&str>)),
Ok((vec!["a", "b"], Wrapper(8), vec!["c"])),
);
assert_eq!(
parse_args!("yoo `that's cool` !" => (&str), (CodeBlock<'_>), (&str)),
Ok((
"yoo",
CodeBlock {
code: "that's cool",
language: None
},
"!"
)),
);
assert_eq!(
parse_args!("hi" => #[lazy] (Option<&str>), (Option<&str>)),
Ok((None, Some("hi"))),
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment