Created
March 28, 2021 15:45
-
-
Save kangalio/56385cfed9de08b5b2b45eb89cb4a851 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#![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