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:
dusk 2025-01-05 13:00:06 +09:00
parent 1a781014bd
commit eec9f64026
No known key found for this signature in database
16 changed files with 358 additions and 502 deletions

View file

@ -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;
};

View file

@ -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"

View file

@ -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"
),

View file

@ -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 => {}
}
}

View file

@ -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

View file

@ -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
}
});
}
}