mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-04 04:56:49 +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
18
Cargo.lock
generated
18
Cargo.lock
generated
|
|
@ -565,12 +565,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "commands"
|
name = "command_definitions"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"command_parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "command_parser"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"ordermap",
|
"ordermap",
|
||||||
"smol_str",
|
"smol_str",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "commands"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"command_definitions",
|
||||||
|
"command_parser",
|
||||||
|
"lazy_static",
|
||||||
"uniffi",
|
"uniffi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
7
crates/command_definitions/Cargo.toml
Normal file
7
crates/command_definitions/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
[package]
|
||||||
|
name = "command_definitions"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
command_parser = { path = "../command_parser"}
|
||||||
29
crates/command_definitions/src/lib.rs
Normal file
29
crates/command_definitions/src/lib.rs
Normal file
|
|
@ -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<Item = Command> {
|
||||||
|
(help::cmds())
|
||||||
|
.chain(system::cmds())
|
||||||
|
.chain(member::cmds())
|
||||||
|
.chain(config::cmds())
|
||||||
|
.chain(fun::cmds())
|
||||||
|
}
|
||||||
9
crates/command_parser/Cargo.toml
Normal file
9
crates/command_parser/Cargo.toml
Normal file
|
|
@ -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"
|
||||||
|
|
@ -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 std::fmt::{Debug, Display};
|
||||||
|
|
||||||
use smol_str::SmolStr;
|
use smol_str::SmolStr;
|
||||||
|
|
||||||
use crate::{any, command, flag::Flag, parameter::*, token::Token};
|
use crate::{flag::Flag, parameter::*, token::Token};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Command {
|
pub struct Command {
|
||||||
|
|
@ -116,14 +96,6 @@ impl Display for Command {
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! command {
|
macro_rules! command {
|
||||||
([$($v:expr),+], $cb:expr$(,)*) => {
|
([$($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<Item = Command> {
|
|
||||||
(help::cmds())
|
|
||||||
.chain(system::cmds())
|
|
||||||
.chain(member::cmds())
|
|
||||||
.chain(config::cmds())
|
|
||||||
.chain(fun::cmds())
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::{fmt::Display, sync::Arc};
|
||||||
|
|
||||||
use smol_str::SmolStr;
|
use smol_str::SmolStr;
|
||||||
|
|
||||||
use crate::{parameter::Parameter, Parameter as FfiParam};
|
use crate::parameter::{Parameter, ParameterValue};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum FlagValueMatchError {
|
pub enum FlagValueMatchError {
|
||||||
|
|
@ -32,7 +32,7 @@ pub enum FlagMatchError {
|
||||||
ValueMatchFailed(FlagValueMatchError),
|
ValueMatchFailed(FlagValueMatchError),
|
||||||
}
|
}
|
||||||
|
|
||||||
type TryMatchFlagResult = Option<Result<Option<FfiParam>, FlagMatchError>>;
|
type TryMatchFlagResult = Option<Result<Option<ParameterValue>, FlagMatchError>>;
|
||||||
|
|
||||||
impl Flag {
|
impl Flag {
|
||||||
pub fn new(name: impl Into<SmolStr>) -> Self {
|
pub fn new(name: impl Into<SmolStr>) -> Self {
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,17 @@ use std::{fmt::Debug, str::FromStr};
|
||||||
|
|
||||||
use smol_str::SmolStr;
|
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 {
|
pub trait Parameter: Debug + Send + Sync {
|
||||||
fn remainder(&self) -> bool {
|
fn remainder(&self) -> bool {
|
||||||
|
|
@ -10,7 +20,7 @@ pub trait Parameter: Debug + Send + Sync {
|
||||||
}
|
}
|
||||||
fn default_name(&self) -> ParamName;
|
fn default_name(&self) -> ParamName;
|
||||||
fn format(&self, f: &mut std::fmt::Formatter, name: &str) -> std::fmt::Result;
|
fn format(&self, f: &mut std::fmt::Formatter, name: &str) -> std::fmt::Result;
|
||||||
fn match_value(&self, input: &str) -> Result<FfiParam, SmolStr>;
|
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||||
|
|
@ -34,8 +44,8 @@ impl Parameter for OpaqueString {
|
||||||
write!(f, "[{name}]")
|
write!(f, "[{name}]")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_value(&self, input: &str) -> Result<FfiParam, SmolStr> {
|
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||||
Ok(FfiParam::OpaqueString { raw: input.into() })
|
Ok(ParameterValue::OpaqueString(input.into()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,10 +61,8 @@ impl Parameter for MemberRef {
|
||||||
write!(f, "<target member>")
|
write!(f, "<target member>")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_value(&self, input: &str) -> Result<FfiParam, SmolStr> {
|
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||||
Ok(FfiParam::MemberRef {
|
Ok(ParameterValue::MemberRef(input.into()))
|
||||||
member: input.into(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,10 +78,8 @@ impl Parameter for SystemRef {
|
||||||
write!(f, "<target system>")
|
write!(f, "<target system>")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_value(&self, input: &str) -> Result<FfiParam, SmolStr> {
|
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||||
Ok(FfiParam::SystemRef {
|
Ok(ParameterValue::SystemRef(input.into()))
|
||||||
system: input.into(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,10 +144,9 @@ impl Parameter for MemberPrivacyTarget {
|
||||||
write!(f, "<privacy target>")
|
write!(f, "<privacy target>")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_value(&self, input: &str) -> Result<FfiParam, SmolStr> {
|
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||||
MemberPrivacyTargetKind::from_str(input).map(|target| FfiParam::MemberPrivacyTarget {
|
MemberPrivacyTargetKind::from_str(input)
|
||||||
target: target.as_ref().into(),
|
.map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into()))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,10 +188,9 @@ impl Parameter for PrivacyLevel {
|
||||||
write!(f, "[privacy level]")
|
write!(f, "[privacy level]")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_value(&self, input: &str) -> Result<FfiParam, SmolStr> {
|
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||||
PrivacyLevelKind::from_str(input).map(|level| FfiParam::PrivacyLevel {
|
PrivacyLevelKind::from_str(input)
|
||||||
level: level.as_ref().into(),
|
.map(|level| ParameterValue::PrivacyLevel(level.as_ref().into()))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,8 +223,8 @@ impl Parameter for Reset {
|
||||||
write!(f, "reset")
|
write!(f, "reset")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_value(&self, input: &str) -> Result<FfiParam, SmolStr> {
|
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||||
Self::from_str(input).map(|_| FfiParam::Toggle { toggle: true })
|
Self::from_str(input).map(|_| ParameterValue::Toggle(true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,11 +240,11 @@ impl Parameter for Toggle {
|
||||||
write!(f, "on/off")
|
write!(f, "on/off")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_value(&self, input: &str) -> Result<FfiParam, SmolStr> {
|
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||||
Enable::from_str(input)
|
Enable::from_str(input)
|
||||||
.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))
|
||||||
.map(|toggle| FfiParam::Toggle { toggle })
|
.map(ParameterValue::Toggle)
|
||||||
.map_err(|_| "invalid toggle".into())
|
.map_err(|_| "invalid toggle".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -268,8 +272,8 @@ impl Parameter for Enable {
|
||||||
write!(f, "on")
|
write!(f, "on")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_value(&self, input: &str) -> Result<FfiParam, SmolStr> {
|
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||||
Self::from_str(input).map(|e| FfiParam::Toggle { toggle: e.into() })
|
Self::from_str(input).map(|e| ParameterValue::Toggle(e.into()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,7 +312,7 @@ impl Parameter for Disable {
|
||||||
write!(f, "off")
|
write!(f, "off")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_value(&self, input: &str) -> Result<FfiParam, SmolStr> {
|
fn match_value(&self, input: &str) -> Result<ParameterValue, SmolStr> {
|
||||||
Self::from_str(input).map(|e| FfiParam::Toggle { toggle: e.into() })
|
Self::from_str(input).map(|e| ParameterValue::Toggle(e.into()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ use std::{
|
||||||
|
|
||||||
use smol_str::SmolStr;
|
use smol_str::SmolStr;
|
||||||
|
|
||||||
use crate::{parameter::Parameter, Parameter as FfiParam};
|
use crate::parameter::{Parameter, ParameterValue};
|
||||||
|
|
||||||
pub type ParamName = &'static str;
|
pub type ParamName = &'static str;
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ pub enum Token {
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! any {
|
macro_rules! any {
|
||||||
($($t:expr),+) => {
|
($($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)]
|
#[derive(Debug)]
|
||||||
pub struct TokenMatchValue {
|
pub(super) struct TokenMatchValue {
|
||||||
pub raw: SmolStr,
|
pub raw: SmolStr,
|
||||||
pub param: Option<(ParamName, FfiParam)>,
|
pub param: Option<(ParamName, ParameterValue)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TokenMatchValue {
|
impl TokenMatchValue {
|
||||||
|
|
@ -84,7 +84,7 @@ impl TokenMatchValue {
|
||||||
fn new_match_param(
|
fn new_match_param(
|
||||||
raw: impl Into<SmolStr>,
|
raw: impl Into<SmolStr>,
|
||||||
param_name: ParamName,
|
param_name: ParamName,
|
||||||
param: FfiParam,
|
param: ParameterValue,
|
||||||
) -> TryMatchResult {
|
) -> TryMatchResult {
|
||||||
Some(Ok(Some(Self {
|
Some(Ok(Some(Self {
|
||||||
raw: raw.into(),
|
raw: raw.into(),
|
||||||
|
|
@ -104,7 +104,7 @@ impl TokenMatchValue {
|
||||||
type TryMatchResult = Option<Result<Option<TokenMatchValue>, TokenMatchError>>;
|
type TryMatchResult = Option<Result<Option<TokenMatchValue>, TokenMatchError>>;
|
||||||
|
|
||||||
impl Token {
|
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 {
|
let input = match input {
|
||||||
Some(input) => input,
|
Some(input) => input,
|
||||||
None => {
|
None => {
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use ordermap::OrderMap;
|
use ordermap::OrderMap;
|
||||||
|
|
||||||
use crate::{commands::Command, Token};
|
use crate::{command::Command, token::Token};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TreeBranch {
|
pub struct TreeBranch {
|
||||||
|
|
@ -8,14 +8,16 @@ pub struct TreeBranch {
|
||||||
branches: OrderMap<Token, TreeBranch>,
|
branches: OrderMap<Token, TreeBranch>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TreeBranch {
|
impl Default for TreeBranch {
|
||||||
pub fn empty() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
current_command: None,
|
current_command: None,
|
||||||
branches: OrderMap::new(),
|
branches: OrderMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TreeBranch {
|
||||||
pub fn register_command(&mut self, command: Command) {
|
pub fn register_command(&mut self, command: Command) {
|
||||||
let mut current_branch = self;
|
let mut current_branch = self;
|
||||||
// iterate over tokens in command
|
// iterate over tokens in command
|
||||||
|
|
@ -24,7 +26,7 @@ impl TreeBranch {
|
||||||
current_branch = current_branch
|
current_branch = current_branch
|
||||||
.branches
|
.branches
|
||||||
.entry(token)
|
.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
|
// 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(
|
||||||
|
|
@ -36,15 +38,15 @@ impl TreeBranch {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn command(&self) -> Option<Command> {
|
pub(super) fn command(&self) -> Option<Command> {
|
||||||
self.current_command.clone()
|
self.current_command.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn possible_tokens(&self) -> impl Iterator<Item = &Token> {
|
pub(super) fn possible_tokens(&self) -> impl Iterator<Item = &Token> {
|
||||||
self.branches.keys()
|
self.branches.keys()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn possible_commands(&self, max_depth: usize) -> impl Iterator<Item = &Command> {
|
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
|
// dusk: i am too lazy to write an iterator for this without using recursion so we box everything
|
||||||
fn box_iter<'a>(
|
fn box_iter<'a>(
|
||||||
iter: impl Iterator<Item = &'a Command> + 'a,
|
iter: impl Iterator<Item = &'a Command> + 'a,
|
||||||
|
|
@ -67,7 +69,7 @@ impl TreeBranch {
|
||||||
commands
|
commands
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_branch(&self, token: &Token) -> Option<&TreeBranch> {
|
pub(super) fn get_branch(&self, token: &Token) -> Option<&TreeBranch> {
|
||||||
self.branches.get(token)
|
self.branches.get(token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,10 +8,9 @@ crate-type = ["cdylib", "lib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lazy_static = { workspace = true }
|
lazy_static = { workspace = true }
|
||||||
|
command_parser = { path = "../command_parser"}
|
||||||
|
command_definitions = { path = "../command_definitions"}
|
||||||
uniffi = { version = "0.25" }
|
uniffi = { version = "0.25" }
|
||||||
smol_str = "0.3.2"
|
|
||||||
ordermap = "0.5"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
uniffi = { version = "0.25", features = [ "build" ] }
|
uniffi = { version = "0.25", features = [ "build" ] }
|
||||||
|
|
@ -1,36 +1,14 @@
|
||||||
#![feature(let_chains)]
|
use std::collections::HashMap;
|
||||||
#![feature(anonymous_lifetime_in_impl_trait)]
|
|
||||||
|
|
||||||
pub mod commands;
|
use command_parser::{parameter::ParameterValue, Tree};
|
||||||
mod flag;
|
|
||||||
mod parameter;
|
|
||||||
mod string;
|
|
||||||
mod token;
|
|
||||||
mod tree;
|
|
||||||
|
|
||||||
uniffi::include_scaffolding!("commands");
|
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! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref COMMAND_TREE: TreeBranch = {
|
pub static ref COMMAND_TREE: Tree = {
|
||||||
let mut tree = TreeBranch::empty();
|
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
|
tree
|
||||||
};
|
};
|
||||||
|
|
@ -52,6 +30,19 @@ pub enum Parameter {
|
||||||
Toggle { toggle: bool },
|
Toggle { toggle: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ParameterValue> 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)]
|
#[derive(Debug)]
|
||||||
pub struct ParsedCommand {
|
pub struct ParsedCommand {
|
||||||
pub command_ref: String,
|
pub command_ref: String,
|
||||||
|
|
@ -60,276 +51,25 @@ pub struct ParsedCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_command(prefix: String, input: String) -> CommandResult {
|
pub fn parse_command(prefix: String, input: String) -> CommandResult {
|
||||||
let input: SmolStr = input.into();
|
command_parser::parse_command(COMMAND_TREE.clone(), prefix, input).map_or_else(
|
||||||
let mut local_tree: TreeBranch = COMMAND_TREE.clone();
|
|error| CommandResult::Err { error },
|
||||||
|
|parsed| CommandResult::Ok {
|
||||||
// end position of all currently matched tokens
|
command: {
|
||||||
let mut current_pos: usize = 0;
|
let command_ref = parsed.command_def.cb.into();
|
||||||
let mut current_token_idx: usize = 0;
|
let mut flags = HashMap::with_capacity(parsed.flags.capacity());
|
||||||
|
for (name, value) in parsed.flags {
|
||||||
let mut params: HashMap<String, Parameter> = HashMap::new();
|
flags.insert(name, value.map(Parameter::from));
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let mut params = HashMap::with_capacity(parsed.parameters.capacity());
|
||||||
if let Some(next_tree) = local_tree.get_branch(&found_token) {
|
for (name, value) in parsed.parameters {
|
||||||
local_tree = next_tree.clone();
|
params.insert(name, Parameter::from(value));
|
||||||
} else {
|
|
||||||
panic!("found token {found_token:?} could not match tree, at {input}");
|
|
||||||
}
|
}
|
||||||
}
|
ParsedCommand {
|
||||||
Some(Err((token, err))) => {
|
command_ref,
|
||||||
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 <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 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)) => {
|
|
||||||
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,
|
|
||||||
flags,
|
flags,
|
||||||
},
|
params,
|
||||||
};
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
)
|
||||||
|
|
||||||
fn match_flag<'a>(
|
|
||||||
possible_flags: impl Iterator<Item = &'a Flag>,
|
|
||||||
matched_flag: MatchedFlag<'a>,
|
|
||||||
) -> Option<Result<(SmolStr, Option<Parameter>), (&'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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
#![feature(iter_intersperse)]
|
#![feature(iter_intersperse)]
|
||||||
|
|
||||||
use commands::commands as cmds;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cmd = std::env::args()
|
let cmd = std::env::args()
|
||||||
.skip(1)
|
.skip(1)
|
||||||
|
|
@ -15,7 +13,7 @@ fn main() {
|
||||||
CommandResult::Err { error } => println!("{error}"),
|
CommandResult::Err { error } => println!("{error}"),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for command in cmds::all() {
|
for command in command_definitions::all() {
|
||||||
println!("{} - {}", command, command.help);
|
println!("{} - {}", command, command.help);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue