mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-15 18:20:11 +00:00
feat(commands): add typed flags, misplaced and non-applicable flags error reporting
This commit is contained in:
parent
816aa68b33
commit
300539fdda
8 changed files with 454 additions and 139 deletions
|
|
@ -24,16 +24,19 @@ use smol_str::SmolStr;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
command,
|
command,
|
||||||
token::{ToToken, Token},
|
flag::{Flag, FlagValue},
|
||||||
|
token::Token,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Command {
|
pub struct Command {
|
||||||
// TODO: fix hygiene
|
// TODO: fix hygiene
|
||||||
pub tokens: Vec<Token>,
|
pub tokens: Vec<Token>,
|
||||||
|
pub flags: Vec<Flag>,
|
||||||
pub help: SmolStr,
|
pub help: SmolStr,
|
||||||
pub cb: SmolStr,
|
pub cb: SmolStr,
|
||||||
pub show_in_suggestions: bool,
|
pub show_in_suggestions: bool,
|
||||||
|
pub parse_flags_before: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
|
|
@ -41,36 +44,88 @@ impl Command {
|
||||||
tokens: impl IntoIterator<Item = Token>,
|
tokens: impl IntoIterator<Item = Token>,
|
||||||
help: impl Into<SmolStr>,
|
help: impl Into<SmolStr>,
|
||||||
cb: impl Into<SmolStr>,
|
cb: impl Into<SmolStr>,
|
||||||
show_in_suggestions: bool,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let tokens = tokens.into_iter().collect::<Vec<_>>();
|
||||||
|
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 {
|
Self {
|
||||||
tokens: tokens.into_iter().collect(),
|
flags: Vec::new(),
|
||||||
help: help.into(),
|
help: help.into(),
|
||||||
cb: cb.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<SmolStr>) -> Self {
|
||||||
|
self.flags.push(Flag::new(name));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value_flag(mut self, name: impl Into<SmolStr>, value: FlagValue) -> Self {
|
||||||
|
self.flags.push(Flag::new(name).with_value(value));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Command {
|
impl Display for Command {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
for (idx, token) in self.tokens.iter().enumerate() {
|
for (idx, token) in self.tokens.iter().enumerate() {
|
||||||
write!(f, "{}", token)?;
|
if idx == self.parse_flags_before {
|
||||||
if idx < self.tokens.len() - 1 {
|
for flag in &self.flags {
|
||||||
write!(f, " ")?;
|
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(())
|
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_export]
|
||||||
macro_rules! command {
|
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) => {
|
([$($v:expr),+], $cb:expr, $help:expr) => {
|
||||||
$crate::command!([$($v),+], $cb, $help, suggest = true)
|
$crate::commands::Command::new([$(Token::from($v)),*], $help, $cb)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,5 @@ interface Parameter {
|
||||||
dictionary ParsedCommand {
|
dictionary ParsedCommand {
|
||||||
string command_ref;
|
string command_ref;
|
||||||
record<string, Parameter> params;
|
record<string, Parameter> params;
|
||||||
record<string, string?> flags;
|
record<string, Parameter?> flags;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ pub fn cmds() -> impl Iterator<Item = Command> {
|
||||||
[member, MemberRef("target")],
|
[member, MemberRef("target")],
|
||||||
"member_show",
|
"member_show",
|
||||||
"Shows information about a member"
|
"Shows information about a member"
|
||||||
),
|
)
|
||||||
|
.value_flag("pt", FlagValue::OpaqueString),
|
||||||
command!(
|
command!(
|
||||||
[member, MemberRef("target"), description],
|
[member, MemberRef("target"), description],
|
||||||
"member_desc_show",
|
"member_desc_show",
|
||||||
|
|
@ -53,9 +54,9 @@ pub fn cmds() -> impl Iterator<Item = Command> {
|
||||||
command!(
|
command!(
|
||||||
[member, MemberRef("target"), "soulscream"],
|
[member, MemberRef("target"), "soulscream"],
|
||||||
"member_soulscream",
|
"member_soulscream",
|
||||||
"todo",
|
"todo"
|
||||||
suggest = false
|
)
|
||||||
),
|
.show_in_suggestions(false),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
102
crates/commands/src/flag.rs
Normal file
102
crates/commands/src/flag.rs
Normal file
|
|
@ -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<Parameter, FlagValueMatchError> {
|
||||||
|
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<FlagValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Result<Option<Parameter>, FlagMatchError>>;
|
||||||
|
|
||||||
|
impl Flag {
|
||||||
|
pub fn new(name: impl Into<SmolStr>) -> 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))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#![feature(anonymous_lifetime_in_impl_trait)]
|
#![feature(anonymous_lifetime_in_impl_trait)]
|
||||||
|
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
mod flag;
|
||||||
mod string;
|
mod string;
|
||||||
mod token;
|
mod token;
|
||||||
mod tree;
|
mod tree;
|
||||||
|
|
@ -13,7 +14,9 @@ use std::collections::HashMap;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::ops::Not;
|
use std::ops::Not;
|
||||||
|
|
||||||
|
use flag::{Flag, FlagMatchError, FlagValueMatchError};
|
||||||
use smol_str::SmolStr;
|
use smol_str::SmolStr;
|
||||||
|
use string::MatchedFlag;
|
||||||
use tree::TreeBranch;
|
use tree::TreeBranch;
|
||||||
|
|
||||||
pub use commands::Command;
|
pub use commands::Command;
|
||||||
|
|
@ -53,7 +56,7 @@ pub enum Parameter {
|
||||||
pub struct ParsedCommand {
|
pub struct ParsedCommand {
|
||||||
pub command_ref: String,
|
pub command_ref: String,
|
||||||
pub params: HashMap<String, Parameter>,
|
pub params: HashMap<String, Parameter>,
|
||||||
pub flags: HashMap<String, Option<String>>,
|
pub flags: HashMap<String, Option<Parameter>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_command(prefix: String, input: String) -> CommandResult {
|
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();
|
let mut local_tree: TreeBranch = COMMAND_TREE.clone();
|
||||||
|
|
||||||
// end position of all currently matched tokens
|
// 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<String, Parameter> = HashMap::new();
|
let mut params: HashMap<String, Parameter> = HashMap::new();
|
||||||
let mut flags: HashMap<String, Option<String>> = HashMap::new();
|
let mut raw_flags: Vec<(usize, MatchedFlag)> = Vec::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let possible_tokens = local_tree.possible_tokens().cloned().collect::<Vec<_>>();
|
println!(
|
||||||
println!("possible: {:?}", possible_tokens);
|
"possible: {:?}",
|
||||||
let next = next_token(possible_tokens.clone(), &input, current_pos);
|
local_tree.possible_tokens().collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
let next = next_token(local_tree.possible_tokens(), &input, current_pos);
|
||||||
println!("next: {:?}", next);
|
println!("next: {:?}", next);
|
||||||
match next {
|
match next {
|
||||||
Some(Ok((found_token, arg, new_pos))) => {
|
Some(Ok((found_token, arg, new_pos))) => {
|
||||||
current_pos = new_pos;
|
current_pos = new_pos;
|
||||||
if let Token::Flag = found_token {
|
current_token_idx += 1;
|
||||||
flags.insert(arg.unwrap().raw.into(), None);
|
|
||||||
// don't try matching flags as tree elements
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(arg) = arg.as_ref() {
|
if let Some(arg) = arg.as_ref() {
|
||||||
// insert arg as paramater if this is a parameter
|
// 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 };
|
return CommandResult::Err { error: error_msg };
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if let Some(command) = local_tree.command() {
|
// if it said command not found on a flag, output better error message
|
||||||
println!("{} {params:?}", command.cb);
|
|
||||||
return CommandResult::Ok {
|
|
||||||
command: ParsedCommand {
|
|
||||||
command_ref: command.cb.into(),
|
|
||||||
params,
|
|
||||||
flags,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut error = format!("Unknown command `{prefix}{input}`.");
|
let mut error = format!("Unknown command `{prefix}{input}`.");
|
||||||
|
|
||||||
if fmt_possible_commands(&mut error, &prefix, local_tree.possible_commands(2)).not()
|
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 };
|
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<String, Option<Parameter>> = HashMap::new();
|
||||||
|
let mut misplaced_flags: Vec<MatchedFlag> = Vec::new();
|
||||||
|
let mut invalid_flags: Vec<MatchedFlag> = 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<Item = &'a Flag>,
|
||||||
|
matched_flag: MatchedFlag<'a>,
|
||||||
|
) -> Option<Result<(SmolStr, Option<Parameter>), (&'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
|
/// Find the next token from an either raw or partially parsed command string
|
||||||
///
|
///
|
||||||
/// Returns:
|
/// 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)
|
/// - matched value (if this command matched an user-provided value such as a member name)
|
||||||
/// - end position of matched token
|
/// - end position of matched token
|
||||||
/// - error when matching
|
/// - error when matching
|
||||||
fn next_token(
|
fn next_token<'a>(
|
||||||
possible_tokens: Vec<Token>,
|
possible_tokens: impl Iterator<Item = &'a Token>,
|
||||||
input: &str,
|
input: &str,
|
||||||
current_pos: usize,
|
current_pos: usize,
|
||||||
) -> Option<Result<(Token, Option<TokenMatchedValue>, usize), (Token, TokenMatchError)>> {
|
) -> Option<Result<(&'a Token, Option<TokenMatchValue>, usize), (&'a Token, TokenMatchError)>> {
|
||||||
// get next parameter, matching quotes
|
// skip if 0 length
|
||||||
let matched = crate::string::next_param(&input, current_pos);
|
if let (_, Some(len)) = possible_tokens.size_hint()
|
||||||
println!("matched: {matched:?}\n---");
|
&& len == 0
|
||||||
|
|
||||||
// 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('-')
|
|
||||||
{
|
{
|
||||||
return Some(Ok((
|
return None;
|
||||||
Token::Flag,
|
|
||||||
Some(TokenMatchedValue {
|
|
||||||
raw: param.value.into(),
|
|
||||||
param: None,
|
|
||||||
}),
|
|
||||||
param.next_pos,
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get next parameter, matching quotes
|
||||||
|
let matched = string::next_param(&input, current_pos);
|
||||||
|
println!("matched: {matched:?}\n---");
|
||||||
|
|
||||||
// iterate over tokens and run try_match
|
// iterate over tokens and run try_match
|
||||||
for token in possible_tokens {
|
for token in possible_tokens {
|
||||||
let is_match_remaining_token = |token: &Token| matches!(token, Token::FullString(_));
|
let is_match_remaining_token = |token: &Token| matches!(token, Token::FullString(_));
|
||||||
// check if this is a token that matches the rest of the input
|
// 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
|
// 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
|
// 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));
|
|| 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) {
|
match token.try_match(input_to_match) {
|
||||||
Some(Ok(value)) => {
|
Some(Ok(value)) => {
|
||||||
// return last possible pos if we matched remaining,
|
let next_pos = match matched {
|
||||||
// otherwise use matched param next pos,
|
// return last possible pos if we matched remaining,
|
||||||
// and if didnt match anything we stay where we are
|
Some(_) if match_remaining => input.len(),
|
||||||
let next_pos = matched
|
// otherwise use matched param next pos,
|
||||||
.map(|v| match_remaining.then_some(input.len()).unwrap_or(v.next_pos))
|
Some(param) => param.next_pos,
|
||||||
.unwrap_or(current_pos);
|
// and if didnt match anything we stay where we are
|
||||||
|
None => current_pos,
|
||||||
|
};
|
||||||
return Some(Ok((token, value, next_pos)));
|
return Some(Ok((token, value, next_pos)));
|
||||||
}
|
}
|
||||||
Some(Err(err)) => {
|
Some(Err(err)) => {
|
||||||
|
|
@ -223,7 +325,7 @@ fn fmt_possible_commands(
|
||||||
mut possible_commands: impl Iterator<Item = &Command>,
|
mut possible_commands: impl Iterator<Item = &Command>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if let Some(first) = possible_commands.next() {
|
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)) {
|
for command in std::iter::once(first).chain(possible_commands.take(MAX_SUGGESTIONS - 1)) {
|
||||||
if !command.show_in_suggestions {
|
if !command.show_in_suggestions {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -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<usize> {
|
||||||
|
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)]
|
#[derive(Debug)]
|
||||||
pub(super) struct MatchedParam<'a> {
|
pub(super) struct MatchedParam<'a> {
|
||||||
pub(super) value: &'a str,
|
pub(super) value: &'a str,
|
||||||
pub(super) next_pos: usize,
|
pub(super) next_pos: usize,
|
||||||
|
#[allow(dead_code)] // this'll prolly be useful sometime later
|
||||||
pub(super) in_quotes: bool,
|
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<MatchedParam<'a>> {
|
pub(super) fn next_param<'a>(input: &'a str, current_pos: usize) -> Option<MatchedParam<'a>> {
|
||||||
if input.len() == current_pos {
|
if input.len() == current_pos {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -63,26 +82,13 @@ pub(super) fn next_param<'a>(input: &'a str, current_pos: usize) -> Option<Match
|
||||||
println!("stuff: {input} {current_pos} {leading_whitespace_count}");
|
println!("stuff: {input} {current_pos} {leading_whitespace_count}");
|
||||||
println!("to match: {substr_to_match}");
|
println!("to match: {substr_to_match}");
|
||||||
|
|
||||||
// try matching end quote
|
if let Some(end_quote_pos) = find_quotes(substr_to_match) {
|
||||||
if let Some(right) = QUOTE_PAIRS.get(&substr_to_match[0..1]) {
|
// return quoted string, without quotes
|
||||||
for possible_quote in right.chars() {
|
return Some(MatchedParam {
|
||||||
for (pos, _) in substr_to_match.match_indices(possible_quote) {
|
value: &substr_to_match[1..end_quote_pos - 1],
|
||||||
if substr_to_match.len() == pos + 1
|
next_pos: current_pos + end_quote_pos + 1,
|
||||||
|| substr_to_match
|
in_quotes: true,
|
||||||
.chars()
|
});
|
||||||
.nth(pos + 1)
|
|
||||||
.unwrap()
|
|
||||||
.is_whitespace()
|
|
||||||
{
|
|
||||||
// return quoted string, without quotes
|
|
||||||
return Some(MatchedParam {
|
|
||||||
value: &substr_to_match[1..pos - 1],
|
|
||||||
next_pos: current_pos + pos + 1,
|
|
||||||
in_quotes: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// find next whitespace character
|
// find next whitespace character
|
||||||
|
|
@ -104,3 +110,69 @@ pub(super) fn next_param<'a>(input: &'a str, current_pos: usize) -> Option<Match
|
||||||
in_quotes: false,
|
in_quotes: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(super) struct MatchedFlag<'a> {
|
||||||
|
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<MatchedFlag<'a>> {
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ pub enum Token {
|
||||||
Empty,
|
Empty,
|
||||||
|
|
||||||
/// multi-token matching
|
/// multi-token matching
|
||||||
|
/// todo: FullString tokens don't work properly in this (they don't get passed the rest of the input)
|
||||||
Any(Vec<Token>),
|
Any(Vec<Token>),
|
||||||
|
|
||||||
/// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`)
|
/// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`)
|
||||||
|
|
@ -39,10 +40,6 @@ pub enum Token {
|
||||||
|
|
||||||
/// reset, clear, default
|
/// reset, clear, default
|
||||||
Reset(ParamName),
|
Reset(ParamName),
|
||||||
|
|
||||||
// todo: currently not included in command definitions
|
|
||||||
// todo: flags with values
|
|
||||||
Flag,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -52,12 +49,12 @@ pub enum TokenMatchError {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TokenMatchedValue {
|
pub struct TokenMatchValue {
|
||||||
pub raw: SmolStr,
|
pub raw: SmolStr,
|
||||||
pub param: Option<(ParamName, Parameter)>,
|
pub param: Option<(ParamName, Parameter)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TokenMatchedValue {
|
impl TokenMatchValue {
|
||||||
fn new_match(raw: impl Into<SmolStr>) -> TryMatchResult {
|
fn new_match(raw: impl Into<SmolStr>) -> TryMatchResult {
|
||||||
Some(Ok(Some(Self {
|
Some(Ok(Some(Self {
|
||||||
raw: raw.into(),
|
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)
|
// 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...
|
// "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
|
// ...while "match failure" means we should stop matching and return the error
|
||||||
type TryMatchResult = Option<Result<Option<TokenMatchedValue>, TokenMatchError>>;
|
type TryMatchResult = Option<Result<Option<TokenMatchValue>, TokenMatchError>>;
|
||||||
|
|
||||||
impl Token {
|
impl Token {
|
||||||
pub fn try_match(&self, input: Option<&str>) -> TryMatchResult {
|
pub fn try_match(&self, input: Option<&str>) -> TryMatchResult {
|
||||||
|
|
@ -114,8 +111,7 @@ impl Token {
|
||||||
}))
|
}))
|
||||||
}),
|
}),
|
||||||
// everything else doesnt match if no input anyway
|
// everything else doesnt match if no input anyway
|
||||||
Token::Value(_) => None,
|
Self::Value(_) => None,
|
||||||
Token::Flag => None,
|
|
||||||
// don't add a _ match here!
|
// don't add a _ match here!
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -125,30 +121,29 @@ impl Token {
|
||||||
// try actually matching stuff
|
// try actually matching stuff
|
||||||
match self {
|
match self {
|
||||||
Self::Empty => None,
|
Self::Empty => None,
|
||||||
Self::Flag => unreachable!(), // matched upstream (dusk: i don't really like this tbh)
|
|
||||||
Self::Any(tokens) => tokens
|
Self::Any(tokens) => tokens
|
||||||
.iter()
|
.iter()
|
||||||
.map(|t| t.try_match(Some(input.into())))
|
.map(|t| t.try_match(Some(input)))
|
||||||
.find(|r| !matches!(r, None))
|
.find(|r| !matches!(r, None))
|
||||||
.unwrap_or(None),
|
.unwrap_or(None),
|
||||||
Self::Value(values) => values
|
Self::Value(values) => values
|
||||||
.iter()
|
.iter()
|
||||||
.any(|v| v.eq(input))
|
.any(|v| v.eq(input))
|
||||||
.then(|| TokenMatchedValue::new_match(input))
|
.then(|| TokenMatchValue::new_match(input))
|
||||||
.unwrap_or(None),
|
.unwrap_or(None),
|
||||||
Self::FullString(param_name) => TokenMatchedValue::new_match_param(
|
Self::FullString(param_name) => TokenMatchValue::new_match_param(
|
||||||
input,
|
input,
|
||||||
param_name,
|
param_name,
|
||||||
Parameter::OpaqueString { raw: input.into() },
|
Parameter::OpaqueString { raw: input.into() },
|
||||||
),
|
),
|
||||||
Self::SystemRef(param_name) => TokenMatchedValue::new_match_param(
|
Self::SystemRef(param_name) => TokenMatchValue::new_match_param(
|
||||||
input,
|
input,
|
||||||
param_name,
|
param_name,
|
||||||
Parameter::SystemRef {
|
Parameter::SystemRef {
|
||||||
system: input.into(),
|
system: input.into(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Self::MemberRef(param_name) => TokenMatchedValue::new_match_param(
|
Self::MemberRef(param_name) => TokenMatchValue::new_match_param(
|
||||||
input,
|
input,
|
||||||
param_name,
|
param_name,
|
||||||
Parameter::MemberRef {
|
Parameter::MemberRef {
|
||||||
|
|
@ -156,7 +151,7 @@ impl Token {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Self::MemberPrivacyTarget(param_name) => match MemberPrivacyTarget::from_str(input) {
|
Self::MemberPrivacyTarget(param_name) => match MemberPrivacyTarget::from_str(input) {
|
||||||
Ok(target) => TokenMatchedValue::new_match_param(
|
Ok(target) => TokenMatchValue::new_match_param(
|
||||||
input,
|
input,
|
||||||
param_name,
|
param_name,
|
||||||
Parameter::MemberPrivacyTarget {
|
Parameter::MemberPrivacyTarget {
|
||||||
|
|
@ -166,7 +161,7 @@ impl Token {
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
},
|
},
|
||||||
Self::PrivacyLevel(param_name) => match PrivacyLevel::from_str(input) {
|
Self::PrivacyLevel(param_name) => match PrivacyLevel::from_str(input) {
|
||||||
Ok(level) => TokenMatchedValue::new_match_param(
|
Ok(level) => TokenMatchValue::new_match_param(
|
||||||
input,
|
input,
|
||||||
param_name,
|
param_name,
|
||||||
Parameter::PrivacyLevel {
|
Parameter::PrivacyLevel {
|
||||||
|
|
@ -179,7 +174,7 @@ impl Token {
|
||||||
.map(Into::<bool>::into)
|
.map(Into::<bool>::into)
|
||||||
.or_else(|_| Disable::from_str(input).map(Into::<bool>::into))
|
.or_else(|_| Disable::from_str(input).map(Into::<bool>::into))
|
||||||
{
|
{
|
||||||
Ok(toggle) => TokenMatchedValue::new_match_param(
|
Ok(toggle) => TokenMatchValue::new_match_param(
|
||||||
input,
|
input,
|
||||||
param_name,
|
param_name,
|
||||||
Parameter::Toggle { toggle },
|
Parameter::Toggle { toggle },
|
||||||
|
|
@ -187,7 +182,7 @@ impl Token {
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
},
|
},
|
||||||
Self::Enable(param_name) => match Enable::from_str(input) {
|
Self::Enable(param_name) => match Enable::from_str(input) {
|
||||||
Ok(t) => TokenMatchedValue::new_match_param(
|
Ok(t) => TokenMatchValue::new_match_param(
|
||||||
input,
|
input,
|
||||||
param_name,
|
param_name,
|
||||||
Parameter::Toggle { toggle: t.into() },
|
Parameter::Toggle { toggle: t.into() },
|
||||||
|
|
@ -195,7 +190,7 @@ impl Token {
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
},
|
},
|
||||||
Self::Disable(param_name) => match Disable::from_str(input) {
|
Self::Disable(param_name) => match Disable::from_str(input) {
|
||||||
Ok(t) => TokenMatchedValue::new_match_param(
|
Ok(t) => TokenMatchValue::new_match_param(
|
||||||
input,
|
input,
|
||||||
param_name,
|
param_name,
|
||||||
Parameter::Toggle { toggle: t.into() },
|
Parameter::Toggle { toggle: t.into() },
|
||||||
|
|
@ -203,7 +198,7 @@ impl Token {
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
},
|
},
|
||||||
Self::Reset(param_name) => match Reset::from_str(input) {
|
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,
|
Err(_) => None,
|
||||||
},
|
},
|
||||||
// don't add a _ match here!
|
// don't add a _ match here!
|
||||||
|
|
@ -219,7 +214,7 @@ impl Display for Token {
|
||||||
write!(f, "(")?;
|
write!(f, "(")?;
|
||||||
for (i, token) in vec.iter().enumerate() {
|
for (i, token) in vec.iter().enumerate() {
|
||||||
if i != 0 {
|
if i != 0 {
|
||||||
write!(f, " | ")?;
|
write!(f, "|")?;
|
||||||
}
|
}
|
||||||
write!(f, "{}", token)?;
|
write!(f, "{}", token)?;
|
||||||
}
|
}
|
||||||
|
|
@ -231,43 +226,31 @@ impl Display for Token {
|
||||||
Token::FullString(param_name) => write!(f, "[{}]", param_name),
|
Token::FullString(param_name) => write!(f, "[{}]", param_name),
|
||||||
Token::MemberRef(param_name) => write!(f, "<{}>", param_name),
|
Token::MemberRef(param_name) => write!(f, "<{}>", param_name),
|
||||||
Token::SystemRef(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::PrivacyLevel(param_name) => write!(f, "[{}]", param_name),
|
||||||
Token::Enable(_) => write!(f, "on"),
|
Token::Enable(_) => write!(f, "on"),
|
||||||
Token::Disable(_) => write!(f, "off"),
|
Token::Disable(_) => write!(f, "off"),
|
||||||
Token::Toggle(_) => write!(f, "on/off"),
|
Token::Toggle(_) => write!(f, "on/off"),
|
||||||
Token::Reset(_) => write!(f, "reset"),
|
Token::Reset(_) => write!(f, "reset"),
|
||||||
Token::Flag => unreachable!("flag tokens should never be in command definitions"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience trait to convert types into [`Token`]s.
|
impl From<&str> for Token {
|
||||||
pub trait ToToken {
|
fn from(value: &str) -> Self {
|
||||||
fn to_token(&self) -> Token;
|
Token::Value(vec![value.to_smolstr()])
|
||||||
}
|
|
||||||
|
|
||||||
impl ToToken for Token {
|
|
||||||
fn to_token(&self) -> Token {
|
|
||||||
self.clone()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToToken for &str {
|
impl<const L: usize> From<[&str; L]> for Token {
|
||||||
fn to_token(&self) -> Token {
|
fn from(value: [&str; L]) -> Self {
|
||||||
Token::Value(vec![self.to_smolstr()])
|
Token::Value(value.into_iter().map(|s| s.to_smolstr()).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToToken for [&str] {
|
impl<const L: usize> From<[Token; L]> for Token {
|
||||||
fn to_token(&self) -> Token {
|
fn from(value: [Token; L]) -> Self {
|
||||||
Token::Value(self.into_iter().map(|s| s.to_smolstr()).collect())
|
Token::Any(value.into_iter().map(|s| s.clone()).collect())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToToken for [Token] {
|
|
||||||
fn to_token(&self) -> Token {
|
|
||||||
Token::Any(self.into_iter().map(|s| s.clone()).collect())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,17 @@ impl TreeBranch {
|
||||||
// iterate over tokens in command
|
// iterate over tokens in command
|
||||||
for token in command.tokens.clone() {
|
for token in command.tokens.clone() {
|
||||||
// recursively get or create a sub-branch for each token
|
// recursively get or create a sub-branch for each token
|
||||||
current_branch = current_branch.branches.entry(token).or_insert(TreeBranch {
|
current_branch = current_branch
|
||||||
current_command: None,
|
.branches
|
||||||
branches: OrderMap::new(),
|
.entry(token)
|
||||||
});
|
.or_insert_with(TreeBranch::empty);
|
||||||
}
|
}
|
||||||
// when we're out of tokens, add an Empty branch with the callback and no sub-branches
|
// when we're out of tokens, add an Empty branch with the callback and no sub-branches
|
||||||
current_branch.branches.insert(
|
current_branch.branches.insert(
|
||||||
Token::Empty,
|
Token::Empty,
|
||||||
TreeBranch {
|
TreeBranch {
|
||||||
current_command: Some(command),
|
|
||||||
branches: OrderMap::new(),
|
branches: OrderMap::new(),
|
||||||
|
current_command: Some(command),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue