From 300539fdda6ad5e38214ec7f7945635f9b9378cd Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 14 Jan 2025 11:53:56 +0900 Subject: [PATCH] feat(commands): add typed flags, misplaced and non-applicable flags error reporting --- crates/commands/src/commands.rs | 77 ++++++++-- crates/commands/src/commands.udl | 2 +- crates/commands/src/commands/member.rs | 9 +- crates/commands/src/flag.rs | 102 +++++++++++++ crates/commands/src/lib.rs | 204 ++++++++++++++++++------- crates/commands/src/string.rs | 118 +++++++++++--- crates/commands/src/token.rs | 71 ++++----- crates/commands/src/tree.rs | 10 +- 8 files changed, 454 insertions(+), 139 deletions(-) create mode 100644 crates/commands/src/flag.rs diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index 04ada503..3326ca1b 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -24,16 +24,19 @@ use smol_str::SmolStr; use crate::{ command, - token::{ToToken, Token}, + flag::{Flag, FlagValue}, + token::Token, }; #[derive(Debug, Clone)] pub struct Command { // TODO: fix hygiene pub tokens: Vec, + pub flags: Vec, pub help: SmolStr, pub cb: SmolStr, pub show_in_suggestions: bool, + pub parse_flags_before: usize, } impl Command { @@ -41,36 +44,88 @@ impl Command { tokens: impl IntoIterator, help: impl Into, cb: impl Into, - show_in_suggestions: bool, ) -> Self { + let tokens = tokens.into_iter().collect::>(); + assert!(tokens.len() > 0); + let mut parse_flags_before = tokens.len(); + let mut was_parameter = true; + for (idx, token) in tokens.iter().enumerate().rev() { + match token { + Token::FullString(_) + | Token::MemberRef(_) + | Token::MemberPrivacyTarget(_) + | Token::SystemRef(_) + | Token::PrivacyLevel(_) + | Token::Toggle(_) + | Token::Enable(_) + | Token::Disable(_) + | Token::Reset(_) + | Token::Any(_) => { + parse_flags_before = idx; + was_parameter = true; + } + Token::Empty | Token::Value(_) => { + if was_parameter { + break; + } + } + } + } Self { - tokens: tokens.into_iter().collect(), + flags: Vec::new(), help: help.into(), cb: cb.into(), - show_in_suggestions, + show_in_suggestions: true, + parse_flags_before, + tokens, } } + + pub fn show_in_suggestions(mut self, v: bool) -> Self { + self.show_in_suggestions = v; + self + } + + pub fn flag(mut self, name: impl Into) -> Self { + self.flags.push(Flag::new(name)); + self + } + + pub fn value_flag(mut self, name: impl Into, value: FlagValue) -> Self { + self.flags.push(Flag::new(name).with_value(value)); + self + } } impl Display for Command { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for (idx, token) in self.tokens.iter().enumerate() { - write!(f, "{}", token)?; - if idx < self.tokens.len() - 1 { - write!(f, " ")?; + if idx == self.parse_flags_before { + for flag in &self.flags { + write!(f, "[{flag}] ")?; + } + } + write!( + f, + "{token}{}", + (idx < self.tokens.len() - 1).then_some(" ").unwrap_or("") + )?; + } + if self.tokens.len() == self.parse_flags_before { + for flag in &self.flags { + write!(f, " [{flag}]")?; } } Ok(()) } } +// a macro is required because generic cant be different types at the same time (which means you couldnt have ["member", MemberRef, "subcmd"] etc) +// (and something like &dyn Trait would require everything to be referenced which doesnt look nice anyway) #[macro_export] macro_rules! command { - ([$($v:expr),+], $cb:expr, $help:expr, suggest = $suggest:expr) => { - $crate::commands::Command::new([$($v.to_token()),*], $help, $cb, $suggest) - }; ([$($v:expr),+], $cb:expr, $help:expr) => { - $crate::command!([$($v),+], $cb, $help, suggest = true) + $crate::commands::Command::new([$(Token::from($v)),*], $help, $cb) }; } diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 5080c4b9..824ec5a5 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -19,5 +19,5 @@ interface Parameter { dictionary ParsedCommand { string command_ref; record params; - record flags; + record flags; }; diff --git a/crates/commands/src/commands/member.rs b/crates/commands/src/commands/member.rs index 534c7417..894e09a6 100644 --- a/crates/commands/src/commands/member.rs +++ b/crates/commands/src/commands/member.rs @@ -18,7 +18,8 @@ pub fn cmds() -> impl Iterator { [member, MemberRef("target")], "member_show", "Shows information about a member" - ), + ) + .value_flag("pt", FlagValue::OpaqueString), command!( [member, MemberRef("target"), description], "member_desc_show", @@ -53,9 +54,9 @@ pub fn cmds() -> impl Iterator { command!( [member, MemberRef("target"), "soulscream"], "member_soulscream", - "todo", - suggest = false - ), + "todo" + ) + .show_in_suggestions(false), ] .into_iter() } diff --git a/crates/commands/src/flag.rs b/crates/commands/src/flag.rs new file mode 100644 index 00000000..f9facb4e --- /dev/null +++ b/crates/commands/src/flag.rs @@ -0,0 +1,102 @@ +use std::fmt::Display; + +use smol_str::SmolStr; + +use crate::Parameter; + +#[derive(Debug, Clone)] +pub enum FlagValue { + OpaqueString, +} + +impl FlagValue { + fn try_match(&self, input: &str) -> Result { + if input.is_empty() { + return Err(FlagValueMatchError::ValueMissing); + } + + match self { + Self::OpaqueString => Ok(Parameter::OpaqueString { raw: input.into() }), + } + } +} + +impl Display for FlagValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FlagValue::OpaqueString => write!(f, "value"), + } + } +} + +#[derive(Debug)] +pub enum FlagValueMatchError { + ValueMissing, +} + +#[derive(Debug, Clone)] +pub struct Flag { + name: SmolStr, + value: Option, +} + +impl Display for Flag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "-{}", self.name)?; + if let Some(value) = self.value.as_ref() { + write!(f, "={value}")?; + } + Ok(()) + } +} + +#[derive(Debug)] +pub enum FlagMatchError { + ValueMatchFailed(FlagValueMatchError), +} + +type TryMatchFlagResult = Option, FlagMatchError>>; + +impl Flag { + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + value: None, + } + } + + pub fn with_value(mut self, value: FlagValue) -> Self { + self.value = Some(value); + self + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn value(&self) -> Option<&FlagValue> { + self.value.as_ref() + } + + pub fn try_match(&self, input_name: &str, input_value: Option<&str>) -> TryMatchFlagResult { + // if not matching flag then skip anymore matching + if self.name != input_name { + return None; + } + // get token to try matching with, if flag doesn't have one then that means it is matched (it is without any value) + let Some(value) = self.value() else { + return Some(Ok(None)); + }; + // check if we have a non-empty flag value, we return error if not (because flag requested a value) + let Some(input_value) = input_value else { + return Some(Err(FlagMatchError::ValueMatchFailed( + FlagValueMatchError::ValueMissing, + ))); + }; + // try matching the value + match value.try_match(input_value) { + Ok(param) => Some(Ok(Some(param))), + Err(err) => Some(Err(FlagMatchError::ValueMatchFailed(err))), + } + } +} diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index d96727a3..422109ef 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -2,6 +2,7 @@ #![feature(anonymous_lifetime_in_impl_trait)] pub mod commands; +mod flag; mod string; mod token; mod tree; @@ -13,7 +14,9 @@ use std::collections::HashMap; use std::fmt::Write; use std::ops::Not; +use flag::{Flag, FlagMatchError, FlagValueMatchError}; use smol_str::SmolStr; +use string::MatchedFlag; use tree::TreeBranch; pub use commands::Command; @@ -53,7 +56,7 @@ pub enum Parameter { pub struct ParsedCommand { pub command_ref: String, pub params: HashMap, - pub flags: HashMap>, + pub flags: HashMap>, } pub fn parse_command(prefix: String, input: String) -> CommandResult { @@ -61,24 +64,23 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { let mut local_tree: TreeBranch = COMMAND_TREE.clone(); // end position of all currently matched tokens - let mut current_pos = 0; + let mut current_pos: usize = 0; + let mut current_token_idx: usize = 0; let mut params: HashMap = HashMap::new(); - let mut flags: HashMap> = HashMap::new(); + let mut raw_flags: Vec<(usize, MatchedFlag)> = Vec::new(); loop { - let possible_tokens = local_tree.possible_tokens().cloned().collect::>(); - println!("possible: {:?}", possible_tokens); - let next = next_token(possible_tokens.clone(), &input, current_pos); + println!( + "possible: {:?}", + local_tree.possible_tokens().collect::>() + ); + let next = next_token(local_tree.possible_tokens(), &input, current_pos); println!("next: {:?}", next); match next { Some(Ok((found_token, arg, new_pos))) => { current_pos = new_pos; - if let Token::Flag = found_token { - flags.insert(arg.unwrap().raw.into(), None); - // don't try matching flags as tree elements - continue; - } + current_token_idx += 1; if let Some(arg) = arg.as_ref() { // insert arg as paramater if this is a parameter @@ -117,17 +119,7 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { return CommandResult::Err { error: error_msg }; } None => { - if let Some(command) = local_tree.command() { - println!("{} {params:?}", command.cb); - return CommandResult::Ok { - command: ParsedCommand { - command_ref: command.cb.into(), - params, - flags, - }, - }; - } - + // if it said command not found on a flag, output better error message let mut error = format!("Unknown command `{prefix}{input}`."); if fmt_possible_commands(&mut error, &prefix, local_tree.possible_commands(2)).not() @@ -144,9 +136,127 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { return CommandResult::Err { error }; } } + // match flags until there are none left + while let Some(matched_flag) = string::next_flag(&input, current_pos) { + current_pos = matched_flag.next_pos; + println!("flag matched {matched_flag:?}"); + raw_flags.push((current_token_idx, matched_flag)); + } + // if we have a command, stop parsing and return it + if let Some(command) = local_tree.command() { + // match the flags against this commands flags + let mut flags: HashMap> = HashMap::new(); + let mut misplaced_flags: Vec = Vec::new(); + let mut invalid_flags: Vec = Vec::new(); + for (token_idx, matched_flag) in raw_flags { + if token_idx != command.parse_flags_before { + misplaced_flags.push(matched_flag); + continue; + } + let Some(matched_flag) = match_flag(command.flags.iter(), matched_flag.clone()) + else { + invalid_flags.push(matched_flag); + continue; + }; + match matched_flag { + // a flag was matched + Ok((name, value)) => { + flags.insert(name.into(), value); + } + Err((flag, err)) => { + match err { + FlagMatchError::ValueMatchFailed(FlagValueMatchError::ValueMissing) => { + return CommandResult::Err { + error: format!( + "Flag `-{name}` in command `{prefix}{input}` is missing a value, try passing `-{name}={value}`.", + name = flag.name(), + value = flag.value().expect("value missing error cant happen without a value"), + ), + } + } + } + } + } + } + if misplaced_flags.is_empty().not() { + let mut error = format!( + "Flag{} ", + (misplaced_flags.len() > 1).then_some("s").unwrap_or("") + ); + for (idx, matched_flag) in misplaced_flags.iter().enumerate() { + write!(&mut error, "`-{}`", matched_flag.name).expect("oom"); + if idx < misplaced_flags.len() - 1 { + error.push_str(", "); + } + } + write!( + &mut error, + " in command `{prefix}{input}` {} misplaced. Try reordering to match the command usage `{prefix}{command}`.", + (misplaced_flags.len() > 1).then_some("are").unwrap_or("is") + ).expect("oom"); + return CommandResult::Err { error }; + } + if invalid_flags.is_empty().not() { + let mut error = format!( + "Flag{} ", + (misplaced_flags.len() > 1).then_some("s").unwrap_or("") + ); + for (idx, matched_flag) in invalid_flags.iter().enumerate() { + write!(&mut error, "`-{}`", matched_flag.name).expect("oom"); + if idx < invalid_flags.len() - 1 { + error.push_str(", "); + } + } + write!( + &mut error, + " {} not applicable in this command (`{prefix}{input}`). Applicable flags are the following:", + (invalid_flags.len() > 1).then_some("are").unwrap_or("is") + ).expect("oom"); + for (idx, flag) in command.flags.iter().enumerate() { + write!(&mut error, " `{flag}`").expect("oom"); + if idx < command.flags.len() - 1 { + error.push_str(", "); + } + } + error.push_str("."); + return CommandResult::Err { error }; + } + println!("{} {flags:?} {params:?}", command.cb); + return CommandResult::Ok { + command: ParsedCommand { + command_ref: command.cb.into(), + params, + flags, + }, + }; + } } } +fn match_flag<'a>( + possible_flags: impl Iterator, + matched_flag: MatchedFlag<'a>, +) -> Option), (&'a Flag, FlagMatchError)>> { + // skip if 0 length (we could just take an array ref here and in next_token aswell but its nice to keep it flexible) + if let (_, Some(len)) = possible_flags.size_hint() + && len == 0 + { + return None; + } + + // check for all (possible) flags, see if token matches + for flag in possible_flags { + println!("matching flag {flag:?}"); + match flag.try_match(matched_flag.name, matched_flag.value) { + Some(Ok(param)) => return Some(Ok((flag.name().into(), param))), + Some(Err(err)) => return Some(Err((flag, err))), + None => {} + } + } + + None +} + /// Find the next token from an either raw or partially parsed command string /// /// Returns: @@ -155,37 +265,27 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { /// - matched value (if this command matched an user-provided value such as a member name) /// - end position of matched token /// - error when matching -fn next_token( - possible_tokens: Vec, +fn next_token<'a>( + possible_tokens: impl Iterator, input: &str, current_pos: usize, -) -> Option, usize), (Token, TokenMatchError)>> { - // get next parameter, matching quotes - let matched = crate::string::next_param(&input, current_pos); - println!("matched: {matched:?}\n---"); - - // try checking if this is a flag - // note: if the param starts with - and if a "match remainder" token was going to be matched - // this is going to override that. to prevent that the param should be quoted - if let Some(param) = matched.as_ref() - && param.in_quotes.not() - && param.value.starts_with('-') +) -> Option, usize), (&'a Token, TokenMatchError)>> { + // skip if 0 length + if let (_, Some(len)) = possible_tokens.size_hint() + && len == 0 { - return Some(Ok(( - Token::Flag, - Some(TokenMatchedValue { - raw: param.value.into(), - param: None, - }), - param.next_pos, - ))); + return None; } + // get next parameter, matching quotes + let matched = string::next_param(&input, current_pos); + println!("matched: {matched:?}\n---"); + // iterate over tokens and run try_match for token in possible_tokens { let is_match_remaining_token = |token: &Token| matches!(token, Token::FullString(_)); // check if this is a token that matches the rest of the input - let match_remaining = is_match_remaining_token(&token) + let match_remaining = is_match_remaining_token(token) // check for Any here if it has a "match remainder" token in it // if there is a "match remainder" token in a command there shouldn't be a command descending from that || matches!(token, Token::Any(ref tokens) if tokens.iter().any(is_match_remaining_token)); @@ -197,12 +297,14 @@ fn next_token( }); match token.try_match(input_to_match) { Some(Ok(value)) => { - // return last possible pos if we matched remaining, - // otherwise use matched param next pos, - // and if didnt match anything we stay where we are - let next_pos = matched - .map(|v| match_remaining.then_some(input.len()).unwrap_or(v.next_pos)) - .unwrap_or(current_pos); + let next_pos = match matched { + // return last possible pos if we matched remaining, + Some(_) if match_remaining => input.len(), + // otherwise use matched param next pos, + Some(param) => param.next_pos, + // and if didnt match anything we stay where we are + None => current_pos, + }; return Some(Ok((token, value, next_pos))); } Some(Err(err)) => { @@ -223,7 +325,7 @@ fn fmt_possible_commands( mut possible_commands: impl Iterator, ) -> bool { if let Some(first) = possible_commands.next() { - f.push_str(" Perhaps you meant to use one of the commands below:\n"); + f.push_str(" Perhaps you meant one of the following commands:\n"); for command in std::iter::once(first).chain(possible_commands.take(MAX_SUGGESTIONS - 1)) { if !command.show_in_suggestions { continue; diff --git a/crates/commands/src/string.rs b/crates/commands/src/string.rs index 1ca2b2c4..9b66da4c 100644 --- a/crates/commands/src/string.rs +++ b/crates/commands/src/string.rs @@ -42,16 +42,35 @@ lazy_static::lazy_static! { }; } +// very very simple quote matching +// expects match_str to be trimmed (no whitespace, from the start at least) +// returns the position of an end quote if any is found +// quotes need to be at start/end of words, and are ignored if a closing quote is not present +// WTB POSIX quoting: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html +fn find_quotes(match_str: &str) -> Option { + if let Some(right) = QUOTE_PAIRS.get(&match_str[0..1]) { + // try matching end quote + for possible_quote in right.chars() { + for (pos, _) in match_str.match_indices(possible_quote) { + if match_str.len() == pos + 1 + || match_str.chars().nth(pos + 1).unwrap().is_whitespace() + { + return Some(pos); + } + } + } + } + None +} + #[derive(Debug)] pub(super) struct MatchedParam<'a> { pub(super) value: &'a str, pub(super) next_pos: usize, + #[allow(dead_code)] // this'll prolly be useful sometime later pub(super) in_quotes: bool, } -// very very simple quote matching -// quotes need to be at start/end of words, and are ignored if a closing quote is not present -// WTB POSIX quoting: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html pub(super) fn next_param<'a>(input: &'a str, current_pos: usize) -> Option> { if input.len() == current_pos { return None; @@ -63,26 +82,13 @@ pub(super) fn next_param<'a>(input: &'a str, current_pos: usize) -> Option(input: &'a str, current_pos: usize) -> Option { + pub(super) name: &'a str, + pub(super) value: Option<&'a str>, + pub(super) next_pos: usize, +} + +pub(super) fn next_flag<'a>(input: &'a str, mut current_pos: usize) -> Option> { + if input.len() == current_pos { + return None; + } + + let leading_whitespace_count = + input[..current_pos].len() - input[..current_pos].trim_start().len(); + let substr_to_match = &input[current_pos + leading_whitespace_count..]; + + // if the param is quoted, it should not be processed as a flag + if find_quotes(substr_to_match).is_some() { + return None; + } + + println!("flag input {substr_to_match}"); + // strip the - + let Some(substr_to_match) = substr_to_match.strip_prefix('-') else { + // if it doesn't have one, then it is not a flag + return None; + }; + current_pos += 1; + + // try finding = or whitespace + for (pos, char) in substr_to_match.char_indices() { + println!("flag find char {char} at {pos}"); + if char == '=' { + let name = &substr_to_match[..pos]; + println!("flag find {name}"); + // try to get the value + let Some(param) = next_param(input, current_pos + pos + 1) else { + return Some(MatchedFlag { + name, + value: Some(""), + next_pos: current_pos + pos + 1, + }); + }; + return Some(MatchedFlag { + name, + value: Some(param.value), + next_pos: param.next_pos, + }); + } else if char.is_whitespace() { + // no value if whitespace + return Some(MatchedFlag { + name: &substr_to_match[..pos], + value: None, + next_pos: current_pos + pos + 1, + }); + } + } + + // if eof then no value + Some(MatchedFlag { + name: substr_to_match, + value: None, + next_pos: current_pos + substr_to_match.len(), + }) +} diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index bdc30013..594517a7 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -13,6 +13,7 @@ pub enum Token { Empty, /// multi-token matching + /// todo: FullString tokens don't work properly in this (they don't get passed the rest of the input) Any(Vec), /// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`) @@ -39,10 +40,6 @@ pub enum Token { /// reset, clear, default Reset(ParamName), - - // todo: currently not included in command definitions - // todo: flags with values - Flag, } #[derive(Debug)] @@ -52,12 +49,12 @@ pub enum TokenMatchError { } #[derive(Debug)] -pub struct TokenMatchedValue { +pub struct TokenMatchValue { pub raw: SmolStr, pub param: Option<(ParamName, Parameter)>, } -impl TokenMatchedValue { +impl TokenMatchValue { fn new_match(raw: impl Into) -> TryMatchResult { Some(Ok(Some(Self { raw: raw.into(), @@ -85,7 +82,7 @@ impl TokenMatchedValue { // a: because we want to differentiate between no match and match failure (it matched with an error) // "no match" has a different charecteristic because we want to continue matching other tokens... // ...while "match failure" means we should stop matching and return the error -type TryMatchResult = Option, TokenMatchError>>; +type TryMatchResult = Option, TokenMatchError>>; impl Token { pub fn try_match(&self, input: Option<&str>) -> TryMatchResult { @@ -114,8 +111,7 @@ impl Token { })) }), // everything else doesnt match if no input anyway - Token::Value(_) => None, - Token::Flag => None, + Self::Value(_) => None, // don't add a _ match here! }; } @@ -125,30 +121,29 @@ impl Token { // try actually matching stuff match self { Self::Empty => None, - Self::Flag => unreachable!(), // matched upstream (dusk: i don't really like this tbh) Self::Any(tokens) => tokens .iter() - .map(|t| t.try_match(Some(input.into()))) + .map(|t| t.try_match(Some(input))) .find(|r| !matches!(r, None)) .unwrap_or(None), Self::Value(values) => values .iter() .any(|v| v.eq(input)) - .then(|| TokenMatchedValue::new_match(input)) + .then(|| TokenMatchValue::new_match(input)) .unwrap_or(None), - Self::FullString(param_name) => TokenMatchedValue::new_match_param( + Self::FullString(param_name) => TokenMatchValue::new_match_param( input, param_name, Parameter::OpaqueString { raw: input.into() }, ), - Self::SystemRef(param_name) => TokenMatchedValue::new_match_param( + Self::SystemRef(param_name) => TokenMatchValue::new_match_param( input, param_name, Parameter::SystemRef { system: input.into(), }, ), - Self::MemberRef(param_name) => TokenMatchedValue::new_match_param( + Self::MemberRef(param_name) => TokenMatchValue::new_match_param( input, param_name, Parameter::MemberRef { @@ -156,7 +151,7 @@ impl Token { }, ), Self::MemberPrivacyTarget(param_name) => match MemberPrivacyTarget::from_str(input) { - Ok(target) => TokenMatchedValue::new_match_param( + Ok(target) => TokenMatchValue::new_match_param( input, param_name, Parameter::MemberPrivacyTarget { @@ -166,7 +161,7 @@ impl Token { Err(_) => None, }, Self::PrivacyLevel(param_name) => match PrivacyLevel::from_str(input) { - Ok(level) => TokenMatchedValue::new_match_param( + Ok(level) => TokenMatchValue::new_match_param( input, param_name, Parameter::PrivacyLevel { @@ -179,7 +174,7 @@ impl Token { .map(Into::::into) .or_else(|_| Disable::from_str(input).map(Into::::into)) { - Ok(toggle) => TokenMatchedValue::new_match_param( + Ok(toggle) => TokenMatchValue::new_match_param( input, param_name, Parameter::Toggle { toggle }, @@ -187,7 +182,7 @@ impl Token { Err(_) => None, }, Self::Enable(param_name) => match Enable::from_str(input) { - Ok(t) => TokenMatchedValue::new_match_param( + Ok(t) => TokenMatchValue::new_match_param( input, param_name, Parameter::Toggle { toggle: t.into() }, @@ -195,7 +190,7 @@ impl Token { Err(_) => None, }, Self::Disable(param_name) => match Disable::from_str(input) { - Ok(t) => TokenMatchedValue::new_match_param( + Ok(t) => TokenMatchValue::new_match_param( input, param_name, Parameter::Toggle { toggle: t.into() }, @@ -203,7 +198,7 @@ impl Token { Err(_) => None, }, Self::Reset(param_name) => match Reset::from_str(input) { - Ok(_) => TokenMatchedValue::new_match_param(input, param_name, Parameter::Reset), + Ok(_) => TokenMatchValue::new_match_param(input, param_name, Parameter::Reset), Err(_) => None, }, // don't add a _ match here! @@ -219,7 +214,7 @@ impl Display for Token { write!(f, "(")?; for (i, token) in vec.iter().enumerate() { if i != 0 { - write!(f, " | ")?; + write!(f, "|")?; } write!(f, "{}", token)?; } @@ -231,43 +226,31 @@ impl Display for Token { Token::FullString(param_name) => write!(f, "[{}]", param_name), Token::MemberRef(param_name) => write!(f, "<{}>", param_name), Token::SystemRef(param_name) => write!(f, "<{}>", param_name), - Token::MemberPrivacyTarget(param_name) => write!(f, "[{}]", param_name), + Token::MemberPrivacyTarget(param_name) => write!(f, "<{}>", param_name), Token::PrivacyLevel(param_name) => write!(f, "[{}]", param_name), Token::Enable(_) => write!(f, "on"), Token::Disable(_) => write!(f, "off"), Token::Toggle(_) => write!(f, "on/off"), Token::Reset(_) => write!(f, "reset"), - Token::Flag => unreachable!("flag tokens should never be in command definitions"), } } } -/// Convenience trait to convert types into [`Token`]s. -pub trait ToToken { - fn to_token(&self) -> Token; -} - -impl ToToken for Token { - fn to_token(&self) -> Token { - self.clone() +impl From<&str> for Token { + fn from(value: &str) -> Self { + Token::Value(vec![value.to_smolstr()]) } } -impl ToToken for &str { - fn to_token(&self) -> Token { - Token::Value(vec![self.to_smolstr()]) +impl From<[&str; L]> for Token { + fn from(value: [&str; L]) -> Self { + Token::Value(value.into_iter().map(|s| s.to_smolstr()).collect()) } } -impl ToToken for [&str] { - fn to_token(&self) -> Token { - Token::Value(self.into_iter().map(|s| s.to_smolstr()).collect()) - } -} - -impl ToToken for [Token] { - fn to_token(&self) -> Token { - Token::Any(self.into_iter().map(|s| s.clone()).collect()) +impl From<[Token; L]> for Token { + fn from(value: [Token; L]) -> Self { + Token::Any(value.into_iter().map(|s| s.clone()).collect()) } } diff --git a/crates/commands/src/tree.rs b/crates/commands/src/tree.rs index f6810f3c..859ba9ed 100644 --- a/crates/commands/src/tree.rs +++ b/crates/commands/src/tree.rs @@ -21,17 +21,17 @@ impl TreeBranch { // iterate over tokens in command for token in command.tokens.clone() { // recursively get or create a sub-branch for each token - current_branch = current_branch.branches.entry(token).or_insert(TreeBranch { - current_command: None, - branches: OrderMap::new(), - }); + current_branch = current_branch + .branches + .entry(token) + .or_insert_with(TreeBranch::empty); } // when we're out of tokens, add an Empty branch with the callback and no sub-branches current_branch.branches.insert( Token::Empty, TreeBranch { - current_command: Some(command), branches: OrderMap::new(), + current_command: Some(command), }, ); }