diff --git a/Cargo.lock b/Cargo.lock index 50dcba38..241b57f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -565,12 +565,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] -name = "commands" +name = "command_definitions" +version = "0.1.0" +dependencies = [ + "command_parser", +] + +[[package]] +name = "command_parser" version = "0.1.0" dependencies = [ "lazy_static", "ordermap", "smol_str", +] + +[[package]] +name = "commands" +version = "0.1.0" +dependencies = [ + "command_definitions", + "command_parser", + "lazy_static", "uniffi", ] diff --git a/crates/command_definitions/Cargo.toml b/crates/command_definitions/Cargo.toml new file mode 100644 index 00000000..e17e23a9 --- /dev/null +++ b/crates/command_definitions/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "command_definitions" +version = "0.1.0" +edition = "2021" + +[dependencies] +command_parser = { path = "../command_parser"} \ No newline at end of file diff --git a/crates/commands/src/commands/admin.rs b/crates/command_definitions/src/admin.rs similarity index 100% rename from crates/commands/src/commands/admin.rs rename to crates/command_definitions/src/admin.rs diff --git a/crates/commands/src/commands/api.rs b/crates/command_definitions/src/api.rs similarity index 100% rename from crates/commands/src/commands/api.rs rename to crates/command_definitions/src/api.rs diff --git a/crates/commands/src/commands/autoproxy.rs b/crates/command_definitions/src/autoproxy.rs similarity index 100% rename from crates/commands/src/commands/autoproxy.rs rename to crates/command_definitions/src/autoproxy.rs diff --git a/crates/commands/src/commands/checks.rs b/crates/command_definitions/src/checks.rs similarity index 100% rename from crates/commands/src/commands/checks.rs rename to crates/command_definitions/src/checks.rs diff --git a/crates/commands/src/commands/commands.rs b/crates/command_definitions/src/commands.rs similarity index 100% rename from crates/commands/src/commands/commands.rs rename to crates/command_definitions/src/commands.rs diff --git a/crates/commands/src/commands/config.rs b/crates/command_definitions/src/config.rs similarity index 100% rename from crates/commands/src/commands/config.rs rename to crates/command_definitions/src/config.rs diff --git a/crates/commands/src/commands/dashboard.rs b/crates/command_definitions/src/dashboard.rs similarity index 100% rename from crates/commands/src/commands/dashboard.rs rename to crates/command_definitions/src/dashboard.rs diff --git a/crates/commands/src/commands/debug.rs b/crates/command_definitions/src/debug.rs similarity index 100% rename from crates/commands/src/commands/debug.rs rename to crates/command_definitions/src/debug.rs diff --git a/crates/commands/src/commands/fun.rs b/crates/command_definitions/src/fun.rs similarity index 100% rename from crates/commands/src/commands/fun.rs rename to crates/command_definitions/src/fun.rs diff --git a/crates/commands/src/commands/group.rs b/crates/command_definitions/src/group.rs similarity index 100% rename from crates/commands/src/commands/group.rs rename to crates/command_definitions/src/group.rs diff --git a/crates/commands/src/commands/help.rs b/crates/command_definitions/src/help.rs similarity index 100% rename from crates/commands/src/commands/help.rs rename to crates/command_definitions/src/help.rs diff --git a/crates/commands/src/commands/import_export.rs b/crates/command_definitions/src/import_export.rs similarity index 100% rename from crates/commands/src/commands/import_export.rs rename to crates/command_definitions/src/import_export.rs diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs new file mode 100644 index 00000000..9e8adb49 --- /dev/null +++ b/crates/command_definitions/src/lib.rs @@ -0,0 +1,29 @@ +pub mod admin; +pub mod api; +pub mod autoproxy; +pub mod checks; +pub mod commands; +pub mod config; +pub mod dashboard; +pub mod debug; +pub mod fun; +pub mod group; +pub mod help; +pub mod import_export; +pub mod member; +pub mod message; +pub mod misc; +pub mod random; +pub mod server_config; +pub mod switch; +pub mod system; + +use command_parser::{any, command, command::Command, parameter::*}; + +pub fn all() -> impl Iterator { + (help::cmds()) + .chain(system::cmds()) + .chain(member::cmds()) + .chain(config::cmds()) + .chain(fun::cmds()) +} diff --git a/crates/commands/src/commands/member.rs b/crates/command_definitions/src/member.rs similarity index 100% rename from crates/commands/src/commands/member.rs rename to crates/command_definitions/src/member.rs diff --git a/crates/commands/src/commands/message.rs b/crates/command_definitions/src/message.rs similarity index 100% rename from crates/commands/src/commands/message.rs rename to crates/command_definitions/src/message.rs diff --git a/crates/commands/src/commands/misc.rs b/crates/command_definitions/src/misc.rs similarity index 100% rename from crates/commands/src/commands/misc.rs rename to crates/command_definitions/src/misc.rs diff --git a/crates/commands/src/commands/random.rs b/crates/command_definitions/src/random.rs similarity index 100% rename from crates/commands/src/commands/random.rs rename to crates/command_definitions/src/random.rs diff --git a/crates/commands/src/commands/server_config.rs b/crates/command_definitions/src/server_config.rs similarity index 100% rename from crates/commands/src/commands/server_config.rs rename to crates/command_definitions/src/server_config.rs diff --git a/crates/commands/src/commands/switch.rs b/crates/command_definitions/src/switch.rs similarity index 100% rename from crates/commands/src/commands/switch.rs rename to crates/command_definitions/src/switch.rs diff --git a/crates/commands/src/commands/system.rs b/crates/command_definitions/src/system.rs similarity index 100% rename from crates/commands/src/commands/system.rs rename to crates/command_definitions/src/system.rs diff --git a/crates/command_parser/Cargo.toml b/crates/command_parser/Cargo.toml new file mode 100644 index 00000000..749d348c --- /dev/null +++ b/crates/command_parser/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "command_parser" +version = "0.1.0" +edition = "2021" + +[dependencies] +lazy_static = { workspace = true } +smol_str = "0.3.2" +ordermap = "0.5" diff --git a/crates/commands/src/commands.rs b/crates/command_parser/src/command.rs similarity index 82% rename from crates/commands/src/commands.rs rename to crates/command_parser/src/command.rs index eef0a9ec..0410834b 100644 --- a/crates/commands/src/commands.rs +++ b/crates/command_parser/src/command.rs @@ -1,28 +1,8 @@ -pub mod admin; -pub mod api; -pub mod autoproxy; -pub mod checks; -pub mod commands; -pub mod config; -pub mod dashboard; -pub mod debug; -pub mod fun; -pub mod group; -pub mod help; -pub mod import_export; -pub mod member; -pub mod message; -pub mod misc; -pub mod random; -pub mod server_config; -pub mod switch; -pub mod system; - use std::fmt::{Debug, Display}; use smol_str::SmolStr; -use crate::{any, command, flag::Flag, parameter::*, token::Token}; +use crate::{flag::Flag, parameter::*, token::Token}; #[derive(Debug, Clone)] pub struct Command { @@ -116,14 +96,6 @@ impl Display for Command { #[macro_export] macro_rules! command { ([$($v:expr),+], $cb:expr$(,)*) => { - $crate::commands::Command::new([$(Token::from($v)),*], $cb) + $crate::command::Command::new([$($crate::token::Token::from($v)),*], $cb) }; } - -pub fn all() -> impl Iterator { - (help::cmds()) - .chain(system::cmds()) - .chain(member::cmds()) - .chain(config::cmds()) - .chain(fun::cmds()) -} diff --git a/crates/commands/src/flag.rs b/crates/command_parser/src/flag.rs similarity index 94% rename from crates/commands/src/flag.rs rename to crates/command_parser/src/flag.rs index 0b3fce4f..6bd12f2b 100644 --- a/crates/commands/src/flag.rs +++ b/crates/command_parser/src/flag.rs @@ -2,7 +2,7 @@ use std::{fmt::Display, sync::Arc}; use smol_str::SmolStr; -use crate::{parameter::Parameter, Parameter as FfiParam}; +use crate::parameter::{Parameter, ParameterValue}; #[derive(Debug)] pub enum FlagValueMatchError { @@ -32,7 +32,7 @@ pub enum FlagMatchError { ValueMatchFailed(FlagValueMatchError), } -type TryMatchFlagResult = Option, FlagMatchError>>; +type TryMatchFlagResult = Option, FlagMatchError>>; impl Flag { pub fn new(name: impl Into) -> Self { diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs new file mode 100644 index 00000000..5da79c36 --- /dev/null +++ b/crates/command_parser/src/lib.rs @@ -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, + pub flags: HashMap>, +} + +pub fn parse_command( + command_tree: Tree, + prefix: String, + input: String, +) -> Result { + 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 = HashMap::new(); + let mut raw_flags: Vec<(usize, MatchedFlag)> = Vec::new(); + + loop { + 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; + 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 .", + ); + + // 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> = 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)) => { + 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, + matched_flag: MatchedFlag<'a>, +) -> Option), (&'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, + input: &str, + current_pos: usize, +) -> Option, 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, +) -> 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; +} diff --git a/crates/commands/src/parameter.rs b/crates/command_parser/src/parameter.rs similarity index 79% rename from crates/commands/src/parameter.rs rename to crates/command_parser/src/parameter.rs index 7833a43e..ff67ee7e 100644 --- a/crates/commands/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -2,7 +2,17 @@ use std::{fmt::Debug, str::FromStr}; use smol_str::SmolStr; -use crate::{ParamName, Parameter as FfiParam}; +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 { @@ -10,7 +20,7 @@ pub trait Parameter: Debug + Send + Sync { } 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; + fn match_value(&self, input: &str) -> Result; } #[derive(Debug, Clone, Eq, Hash, PartialEq)] @@ -34,8 +44,8 @@ impl Parameter for OpaqueString { write!(f, "[{name}]") } - fn match_value(&self, input: &str) -> Result { - Ok(FfiParam::OpaqueString { raw: input.into() }) + fn match_value(&self, input: &str) -> Result { + Ok(ParameterValue::OpaqueString(input.into())) } } @@ -51,10 +61,8 @@ impl Parameter for MemberRef { write!(f, "") } - fn match_value(&self, input: &str) -> Result { - Ok(FfiParam::MemberRef { - member: input.into(), - }) + fn match_value(&self, input: &str) -> Result { + Ok(ParameterValue::MemberRef(input.into())) } } @@ -70,10 +78,8 @@ impl Parameter for SystemRef { write!(f, "") } - fn match_value(&self, input: &str) -> Result { - Ok(FfiParam::SystemRef { - system: input.into(), - }) + fn match_value(&self, input: &str) -> Result { + Ok(ParameterValue::SystemRef(input.into())) } } @@ -138,10 +144,9 @@ impl Parameter for MemberPrivacyTarget { write!(f, "") } - fn match_value(&self, input: &str) -> Result { - MemberPrivacyTargetKind::from_str(input).map(|target| FfiParam::MemberPrivacyTarget { - target: target.as_ref().into(), - }) + fn match_value(&self, input: &str) -> Result { + MemberPrivacyTargetKind::from_str(input) + .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())) } } @@ -183,10 +188,9 @@ impl Parameter for PrivacyLevel { write!(f, "[privacy level]") } - fn match_value(&self, input: &str) -> Result { - PrivacyLevelKind::from_str(input).map(|level| FfiParam::PrivacyLevel { - level: level.as_ref().into(), - }) + fn match_value(&self, input: &str) -> Result { + PrivacyLevelKind::from_str(input) + .map(|level| ParameterValue::PrivacyLevel(level.as_ref().into())) } } @@ -219,8 +223,8 @@ impl Parameter for Reset { write!(f, "reset") } - fn match_value(&self, input: &str) -> Result { - Self::from_str(input).map(|_| FfiParam::Toggle { toggle: true }) + fn match_value(&self, input: &str) -> Result { + Self::from_str(input).map(|_| ParameterValue::Toggle(true)) } } @@ -236,11 +240,11 @@ impl Parameter for Toggle { write!(f, "on/off") } - fn match_value(&self, input: &str) -> Result { + fn match_value(&self, input: &str) -> Result { Enable::from_str(input) .map(Into::::into) .or_else(|_| Disable::from_str(input).map(Into::::into)) - .map(|toggle| FfiParam::Toggle { toggle }) + .map(ParameterValue::Toggle) .map_err(|_| "invalid toggle".into()) } } @@ -268,8 +272,8 @@ impl Parameter for Enable { write!(f, "on") } - fn match_value(&self, input: &str) -> Result { - Self::from_str(input).map(|e| FfiParam::Toggle { toggle: e.into() }) + fn match_value(&self, input: &str) -> Result { + Self::from_str(input).map(|e| ParameterValue::Toggle(e.into())) } } @@ -308,7 +312,7 @@ impl Parameter for Disable { write!(f, "off") } - fn match_value(&self, input: &str) -> Result { - Self::from_str(input).map(|e| FfiParam::Toggle { toggle: e.into() }) + fn match_value(&self, input: &str) -> Result { + Self::from_str(input).map(|e| ParameterValue::Toggle(e.into())) } } diff --git a/crates/commands/src/string.rs b/crates/command_parser/src/string.rs similarity index 100% rename from crates/commands/src/string.rs rename to crates/command_parser/src/string.rs diff --git a/crates/commands/src/token.rs b/crates/command_parser/src/token.rs similarity index 94% rename from crates/commands/src/token.rs rename to crates/command_parser/src/token.rs index c21ee16e..aae2f06e 100644 --- a/crates/commands/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -7,7 +7,7 @@ use std::{ use smol_str::SmolStr; -use crate::{parameter::Parameter, Parameter as FfiParam}; +use crate::parameter::{Parameter, ParameterValue}; pub type ParamName = &'static str; @@ -31,7 +31,7 @@ pub enum Token { #[macro_export] macro_rules! any { ($($t:expr),+) => { - Token::Any(vec![$(Token::from($t)),+]) + $crate::token::Token::Any(vec![$($crate::token::Token::from($t)),+]) }; } @@ -68,9 +68,9 @@ pub enum TokenMatchError { } #[derive(Debug)] -pub struct TokenMatchValue { +pub(super) struct TokenMatchValue { pub raw: SmolStr, - pub param: Option<(ParamName, FfiParam)>, + pub param: Option<(ParamName, ParameterValue)>, } impl TokenMatchValue { @@ -84,7 +84,7 @@ impl TokenMatchValue { fn new_match_param( raw: impl Into, param_name: ParamName, - param: FfiParam, + param: ParameterValue, ) -> TryMatchResult { Some(Ok(Some(Self { raw: raw.into(), @@ -104,7 +104,7 @@ impl TokenMatchValue { type TryMatchResult = Option, TokenMatchError>>; impl Token { - pub fn try_match(&self, input: Option<&str>) -> TryMatchResult { + pub(super) fn try_match(&self, input: Option<&str>) -> TryMatchResult { let input = match input { Some(input) => input, None => { diff --git a/crates/commands/src/tree.rs b/crates/command_parser/src/tree.rs similarity index 81% rename from crates/commands/src/tree.rs rename to crates/command_parser/src/tree.rs index 859ba9ed..5f78eff6 100644 --- a/crates/commands/src/tree.rs +++ b/crates/command_parser/src/tree.rs @@ -1,6 +1,6 @@ use ordermap::OrderMap; -use crate::{commands::Command, Token}; +use crate::{command::Command, token::Token}; #[derive(Debug, Clone)] pub struct TreeBranch { @@ -8,14 +8,16 @@ pub struct TreeBranch { branches: OrderMap, } -impl TreeBranch { - pub fn empty() -> Self { +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 @@ -24,7 +26,7 @@ impl TreeBranch { current_branch = current_branch .branches .entry(token) - .or_insert_with(TreeBranch::empty); + .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( @@ -36,15 +38,15 @@ impl TreeBranch { ); } - pub fn command(&self) -> Option { + pub(super) fn command(&self) -> Option { self.current_command.clone() } - pub fn possible_tokens(&self) -> impl Iterator { + pub(super) fn possible_tokens(&self) -> impl Iterator { self.branches.keys() } - pub fn possible_commands(&self, max_depth: usize) -> impl Iterator { + pub(super) fn possible_commands(&self, max_depth: usize) -> impl Iterator { // 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 + 'a, @@ -67,7 +69,7 @@ impl TreeBranch { commands } - pub fn get_branch(&self, token: &Token) -> Option<&TreeBranch> { + pub(super) fn get_branch(&self, token: &Token) -> Option<&TreeBranch> { self.branches.get(token) } } diff --git a/crates/commands/Cargo.toml b/crates/commands/Cargo.toml index 4983cb53..46bca6a7 100644 --- a/crates/commands/Cargo.toml +++ b/crates/commands/Cargo.toml @@ -8,10 +8,9 @@ crate-type = ["cdylib", "lib"] [dependencies] lazy_static = { workspace = true } - +command_parser = { path = "../command_parser"} +command_definitions = { path = "../command_definitions"} uniffi = { version = "0.25" } -smol_str = "0.3.2" -ordermap = "0.5" [build-dependencies] -uniffi = { version = "0.25", features = [ "build" ] } +uniffi = { version = "0.25", features = [ "build" ] } \ No newline at end of file diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 4eb10a56..21c1db13 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -1,36 +1,14 @@ -#![feature(let_chains)] -#![feature(anonymous_lifetime_in_impl_trait)] +use std::collections::HashMap; -pub mod commands; -mod flag; -mod parameter; -mod string; -mod token; -mod tree; +use command_parser::{parameter::ParameterValue, Tree}; uniffi::include_scaffolding!("commands"); -use core::panic; -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; -pub use token::*; - -// todo: this should come from the bot probably -const MAX_SUGGESTIONS: usize = 7; - lazy_static::lazy_static! { - pub static ref COMMAND_TREE: TreeBranch = { - let mut tree = TreeBranch::empty(); + pub static ref COMMAND_TREE: Tree = { + let mut tree = Tree::default(); - crate::commands::all().into_iter().for_each(|x| tree.register_command(x)); + command_definitions::all().into_iter().for_each(|x| tree.register_command(x)); tree }; @@ -52,6 +30,19 @@ pub enum Parameter { Toggle { toggle: bool }, } +impl From for Parameter { + fn from(value: ParameterValue) -> Self { + match value { + ParameterValue::MemberRef(member) => Self::MemberRef { member }, + ParameterValue::SystemRef(system) => Self::SystemRef { system }, + ParameterValue::MemberPrivacyTarget(target) => Self::MemberPrivacyTarget { target }, + ParameterValue::PrivacyLevel(level) => Self::PrivacyLevel { level }, + ParameterValue::OpaqueString(raw) => Self::OpaqueString { raw }, + ParameterValue::Toggle(toggle) => Self::Toggle { toggle }, + } + } +} + #[derive(Debug)] pub struct ParsedCommand { pub command_ref: String, @@ -60,276 +51,25 @@ pub struct ParsedCommand { } pub fn parse_command(prefix: String, input: String) -> CommandResult { - let input: SmolStr = input.into(); - let mut local_tree: TreeBranch = 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 = HashMap::new(); - let mut raw_flags: Vec<(usize, MatchedFlag)> = Vec::new(); - - loop { - 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; - 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()); - } + command_parser::parse_command(COMMAND_TREE.clone(), prefix, input).map_or_else( + |error| CommandResult::Err { error }, + |parsed| CommandResult::Ok { + command: { + let command_ref = parsed.command_def.cb.into(); + let mut flags = HashMap::with_capacity(parsed.flags.capacity()); + for (name, value) in parsed.flags { + flags.insert(name, value.map(Parameter::from)); } - - 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}"); + let mut params = HashMap::with_capacity(parsed.parameters.capacity()); + for (name, value) in parsed.parameters { + params.insert(name, Parameter::from(value)); } - } - 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 CommandResult::Err { error: 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 .", - ); - - // 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 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)) => { - 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 CommandResult::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 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, + ParsedCommand { + command_ref, flags, - }, - }; - } - } -} - -fn match_flag<'a>( - possible_flags: impl Iterator, - matched_flag: MatchedFlag<'a>, -) -> Option), (&'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, - input: &str, - current_pos: usize, -) -> Option, 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, -) -> 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; + params, + } + }, + }, + ) } diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index 6c722be3..313c6d74 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -1,7 +1,5 @@ #![feature(iter_intersperse)] -use commands::commands as cmds; - fn main() { let cmd = std::env::args() .skip(1) @@ -15,7 +13,7 @@ fn main() { CommandResult::Err { error } => println!("{error}"), } } else { - for command in cmds::all() { + for command in command_definitions::all() { println!("{} - {}", command, command.help); } }