mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-05 05:17:54 +00:00
refactor: separate commands into command_parser, command_definitions crates
This commit is contained in:
parent
4f390e2a14
commit
0c012e98b5
33 changed files with 464 additions and 378 deletions
101
crates/command_parser/src/command.rs
Normal file
101
crates/command_parser/src/command.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
use std::fmt::{Debug, Display};
|
||||
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::{flag::Flag, parameter::*, token::Token};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Command {
|
||||
// TODO: fix hygiene
|
||||
pub tokens: Vec<Token>,
|
||||
pub flags: Vec<Flag>,
|
||||
pub help: SmolStr,
|
||||
pub cb: SmolStr,
|
||||
pub show_in_suggestions: bool,
|
||||
pub parse_flags_before: usize,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn new(tokens: impl IntoIterator<Item = Token>, cb: impl Into<SmolStr>) -> Self {
|
||||
let tokens = tokens.into_iter().collect::<Vec<_>>();
|
||||
assert!(tokens.len() > 0);
|
||||
// figure out which token to parse / put flags after
|
||||
// (by default, put flags after the last token)
|
||||
let mut parse_flags_before = tokens.len();
|
||||
let mut was_parameter = true;
|
||||
for (idx, token) in tokens.iter().enumerate().rev() {
|
||||
match token {
|
||||
// we want flags to go before any parameters
|
||||
Token::Parameter(_, _) | Token::Any(_) => {
|
||||
parse_flags_before = idx;
|
||||
was_parameter = true;
|
||||
}
|
||||
Token::Empty | Token::Value(_) => {
|
||||
if was_parameter {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Self {
|
||||
flags: Vec::new(),
|
||||
help: SmolStr::new_static("<no help text>"),
|
||||
cb: cb.into(),
|
||||
show_in_suggestions: true,
|
||||
parse_flags_before,
|
||||
tokens,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn help(mut self, v: impl Into<SmolStr>) -> Self {
|
||||
self.help = v.into();
|
||||
self
|
||||
}
|
||||
|
||||
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: impl Parameter + 'static) -> 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() {
|
||||
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$(,)*) => {
|
||||
$crate::command::Command::new([$($crate::token::Token::from($v)),*], $cb)
|
||||
};
|
||||
}
|
||||
80
crates/command_parser/src/flag.rs
Normal file
80
crates/command_parser/src/flag.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use std::{fmt::Display, sync::Arc};
|
||||
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::parameter::{Parameter, ParameterValue};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FlagValueMatchError {
|
||||
ValueMissing,
|
||||
InvalidValue { raw: SmolStr, msg: SmolStr },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Flag {
|
||||
name: SmolStr,
|
||||
value: Option<Arc<dyn Parameter>>,
|
||||
}
|
||||
|
||||
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.format(f, value.default_name())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FlagMatchError {
|
||||
ValueMatchFailed(FlagValueMatchError),
|
||||
}
|
||||
|
||||
type TryMatchFlagResult = Option<Result<Option<ParameterValue>, FlagMatchError>>;
|
||||
|
||||
impl Flag {
|
||||
pub fn new(name: impl Into<SmolStr>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_value(mut self, param: impl Parameter + 'static) -> Self {
|
||||
self.value = Some(Arc::new(param));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
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.as_deref() 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.match_value(input_value) {
|
||||
Ok(param) => Some(Ok(Some(param))),
|
||||
Err(err) => Some(Err(FlagMatchError::ValueMatchFailed(
|
||||
FlagValueMatchError::InvalidValue {
|
||||
raw: input_value.into(),
|
||||
msg: err,
|
||||
},
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
310
crates/command_parser/src/lib.rs
Normal file
310
crates/command_parser/src/lib.rs
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
#![feature(let_chains)]
|
||||
#![feature(anonymous_lifetime_in_impl_trait)]
|
||||
|
||||
pub mod command;
|
||||
mod flag;
|
||||
pub mod parameter;
|
||||
mod string;
|
||||
pub mod token;
|
||||
pub mod tree;
|
||||
|
||||
use core::panic;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::ops::Not;
|
||||
|
||||
use command::Command;
|
||||
use flag::{Flag, FlagMatchError, FlagValueMatchError};
|
||||
use parameter::ParameterValue;
|
||||
use smol_str::SmolStr;
|
||||
use string::MatchedFlag;
|
||||
use token::{Token, TokenMatchError, TokenMatchValue};
|
||||
|
||||
// todo: this should come from the bot probably
|
||||
const MAX_SUGGESTIONS: usize = 7;
|
||||
|
||||
pub type Tree = tree::TreeBranch;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedCommand {
|
||||
pub command_def: Command,
|
||||
pub parameters: HashMap<String, ParameterValue>,
|
||||
pub flags: HashMap<String, Option<ParameterValue>>,
|
||||
}
|
||||
|
||||
pub fn parse_command(
|
||||
command_tree: Tree,
|
||||
prefix: String,
|
||||
input: String,
|
||||
) -> Result<ParsedCommand, String> {
|
||||
let input: SmolStr = input.into();
|
||||
let mut local_tree: Tree = command_tree.clone();
|
||||
|
||||
// end position of all currently matched tokens
|
||||
let mut current_pos: usize = 0;
|
||||
let mut current_token_idx: usize = 0;
|
||||
|
||||
let mut params: HashMap<String, ParameterValue> = HashMap::new();
|
||||
let mut raw_flags: Vec<(usize, MatchedFlag)> = Vec::new();
|
||||
|
||||
loop {
|
||||
println!(
|
||||
"possible: {:?}",
|
||||
local_tree.possible_tokens().collect::<Vec<_>>()
|
||||
);
|
||||
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;
|
||||
current_token_idx += 1;
|
||||
|
||||
if let Some(arg) = arg.as_ref() {
|
||||
// insert arg as paramater if this is a parameter
|
||||
if let Some((param_name, param)) = arg.param.as_ref() {
|
||||
params.insert(param_name.to_string(), param.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(next_tree) = local_tree.get_branch(&found_token) {
|
||||
local_tree = next_tree.clone();
|
||||
} else {
|
||||
panic!("found token {found_token:?} could not match tree, at {input}");
|
||||
}
|
||||
}
|
||||
Some(Err((token, err))) => {
|
||||
let error_msg = match err {
|
||||
TokenMatchError::MissingParameter { name } => {
|
||||
format!("Expected parameter `{name}` in command `{prefix}{input} {token}`.")
|
||||
}
|
||||
TokenMatchError::MissingAny { tokens } => {
|
||||
let mut msg = format!("Expected one of ");
|
||||
for (idx, token) in tokens.iter().enumerate() {
|
||||
write!(&mut msg, "`{token}`").expect("oom");
|
||||
if idx < tokens.len() - 1 {
|
||||
if tokens.len() > 2 && idx == tokens.len() - 2 {
|
||||
msg.push_str(" or ");
|
||||
} else {
|
||||
msg.push_str(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
write!(&mut msg, " in command `{prefix}{input} {token}`.").expect("oom");
|
||||
msg
|
||||
}
|
||||
TokenMatchError::ParameterMatchError { input: raw, msg } => {
|
||||
format!("Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}.")
|
||||
}
|
||||
};
|
||||
return Err(error_msg);
|
||||
}
|
||||
None => {
|
||||
// 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()
|
||||
{
|
||||
error.push_str(" ");
|
||||
}
|
||||
|
||||
error.push_str(
|
||||
"For a list of all possible commands, see <https://pluralkit.me/commands>.",
|
||||
);
|
||||
|
||||
// todo: check if last token is a common incorrect unquote (multi-member names etc)
|
||||
// todo: check if this is a system name in pk;s command
|
||||
return 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<ParameterValue>> = 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)) => {
|
||||
let error = match err {
|
||||
FlagMatchError::ValueMatchFailed(FlagValueMatchError::ValueMissing) => {
|
||||
format!(
|
||||
"Flag `-{name}` in command `{prefix}{input}` is missing a value, try passing `{flag}`.",
|
||||
name = flag.name()
|
||||
)
|
||||
}
|
||||
FlagMatchError::ValueMatchFailed(
|
||||
FlagValueMatchError::InvalidValue { msg, raw },
|
||||
) => {
|
||||
format!(
|
||||
"Flag `-{name}` in command `{prefix}{input}` has a value (`{raw}`) that could not be parsed: {msg}.",
|
||||
name = flag.name()
|
||||
)
|
||||
}
|
||||
};
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 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 Err(error);
|
||||
}
|
||||
println!("{} {flags:?} {params:?}", command.cb);
|
||||
return Ok(ParsedCommand {
|
||||
command_def: command,
|
||||
flags,
|
||||
parameters: params,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn match_flag<'a>(
|
||||
possible_flags: impl Iterator<Item = &'a Flag>,
|
||||
matched_flag: MatchedFlag<'a>,
|
||||
) -> Option<Result<(SmolStr, Option<ParameterValue>), (&'a Flag, FlagMatchError)>> {
|
||||
// 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:
|
||||
/// - nothing (none matched)
|
||||
/// - matched token, to move deeper into the tree
|
||||
/// - 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<'a>(
|
||||
possible_tokens: impl Iterator<Item = &'a Token>,
|
||||
input: &str,
|
||||
current_pos: usize,
|
||||
) -> Option<Result<(&'a Token, Option<TokenMatchValue>, usize), (&'a Token, TokenMatchError)>> {
|
||||
// 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::Parameter(_, param) if param.remainder());
|
||||
// check if this is a token that matches the rest of the input
|
||||
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));
|
||||
// either use matched param or rest of the input if matching remaining
|
||||
let input_to_match = matched.as_ref().map(|v| {
|
||||
match_remaining
|
||||
.then_some(&input[current_pos..])
|
||||
.unwrap_or(v.value)
|
||||
});
|
||||
match token.try_match(input_to_match) {
|
||||
Some(Ok(value)) => {
|
||||
println!("matched token: {}", token);
|
||||
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)) => {
|
||||
return Some(Err((token, err)));
|
||||
}
|
||||
None => {} // continue matching until we exhaust all tokens
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// todo: should probably move this somewhere else
|
||||
/// returns true if wrote possible commands, false if not
|
||||
fn fmt_possible_commands(
|
||||
f: &mut String,
|
||||
prefix: &str,
|
||||
mut possible_commands: impl Iterator<Item = &Command>,
|
||||
) -> bool {
|
||||
if let Some(first) = possible_commands.next() {
|
||||
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;
|
||||
}
|
||||
writeln!(f, "- **{prefix}{command}** - *{}*", command.help).expect("oom");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
318
crates/command_parser/src/parameter.rs
Normal file
318
crates/command_parser/src/parameter.rs
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
use std::{fmt::Debug, str::FromStr};
|
||||
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::token::ParamName;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ParameterValue {
|
||||
MemberRef(String),
|
||||
SystemRef(String),
|
||||
MemberPrivacyTarget(String),
|
||||
PrivacyLevel(String),
|
||||
OpaqueString(String),
|
||||
Toggle(bool),
|
||||
}
|
||||
|
||||
pub trait Parameter: Debug + Send + Sync {
|
||||
fn remainder(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn default_name(&self) -> ParamName;
|
||||
fn format(&self, f: &mut std::fmt::Formatter, name: &str) -> std::fmt::Result;
|
||||
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct OpaqueString(bool);
|
||||
|
||||
impl OpaqueString {
|
||||
pub const SINGLE: Self = Self(false);
|
||||
pub const REMAINDER: Self = Self(true);
|
||||
}
|
||||
|
||||
impl Parameter for OpaqueString {
|
||||
fn remainder(&self) -> bool {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn default_name(&self) -> ParamName {
|
||||
"string"
|
||||
}
|
||||
|
||||
fn format(&self, f: &mut std::fmt::Formatter, name: &str) -> std::fmt::Result {
|
||||
write!(f, "[{name}]")
|
||||
}
|
||||
|
||||
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||
Ok(ParameterValue::OpaqueString(input.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct MemberRef;
|
||||
|
||||
impl Parameter for MemberRef {
|
||||
fn default_name(&self) -> ParamName {
|
||||
"member"
|
||||
}
|
||||
|
||||
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
|
||||
write!(f, "<target member>")
|
||||
}
|
||||
|
||||
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||
Ok(ParameterValue::MemberRef(input.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct SystemRef;
|
||||
|
||||
impl Parameter for SystemRef {
|
||||
fn default_name(&self) -> ParamName {
|
||||
"system"
|
||||
}
|
||||
|
||||
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
|
||||
write!(f, "<target system>")
|
||||
}
|
||||
|
||||
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||
Ok(ParameterValue::SystemRef(input.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct MemberPrivacyTarget;
|
||||
|
||||
pub enum MemberPrivacyTargetKind {
|
||||
Visibility,
|
||||
Name,
|
||||
Description,
|
||||
Banner,
|
||||
Avatar,
|
||||
Birthday,
|
||||
Pronouns,
|
||||
Proxy,
|
||||
Metadata,
|
||||
}
|
||||
|
||||
impl AsRef<str> for MemberPrivacyTargetKind {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::Visibility => "visibility",
|
||||
Self::Name => "name",
|
||||
Self::Description => "description",
|
||||
Self::Banner => "banner",
|
||||
Self::Avatar => "avatar",
|
||||
Self::Birthday => "birthday",
|
||||
Self::Pronouns => "pronouns",
|
||||
Self::Proxy => "proxy",
|
||||
Self::Metadata => "metadata",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MemberPrivacyTargetKind {
|
||||
// todo: figure out how to represent these errors best
|
||||
type Err = SmolStr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// todo: this doesnt parse all the possible ways
|
||||
match s.to_lowercase().as_str() {
|
||||
"visibility" => Ok(Self::Visibility),
|
||||
"name" => Ok(Self::Name),
|
||||
"description" => Ok(Self::Description),
|
||||
"banner" => Ok(Self::Banner),
|
||||
"avatar" => Ok(Self::Avatar),
|
||||
"birthday" => Ok(Self::Birthday),
|
||||
"pronouns" => Ok(Self::Pronouns),
|
||||
"proxy" => Ok(Self::Proxy),
|
||||
"metadata" => Ok(Self::Metadata),
|
||||
_ => Err("invalid member privacy target".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parameter for MemberPrivacyTarget {
|
||||
fn default_name(&self) -> ParamName {
|
||||
"member_privacy_target"
|
||||
}
|
||||
|
||||
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
|
||||
write!(f, "<privacy target>")
|
||||
}
|
||||
|
||||
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||
MemberPrivacyTargetKind::from_str(input)
|
||||
.map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct PrivacyLevel;
|
||||
|
||||
pub enum PrivacyLevelKind {
|
||||
Public,
|
||||
Private,
|
||||
}
|
||||
|
||||
impl AsRef<str> for PrivacyLevelKind {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::Public => "public",
|
||||
Self::Private => "private",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PrivacyLevelKind {
|
||||
type Err = SmolStr; // todo
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"public" => Ok(PrivacyLevelKind::Public),
|
||||
"private" => Ok(PrivacyLevelKind::Private),
|
||||
_ => Err("invalid privacy level".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parameter for PrivacyLevel {
|
||||
fn default_name(&self) -> ParamName {
|
||||
"privacy_level"
|
||||
}
|
||||
|
||||
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
|
||||
write!(f, "[privacy level]")
|
||||
}
|
||||
|
||||
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||
PrivacyLevelKind::from_str(input)
|
||||
.map(|level| ParameterValue::PrivacyLevel(level.as_ref().into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct Reset;
|
||||
|
||||
impl AsRef<str> for Reset {
|
||||
fn as_ref(&self) -> &str {
|
||||
"reset"
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Reset {
|
||||
type Err = SmolStr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"reset" | "clear" | "default" => Ok(Self),
|
||||
_ => Err("not reset".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parameter for Reset {
|
||||
fn default_name(&self) -> ParamName {
|
||||
"reset"
|
||||
}
|
||||
|
||||
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
|
||||
write!(f, "reset")
|
||||
}
|
||||
|
||||
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||
Self::from_str(input).map(|_| ParameterValue::Toggle(true))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct Toggle;
|
||||
|
||||
impl Parameter for Toggle {
|
||||
fn default_name(&self) -> ParamName {
|
||||
"toggle"
|
||||
}
|
||||
|
||||
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
|
||||
write!(f, "on/off")
|
||||
}
|
||||
|
||||
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||
Enable::from_str(input)
|
||||
.map(Into::<bool>::into)
|
||||
.or_else(|_| Disable::from_str(input).map(Into::<bool>::into))
|
||||
.map(ParameterValue::Toggle)
|
||||
.map_err(|_| "invalid toggle".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct Enable;
|
||||
|
||||
impl FromStr for Enable {
|
||||
type Err = SmolStr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"on" | "yes" | "true" | "enable" | "enabled" => Ok(Self),
|
||||
_ => Err("invalid enable".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parameter for Enable {
|
||||
fn default_name(&self) -> ParamName {
|
||||
"enable"
|
||||
}
|
||||
|
||||
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
|
||||
write!(f, "on")
|
||||
}
|
||||
|
||||
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||
Self::from_str(input).map(|e| ParameterValue::Toggle(e.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<bool> for Enable {
|
||||
fn into(self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct Disable;
|
||||
|
||||
impl FromStr for Disable {
|
||||
type Err = SmolStr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"off" | "no" | "false" | "disable" | "disabled" => Ok(Self),
|
||||
_ => Err("invalid disable".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<bool> for Disable {
|
||||
fn into(self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Parameter for Disable {
|
||||
fn default_name(&self) -> ParamName {
|
||||
"disable"
|
||||
}
|
||||
|
||||
fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result {
|
||||
write!(f, "off")
|
||||
}
|
||||
|
||||
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||
Self::from_str(input).map(|e| ParameterValue::Toggle(e.into()))
|
||||
}
|
||||
}
|
||||
178
crates/command_parser/src/string.rs
Normal file
178
crates/command_parser/src/string.rs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use smol_str::{SmolStr, ToSmolStr};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// Dictionary of (left, right) quote pairs
|
||||
// Each char in the string is an individual quote, multi-char strings imply "one of the following chars"
|
||||
// Certain languages can have quote patterns that have a different character for open and close
|
||||
pub static ref QUOTE_PAIRS: HashMap<SmolStr, SmolStr> = {
|
||||
let mut pairs: HashMap<SmolStr, SmolStr> = HashMap::new();
|
||||
|
||||
let mut insert_pair = |a: &'static str, b: &'static str| {
|
||||
let a = SmolStr::new_static(a);
|
||||
let b = SmolStr::new_static(b);
|
||||
pairs.insert(a.clone(), b.clone());
|
||||
// make it easier to look up right quotes
|
||||
for char in a.chars() {
|
||||
pairs.insert(char.to_smolstr(), b.clone());
|
||||
}
|
||||
};
|
||||
|
||||
// Basic
|
||||
insert_pair( "'", "'" ); // ASCII single quotes
|
||||
insert_pair( "\"", "\"" ); // ASCII double quotes
|
||||
|
||||
// "Smart quotes"
|
||||
// Specifically ignore the left/right status of the quotes and match any combination of them
|
||||
// Left string also includes "low" quotes to allow for the low-high style used in some locales
|
||||
insert_pair( "\u{201C}\u{201D}\u{201F}\u{201E}", "\u{201C}\u{201D}\u{201F}" ); // double quotes
|
||||
insert_pair( "\u{2018}\u{2019}\u{201B}\u{201A}", "\u{2018}\u{2019}\u{201B}" ); // single quotes
|
||||
|
||||
// Chevrons (normal and "fullwidth" variants)
|
||||
insert_pair( "\u{00AB}\u{300A}", "\u{00BB}\u{300B}" ); // double chevrons, pointing away (<<text>>)
|
||||
insert_pair( "\u{00BB}\u{300B}", "\u{00AB}\u{300A}" ); // double chevrons, pointing together (>>text<<)
|
||||
insert_pair( "\u{2039}\u{3008}", "\u{203A}\u{3009}" ); // single chevrons, pointing away (<text>)
|
||||
insert_pair( "\u{203A}\u{3009}", "\u{2039}\u{3008}" ); // single chevrons, pointing together (>text<)
|
||||
|
||||
// Other
|
||||
insert_pair( "\u{300C}\u{300E}", "\u{300D}\u{300F}" ); // corner brackets (Japanese/Chinese)
|
||||
|
||||
pairs
|
||||
};
|
||||
}
|
||||
|
||||
// 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)]
|
||||
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,
|
||||
}
|
||||
|
||||
pub(super) fn next_param<'a>(input: &'a str, current_pos: usize) -> Option<MatchedParam<'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..];
|
||||
println!("stuff: {input} {current_pos} {leading_whitespace_count}");
|
||||
println!("to match: {substr_to_match}");
|
||||
|
||||
if let Some(end_quote_pos) = find_quotes(substr_to_match) {
|
||||
// return quoted string, without quotes
|
||||
return Some(MatchedParam {
|
||||
value: &substr_to_match[1..end_quote_pos - 1],
|
||||
next_pos: current_pos + end_quote_pos + 1,
|
||||
in_quotes: true,
|
||||
});
|
||||
}
|
||||
|
||||
// find next whitespace character
|
||||
for (pos, char) in substr_to_match.char_indices() {
|
||||
if char.is_whitespace() {
|
||||
return Some(MatchedParam {
|
||||
value: &substr_to_match[..pos],
|
||||
next_pos: current_pos + pos + 1,
|
||||
in_quotes: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// if we're here, we went to EOF and didn't match any whitespace
|
||||
// so we return the whole string
|
||||
Some(MatchedParam {
|
||||
value: substr_to_match,
|
||||
next_pos: current_pos + substr_to_match.len(),
|
||||
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(),
|
||||
})
|
||||
}
|
||||
199
crates/command_parser/src/token.rs
Normal file
199
crates/command_parser/src/token.rs
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
hash::Hash,
|
||||
ops::Not,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::parameter::{Parameter, ParameterValue};
|
||||
|
||||
pub type ParamName = &'static str;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Token {
|
||||
/// Token used to represent a finished command (i.e. no more parameters required)
|
||||
// todo: this is likely not the right way to represent this
|
||||
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<Token>),
|
||||
|
||||
/// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`)
|
||||
Value(Vec<SmolStr>),
|
||||
|
||||
/// A parameter that must be provided a value
|
||||
Parameter(ParamName, Arc<dyn Parameter>),
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! any {
|
||||
($($t:expr),+) => {
|
||||
$crate::token::Token::Any(vec![$($crate::token::Token::from($t)),+])
|
||||
};
|
||||
}
|
||||
|
||||
impl PartialEq for Token {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Any(l0), Self::Any(r0)) => l0 == r0,
|
||||
(Self::Value(l0), Self::Value(r0)) => l0 == r0,
|
||||
(Self::Parameter(l0, _), Self::Parameter(r0, _)) => l0 == r0,
|
||||
(Self::Empty, Self::Empty) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Eq for Token {}
|
||||
|
||||
impl Hash for Token {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
core::mem::discriminant(self).hash(state);
|
||||
match self {
|
||||
Token::Empty => {}
|
||||
Token::Any(vec) => vec.hash(state),
|
||||
Token::Value(vec) => vec.hash(state),
|
||||
Token::Parameter(name, _) => name.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TokenMatchError {
|
||||
ParameterMatchError { input: SmolStr, msg: SmolStr },
|
||||
MissingParameter { name: ParamName },
|
||||
MissingAny { tokens: Vec<Token> },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct TokenMatchValue {
|
||||
pub raw: SmolStr,
|
||||
pub param: Option<(ParamName, ParameterValue)>,
|
||||
}
|
||||
|
||||
impl TokenMatchValue {
|
||||
fn new_match(raw: impl Into<SmolStr>) -> TryMatchResult {
|
||||
Some(Ok(Some(Self {
|
||||
raw: raw.into(),
|
||||
param: None,
|
||||
})))
|
||||
}
|
||||
|
||||
fn new_match_param(
|
||||
raw: impl Into<SmolStr>,
|
||||
param_name: ParamName,
|
||||
param: ParameterValue,
|
||||
) -> TryMatchResult {
|
||||
Some(Ok(Some(Self {
|
||||
raw: raw.into(),
|
||||
param: Some((param_name, param)),
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
/// None -> no match
|
||||
/// Some(Ok(None)) -> match, no value
|
||||
/// Some(Ok(Some(_))) -> match, with value
|
||||
/// Some(Err(_)) -> error while matching
|
||||
// q: why do this while we could have a NoMatch in TokenMatchError?
|
||||
// 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<Result<Option<TokenMatchValue>, TokenMatchError>>;
|
||||
|
||||
impl Token {
|
||||
pub(super) fn try_match(&self, input: Option<&str>) -> TryMatchResult {
|
||||
let input = match input {
|
||||
Some(input) => input,
|
||||
None => {
|
||||
// short circuit on:
|
||||
return match self {
|
||||
// empty token
|
||||
Self::Empty => Some(Ok(None)),
|
||||
// missing paramaters
|
||||
Self::Parameter(name, _) => {
|
||||
Some(Err(TokenMatchError::MissingParameter { name }))
|
||||
}
|
||||
Self::Any(tokens) => tokens.is_empty().then_some(None).unwrap_or_else(|| {
|
||||
Some(Err(TokenMatchError::MissingAny {
|
||||
tokens: tokens.clone(),
|
||||
}))
|
||||
}),
|
||||
// everything else doesnt match if no input anyway
|
||||
Self::Value(_) => None,
|
||||
// don't add a _ match here!
|
||||
};
|
||||
}
|
||||
};
|
||||
let input = input.trim();
|
||||
|
||||
// try actually matching stuff
|
||||
match self {
|
||||
Self::Empty => None,
|
||||
Self::Any(tokens) => tokens
|
||||
.iter()
|
||||
.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(|| TokenMatchValue::new_match(input))
|
||||
.unwrap_or(None),
|
||||
Self::Parameter(name, param) => match param.match_value(input) {
|
||||
Ok(matched) => TokenMatchValue::new_match_param(input, name, matched),
|
||||
Err(err) => Some(Err(TokenMatchError::ParameterMatchError {
|
||||
input: input.into(),
|
||||
msg: err,
|
||||
})),
|
||||
}, // don't add a _ match here!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Token {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Token::Empty => write!(f, ""),
|
||||
Token::Any(vec) => {
|
||||
write!(f, "(")?;
|
||||
for (i, token) in vec.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(f, "|")?;
|
||||
}
|
||||
write!(f, "{}", token)?;
|
||||
}
|
||||
write!(f, ")")
|
||||
}
|
||||
Token::Value(vec) if vec.is_empty().not() => write!(f, "{}", vec.first().unwrap()),
|
||||
Token::Value(_) => Ok(()), // if value token has no values (lol), don't print anything
|
||||
Token::Parameter(name, param) => param.format(f, name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Token {
|
||||
fn from(value: &str) -> Self {
|
||||
Token::Value(vec![SmolStr::new(value)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<const L: usize> From<[&str; L]> for Token {
|
||||
fn from(value: [&str; L]) -> Self {
|
||||
Token::Value(value.into_iter().map(SmolStr::from).collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Parameter + 'static> From<P> for Token {
|
||||
fn from(value: P) -> Self {
|
||||
Token::Parameter(value.default_name(), Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Parameter + 'static> From<(ParamName, P)> for Token {
|
||||
fn from(value: (ParamName, P)) -> Self {
|
||||
Token::Parameter(value.0, Arc::new(value.1))
|
||||
}
|
||||
}
|
||||
75
crates/command_parser/src/tree.rs
Normal file
75
crates/command_parser/src/tree.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
use ordermap::OrderMap;
|
||||
|
||||
use crate::{command::Command, token::Token};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TreeBranch {
|
||||
current_command: Option<Command>,
|
||||
branches: OrderMap<Token, TreeBranch>,
|
||||
}
|
||||
|
||||
impl Default for TreeBranch {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
current_command: None,
|
||||
branches: OrderMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TreeBranch {
|
||||
pub fn register_command(&mut self, command: Command) {
|
||||
let mut current_branch = self;
|
||||
// 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_with(TreeBranch::default);
|
||||
}
|
||||
// when we're out of tokens, add an Empty branch with the callback and no sub-branches
|
||||
current_branch.branches.insert(
|
||||
Token::Empty,
|
||||
TreeBranch {
|
||||
branches: OrderMap::new(),
|
||||
current_command: Some(command),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn command(&self) -> Option<Command> {
|
||||
self.current_command.clone()
|
||||
}
|
||||
|
||||
pub(super) fn possible_tokens(&self) -> impl Iterator<Item = &Token> {
|
||||
self.branches.keys()
|
||||
}
|
||||
|
||||
pub(super) fn possible_commands(&self, max_depth: usize) -> impl Iterator<Item = &Command> {
|
||||
// dusk: i am too lazy to write an iterator for this without using recursion so we box everything
|
||||
fn box_iter<'a>(
|
||||
iter: impl Iterator<Item = &'a Command> + 'a,
|
||||
) -> Box<dyn Iterator<Item = &'a Command> + 'a> {
|
||||
Box::new(iter)
|
||||
}
|
||||
|
||||
if max_depth == 0 {
|
||||
return box_iter(std::iter::empty());
|
||||
}
|
||||
let mut commands = box_iter(std::iter::empty());
|
||||
for branch in self.branches.values() {
|
||||
if let Some(command) = branch.current_command.as_ref() {
|
||||
commands = box_iter(commands.chain(std::iter::once(command)));
|
||||
// we dont need to look further if we found a command (only Empty tokens have commands)
|
||||
continue;
|
||||
}
|
||||
commands = box_iter(commands.chain(branch.possible_commands(max_depth - 1)));
|
||||
}
|
||||
commands
|
||||
}
|
||||
|
||||
pub(super) fn get_branch(&self, token: &Token) -> Option<&TreeBranch> {
|
||||
self.branches.get(token)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue