mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-12 00:30:11 +00:00
feat: implement proper ("static") parameters handling command parser -> bot
feat: handle few more commands bot side fix(commands): handle missing parameters and return error refactor(commands): use ordermap instead of relying on a sort function to sort tokens
This commit is contained in:
parent
1a781014bd
commit
eec9f64026
16 changed files with 358 additions and 502 deletions
|
|
@ -6,8 +6,21 @@ interface CommandResult {
|
|||
Ok(ParsedCommand command);
|
||||
Err(string error);
|
||||
};
|
||||
[Enum]
|
||||
interface ParameterKind {
|
||||
MemberRef();
|
||||
SystemRef();
|
||||
MemberPrivacyTarget();
|
||||
PrivacyLevel();
|
||||
OpaqueString();
|
||||
};
|
||||
dictionary Parameter {
|
||||
string raw;
|
||||
ParameterKind kind;
|
||||
};
|
||||
dictionary ParsedCommand {
|
||||
string command_ref;
|
||||
sequence<string> args;
|
||||
record<string, Parameter> params;
|
||||
record<string, string?> flags;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,37 +10,37 @@ pub fn cmds() -> impl Iterator<Item = Command> {
|
|||
|
||||
[
|
||||
command!(
|
||||
[member, new, MemberRef],
|
||||
[member, new, FullString("name")],
|
||||
"member_new",
|
||||
"Creates a new system member"
|
||||
),
|
||||
command!(
|
||||
[member, MemberRef],
|
||||
[member, MemberRef("target")],
|
||||
"member_show",
|
||||
"Shows information about a member"
|
||||
),
|
||||
command!(
|
||||
[member, MemberRef, description],
|
||||
[member, MemberRef("target"), description],
|
||||
"member_desc_show",
|
||||
"Shows a member's description"
|
||||
),
|
||||
command!(
|
||||
[member, MemberRef, description, FullString],
|
||||
[member, MemberRef("target"), description, FullString("description")],
|
||||
"member_desc_update",
|
||||
"Changes a member's description"
|
||||
),
|
||||
command!(
|
||||
[member, MemberRef, privacy],
|
||||
[member, MemberRef("target"), privacy],
|
||||
"member_privacy_show",
|
||||
"Displays a member's current privacy settings"
|
||||
),
|
||||
command!(
|
||||
[
|
||||
member,
|
||||
MemberRef,
|
||||
MemberRef("target"),
|
||||
privacy,
|
||||
MemberPrivacyTarget,
|
||||
PrivacyLevel
|
||||
MemberPrivacyTarget("privacy_target"),
|
||||
PrivacyLevel("new_privacy_level")
|
||||
],
|
||||
"member_privacy_update",
|
||||
"Changes a member's privacy settings"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ pub fn cmds() -> impl Iterator<Item = Command> {
|
|||
),
|
||||
command!([system, new], "system_new", "Creates a new system"),
|
||||
command!(
|
||||
[system, new, SystemRef],
|
||||
[system, new, FullString("name")],
|
||||
"system_new",
|
||||
"Creates a new system"
|
||||
),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ uniffi::include_scaffolding!("commands");
|
|||
use core::panic;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use smol_str::SmolStr;
|
||||
use ordermap::OrderMap;
|
||||
use smol_str::{format_smolstr, SmolStr};
|
||||
use tree::TreeBranch;
|
||||
|
||||
pub use commands::Command;
|
||||
|
|
@ -21,27 +22,70 @@ lazy_static::lazy_static! {
|
|||
let mut tree = TreeBranch {
|
||||
current_command_key: None,
|
||||
possible_tokens: vec![],
|
||||
branches: HashMap::new(),
|
||||
branches: OrderMap::new(),
|
||||
};
|
||||
|
||||
crate::commands::all().iter().for_each(|x| tree.register_command(x.clone()));
|
||||
|
||||
tree.sort_tokens();
|
||||
|
||||
// println!("{{tree:#?}}");
|
||||
crate::commands::all().into_iter().for_each(|x| tree.register_command(x));
|
||||
|
||||
tree
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CommandResult {
|
||||
Ok { command: ParsedCommand },
|
||||
Err { error: String },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ParameterKind {
|
||||
MemberRef,
|
||||
SystemRef,
|
||||
MemberPrivacyTarget,
|
||||
PrivacyLevel,
|
||||
OpaqueString,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Parameter {
|
||||
raw: String,
|
||||
kind: ParameterKind,
|
||||
}
|
||||
|
||||
impl Parameter {
|
||||
fn new(raw: impl ToString, kind: ParameterKind) -> Self {
|
||||
Self {
|
||||
raw: raw.to_string(),
|
||||
kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! parameter_impl {
|
||||
($($name:ident $kind:ident),*) => {
|
||||
impl Parameter {
|
||||
$(
|
||||
fn $name(raw: impl ToString) -> Self {
|
||||
Self::new(raw, $crate::ParameterKind::$kind)
|
||||
}
|
||||
)*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
parameter_impl! {
|
||||
opaque OpaqueString,
|
||||
member MemberRef,
|
||||
system SystemRef,
|
||||
member_privacy_target MemberPrivacyTarget,
|
||||
privacy_level PrivacyLevel
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedCommand {
|
||||
pub command_ref: String,
|
||||
pub args: Vec<String>,
|
||||
pub params: HashMap<String, Parameter>,
|
||||
pub flags: HashMap<String, Option<String>>,
|
||||
}
|
||||
|
||||
|
|
@ -53,9 +97,11 @@ fn parse_command(input: String) -> CommandResult {
|
|||
let mut current_pos = 0;
|
||||
|
||||
let mut args: Vec<String> = Vec::new();
|
||||
let mut params: HashMap<String, Parameter> = HashMap::new();
|
||||
let mut flags: HashMap<String, Option<String>> = HashMap::new();
|
||||
|
||||
loop {
|
||||
println!("{:?}", local_tree.possible_tokens);
|
||||
let next = next_token(
|
||||
local_tree.possible_tokens.clone(),
|
||||
input.clone(),
|
||||
|
|
@ -70,8 +116,22 @@ fn parse_command(input: String) -> CommandResult {
|
|||
continue;
|
||||
}
|
||||
|
||||
if let Some(arg) = arg {
|
||||
args.push(arg.into());
|
||||
if let Some(arg) = arg.as_ref() {
|
||||
// get param name from token
|
||||
// TODO: idk if this should be on token itself, doesn't feel right, but does work
|
||||
let param = match &found_token {
|
||||
Token::FullString(n) => Some((n, Parameter::opaque(arg))),
|
||||
Token::MemberRef(n) => Some((n, Parameter::member(arg))),
|
||||
Token::MemberPrivacyTarget(n) => Some((n, Parameter::member_privacy_target(arg))),
|
||||
Token::SystemRef(n) => Some((n, Parameter::system(arg))),
|
||||
Token::PrivacyLevel(n) => Some((n, Parameter::privacy_level(arg))),
|
||||
_ => None,
|
||||
};
|
||||
// insert arg as paramater if this is a parameter
|
||||
if let Some((param_name, param)) = param {
|
||||
params.insert(param_name.to_string(), param);
|
||||
}
|
||||
args.push(arg.to_string());
|
||||
}
|
||||
|
||||
if let Some(next_tree) = local_tree.branches.get(&found_token) {
|
||||
|
|
@ -82,9 +142,11 @@ fn parse_command(input: String) -> CommandResult {
|
|||
}
|
||||
Err(None) => {
|
||||
if let Some(command_ref) = local_tree.current_command_key {
|
||||
println!("{command_ref} {params:?}");
|
||||
return CommandResult::Ok {
|
||||
command: ParsedCommand {
|
||||
command_ref: command_ref.into(),
|
||||
params,
|
||||
args,
|
||||
flags,
|
||||
},
|
||||
|
|
@ -136,19 +198,12 @@ fn next_token(
|
|||
|
||||
// iterate over tokens and run try_match
|
||||
for token in possible_tokens {
|
||||
if let TokenMatchResult::Match(value) =
|
||||
// for FullString just send the whole string
|
||||
token.try_match(if matches!(token, Token::FullString) {
|
||||
if input.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(input.clone())
|
||||
}
|
||||
} else {
|
||||
param.clone().map(|v| v.0)
|
||||
})
|
||||
{
|
||||
return Ok((token, value, param.map(|v| v.1).unwrap_or(current_pos)));
|
||||
// for FullString just send the whole string
|
||||
let input_to_match = param.clone().map(|v| v.0);
|
||||
match token.try_match(input_to_match) {
|
||||
TokenMatchResult::Match(value) => return Ok((token, value, param.map(|v| v.1).unwrap_or(current_pos))),
|
||||
TokenMatchResult::MissingParameter { name } => return Err(Some(format_smolstr!("Missing parameter `{name}` in command `{input} [{name}]`."))),
|
||||
TokenMatchResult::NoMatch => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use smol_str::{SmolStr, ToSmolStr};
|
||||
|
||||
type ParamName = &'static str;
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub enum Token {
|
||||
/// Token used to represent a finished command (i.e. no more parameters required)
|
||||
|
|
@ -12,16 +14,16 @@ pub enum Token {
|
|||
// todo!
|
||||
MultiValue(Vec<Vec<SmolStr>>),
|
||||
|
||||
FullString,
|
||||
FullString(ParamName),
|
||||
|
||||
/// Member reference (hid or member name)
|
||||
MemberRef,
|
||||
MemberPrivacyTarget,
|
||||
MemberRef(ParamName),
|
||||
MemberPrivacyTarget(ParamName),
|
||||
|
||||
/// System reference
|
||||
SystemRef,
|
||||
SystemRef(ParamName),
|
||||
|
||||
PrivacyLevel,
|
||||
PrivacyLevel(ParamName),
|
||||
|
||||
// currently not included in command definitions
|
||||
// todo: flags with values
|
||||
|
|
@ -32,6 +34,9 @@ pub enum TokenMatchResult {
|
|||
NoMatch,
|
||||
/// Token matched, optionally with a value.
|
||||
Match(Option<SmolStr>),
|
||||
MissingParameter {
|
||||
name: ParamName,
|
||||
},
|
||||
}
|
||||
|
||||
// move this somewhere else
|
||||
|
|
@ -43,36 +48,38 @@ impl Token {
|
|||
if matches!(self, Self::Empty) && input.is_none() {
|
||||
return TokenMatchResult::Match(None);
|
||||
} else if input.is_none() {
|
||||
return TokenMatchResult::NoMatch;
|
||||
return match self {
|
||||
Self::FullString(param_name) => TokenMatchResult::MissingParameter { name: param_name },
|
||||
Self::MemberRef(param_name) => TokenMatchResult::MissingParameter { name: param_name },
|
||||
Self::MemberPrivacyTarget(param_name) => TokenMatchResult::MissingParameter { name: param_name },
|
||||
Self::SystemRef(param_name) => TokenMatchResult::MissingParameter { name: param_name },
|
||||
Self::PrivacyLevel(param_name) => TokenMatchResult::MissingParameter { name: param_name },
|
||||
_ => TokenMatchResult::NoMatch,
|
||||
}
|
||||
}
|
||||
|
||||
let input = input.unwrap();
|
||||
let input = input.as_ref().map(|s| s.trim()).unwrap();
|
||||
|
||||
// try actually matching stuff
|
||||
match self {
|
||||
Self::Empty => return TokenMatchResult::NoMatch,
|
||||
Self::Flag => unreachable!(), // matched upstream
|
||||
Self::Value(values) => {
|
||||
for v in values {
|
||||
if input.trim() == v {
|
||||
// c# bot currently needs subcommands provided as arguments
|
||||
// todo!: remove this
|
||||
return TokenMatchResult::Match(Some(v.clone()));
|
||||
}
|
||||
}
|
||||
Self::Value(values) if values.iter().any(|v| v.eq(input)) => {
|
||||
return TokenMatchResult::Match(None);
|
||||
}
|
||||
Self::Value(_) => {}
|
||||
Self::MultiValue(_) => todo!(),
|
||||
Self::FullString => return TokenMatchResult::Match(Some(input)),
|
||||
Self::SystemRef => return TokenMatchResult::Match(Some(input)),
|
||||
Self::MemberRef => return TokenMatchResult::Match(Some(input)),
|
||||
Self::MemberPrivacyTarget if MEMBER_PRIVACY_TARGETS.contains(&input.trim()) => {
|
||||
return TokenMatchResult::Match(Some(input))
|
||||
Self::FullString(_) => return TokenMatchResult::Match(Some(input.into())),
|
||||
Self::SystemRef(_) => return TokenMatchResult::Match(Some(input.into())),
|
||||
Self::MemberRef(_) => return TokenMatchResult::Match(Some(input.into())),
|
||||
Self::MemberPrivacyTarget(_) if MEMBER_PRIVACY_TARGETS.contains(&input) => {
|
||||
return TokenMatchResult::Match(Some(input.into()))
|
||||
}
|
||||
Self::MemberPrivacyTarget => {}
|
||||
Self::PrivacyLevel if input == "public" || input == "private" => {
|
||||
return TokenMatchResult::Match(Some(input))
|
||||
Self::MemberPrivacyTarget(_) => {}
|
||||
Self::PrivacyLevel(_) if input == "public" || input == "private" => {
|
||||
return TokenMatchResult::Match(Some(input.into()))
|
||||
}
|
||||
Self::PrivacyLevel => {}
|
||||
Self::PrivacyLevel(_) => {}
|
||||
}
|
||||
// note: must not add a _ case to the above match
|
||||
// instead, for conditional matches, also add generic cases with no return
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
use ordermap::OrderMap;
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::{commands::Command, Token};
|
||||
use std::{cmp::Ordering, collections::HashMap};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TreeBranch {
|
||||
pub current_command_key: Option<SmolStr>,
|
||||
/// branches.keys(), but sorted by specificity
|
||||
pub possible_tokens: Vec<Token>,
|
||||
pub branches: HashMap<Token, TreeBranch>,
|
||||
pub branches: OrderMap<Token, TreeBranch>,
|
||||
}
|
||||
|
||||
impl TreeBranch {
|
||||
|
|
@ -20,7 +21,7 @@ impl TreeBranch {
|
|||
current_branch = current_branch.branches.entry(token).or_insert(TreeBranch {
|
||||
current_command_key: None,
|
||||
possible_tokens: vec![],
|
||||
branches: HashMap::new(),
|
||||
branches: OrderMap::new(),
|
||||
})
|
||||
}
|
||||
// when we're out of tokens, add an Empty branch with the callback and no sub-branches
|
||||
|
|
@ -29,31 +30,8 @@ impl TreeBranch {
|
|||
TreeBranch {
|
||||
current_command_key: Some(command.cb),
|
||||
possible_tokens: vec![],
|
||||
branches: HashMap::new(),
|
||||
branches: OrderMap::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn sort_tokens(&mut self) {
|
||||
for branch in self.branches.values_mut() {
|
||||
branch.sort_tokens();
|
||||
}
|
||||
|
||||
// put Value tokens at the end
|
||||
// i forget exactly how this works
|
||||
// todo!: document this before PR mergs
|
||||
self.possible_tokens = self
|
||||
.branches
|
||||
.keys()
|
||||
.into_iter()
|
||||
.map(|v| v.clone())
|
||||
.collect();
|
||||
self.possible_tokens.sort_by(|v, _| {
|
||||
if matches!(v, Token::Value(_)) {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Less
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue