mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-15 10:10:12 +00:00
feat: better parameters handling, implement multi-token matching
This commit is contained in:
parent
b29c51f103
commit
482c923507
14 changed files with 521 additions and 251 deletions
|
|
@ -58,6 +58,7 @@ pub fn all() -> Vec<Command> {
|
|||
(help::cmds())
|
||||
.chain(system::cmds())
|
||||
.chain(member::cmds())
|
||||
.chain(config::cmds())
|
||||
.chain(fun::cmds())
|
||||
.collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,16 +7,14 @@ interface CommandResult {
|
|||
Err(string error);
|
||||
};
|
||||
[Enum]
|
||||
interface ParameterKind {
|
||||
MemberRef();
|
||||
SystemRef();
|
||||
MemberPrivacyTarget();
|
||||
PrivacyLevel();
|
||||
OpaqueString();
|
||||
};
|
||||
dictionary Parameter {
|
||||
string raw;
|
||||
ParameterKind kind;
|
||||
interface Parameter {
|
||||
MemberRef(string member);
|
||||
SystemRef(string system);
|
||||
MemberPrivacyTarget(string target);
|
||||
PrivacyLevel(string level);
|
||||
OpaqueString(string raw);
|
||||
Toggle(boolean toggle);
|
||||
Reset();
|
||||
};
|
||||
dictionary ParsedCommand {
|
||||
string command_ref;
|
||||
|
|
|
|||
|
|
@ -1 +1,32 @@
|
|||
use super::*;
|
||||
|
||||
pub fn cmds() -> impl Iterator<Item = Command> {
|
||||
use Token::*;
|
||||
|
||||
let cfg = ["config", "cfg"];
|
||||
let autoproxy = ["autoproxy", "ap"];
|
||||
|
||||
[
|
||||
command!(
|
||||
[cfg, autoproxy, ["account", "ac"]],
|
||||
"cfg_ap_account_show",
|
||||
"Shows autoproxy status for the account"
|
||||
),
|
||||
command!(
|
||||
[cfg, autoproxy, ["account", "ac"], Toggle("toggle")],
|
||||
"cfg_ap_account_update",
|
||||
"Toggles autoproxy for the account"
|
||||
),
|
||||
command!(
|
||||
[cfg, autoproxy, ["timeout", "tm"]],
|
||||
"cfg_ap_timeout_show",
|
||||
"Shows the autoproxy timeout"
|
||||
),
|
||||
command!(
|
||||
[cfg, autoproxy, ["timeout", "tm"], [Toggle("toggle"), Reset("reset"), FullString("timeout")]],
|
||||
"cfg_ap_timeout_update",
|
||||
"Sets the autoproxy timeout"
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ pub fn cmds() -> impl Iterator<Item = Command> {
|
|||
"member_show",
|
||||
"Shows information about a member"
|
||||
),
|
||||
command!(
|
||||
[member, MemberRef("target"), "soulscream"],
|
||||
"member_soulscream",
|
||||
"todo"
|
||||
),
|
||||
command!(
|
||||
[member, MemberRef("target"), description],
|
||||
"member_desc_show",
|
||||
|
|
|
|||
|
|
@ -36,48 +36,15 @@ pub enum CommandResult {
|
|||
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, Clone)]
|
||||
pub enum Parameter {
|
||||
MemberRef { member: String },
|
||||
SystemRef { system: String },
|
||||
MemberPrivacyTarget { target: String },
|
||||
PrivacyLevel { level: String },
|
||||
OpaqueString { raw: String },
|
||||
Toggle { toggle: bool },
|
||||
Reset,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -111,27 +78,17 @@ fn parse_command(input: String) -> CommandResult {
|
|||
Ok((found_token, arg, new_pos)) => {
|
||||
current_pos = new_pos;
|
||||
if let Token::Flag = found_token {
|
||||
flags.insert(arg.unwrap().into(), None);
|
||||
flags.insert(arg.unwrap().raw.into(), None);
|
||||
// don't try matching flags as tree elements
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
if let Some((param_name, param)) = arg.param.as_ref() {
|
||||
params.insert(param_name.to_string(), param.clone());
|
||||
}
|
||||
args.push(arg.to_string());
|
||||
args.push(arg.raw.to_string());
|
||||
}
|
||||
|
||||
if let Some(next_tree) = local_tree.branches.get(&found_token) {
|
||||
|
|
@ -178,7 +135,7 @@ fn next_token(
|
|||
possible_tokens: Vec<Token>,
|
||||
input: SmolStr,
|
||||
current_pos: usize,
|
||||
) -> Result<(Token, Option<SmolStr>, usize), Option<SmolStr>> {
|
||||
) -> Result<(Token, Option<TokenMatchedValue>, usize), Option<SmolStr>> {
|
||||
// get next parameter, matching quotes
|
||||
let param = crate::string::next_param(input.clone(), current_pos);
|
||||
println!("matched: {param:?}\n---");
|
||||
|
|
@ -191,7 +148,10 @@ fn next_token(
|
|||
{
|
||||
return Ok((
|
||||
Token::Flag,
|
||||
Some(value.trim_start_matches('-').into()),
|
||||
Some(TokenMatchedValue {
|
||||
raw: value,
|
||||
param: None,
|
||||
}),
|
||||
new_pos,
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use smol_str::{SmolStr, ToSmolStr};
|
||||
|
||||
use crate::Parameter;
|
||||
|
||||
type ParamName = &'static str;
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
|
|
@ -8,86 +12,180 @@ pub enum Token {
|
|||
// todo: this is likely not the right way to represent this
|
||||
Empty,
|
||||
|
||||
/// A bot-defined value ("member" in `pk;member MyName`)
|
||||
Value(Vec<SmolStr>),
|
||||
/// A command defined by multiple values
|
||||
// todo!
|
||||
MultiValue(Vec<Vec<SmolStr>>),
|
||||
/// multi-token matching
|
||||
Any(Vec<Token>),
|
||||
|
||||
/// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`)
|
||||
Value(Vec<SmolStr>),
|
||||
|
||||
/// Opaque string (eg. "name" in `pk;member new name`)
|
||||
FullString(ParamName),
|
||||
|
||||
/// Member reference (hid or member name)
|
||||
MemberRef(ParamName),
|
||||
/// todo: doc
|
||||
MemberPrivacyTarget(ParamName),
|
||||
|
||||
/// System reference
|
||||
SystemRef(ParamName),
|
||||
|
||||
/// todo: doc
|
||||
PrivacyLevel(ParamName),
|
||||
|
||||
// currently not included in command definitions
|
||||
/// on, off; yes, no; true, false
|
||||
Toggle(ParamName),
|
||||
|
||||
/// reset, clear, default
|
||||
Reset(ParamName),
|
||||
|
||||
// todo: currently not included in command definitions
|
||||
// todo: flags with values
|
||||
Flag,
|
||||
}
|
||||
|
||||
// #[macro_export]
|
||||
// macro_rules! any {
|
||||
// ($($token:expr),+) => {
|
||||
// Token::Any(vec![$($token.to_token()),+])
|
||||
// };
|
||||
// }
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TokenMatchResult {
|
||||
/// Token did not match.
|
||||
NoMatch,
|
||||
/// Token matched, optionally with a value.
|
||||
Match(Option<SmolStr>),
|
||||
MissingParameter {
|
||||
name: ParamName,
|
||||
},
|
||||
Match(Option<TokenMatchedValue>),
|
||||
/// A required parameter was missing.
|
||||
MissingParameter { name: ParamName },
|
||||
}
|
||||
|
||||
// move this somewhere else
|
||||
const MEMBER_PRIVACY_TARGETS: &[&str] = &["visibility", "name", "todo"];
|
||||
#[derive(Debug)]
|
||||
pub struct TokenMatchedValue {
|
||||
pub raw: SmolStr,
|
||||
pub param: Option<(ParamName, Parameter)>,
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn try_match(&self, input: Option<SmolStr>) -> TokenMatchResult {
|
||||
// short circuit on empty things
|
||||
if matches!(self, Self::Empty) && input.is_none() {
|
||||
return TokenMatchResult::Match(None);
|
||||
} else if input.is_none() {
|
||||
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,
|
||||
}
|
||||
}
|
||||
impl TokenMatchResult {
|
||||
fn new_match(raw: impl Into<SmolStr>) -> Self {
|
||||
Self::Match(Some(TokenMatchedValue {
|
||||
raw: raw.into(),
|
||||
param: None,
|
||||
}))
|
||||
}
|
||||
|
||||
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) if values.iter().any(|v| v.eq(input)) => {
|
||||
return TokenMatchResult::Match(None);
|
||||
}
|
||||
Self::Value(_) => {}
|
||||
Self::MultiValue(_) => todo!(),
|
||||
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.into()))
|
||||
}
|
||||
Self::PrivacyLevel(_) => {}
|
||||
}
|
||||
// note: must not add a _ case to the above match
|
||||
// instead, for conditional matches, also add generic cases with no return
|
||||
|
||||
return TokenMatchResult::NoMatch;
|
||||
fn new_match_param(raw: impl Into<SmolStr>, param_name: ParamName, param: Parameter) -> Self {
|
||||
Self::Match(Some(TokenMatchedValue {
|
||||
raw: raw.into(),
|
||||
param: Some((param_name, param)),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn try_match(&self, input: Option<SmolStr>) -> TokenMatchResult {
|
||||
use TokenMatchResult::*;
|
||||
|
||||
let input = match input {
|
||||
Some(input) => input,
|
||||
None => {
|
||||
// short circuit on:
|
||||
return match self {
|
||||
// empty token
|
||||
Self::Empty => Match(None),
|
||||
// missing paramaters
|
||||
Self::FullString(param_name)
|
||||
| Self::MemberRef(param_name)
|
||||
| Self::MemberPrivacyTarget(param_name)
|
||||
| Self::SystemRef(param_name)
|
||||
| Self::PrivacyLevel(param_name)
|
||||
| Self::Toggle(param_name)
|
||||
| Self::Reset(param_name) => MissingParameter { name: param_name },
|
||||
Self::Any(tokens) => tokens.is_empty().then_some(NoMatch).unwrap_or_else(|| {
|
||||
let mut results = tokens.iter().map(|t| t.try_match(None));
|
||||
results.find(|r| !matches!(r, NoMatch)).unwrap_or(NoMatch)
|
||||
}),
|
||||
// everything else doesnt match if no input anyway
|
||||
Token::Value(_) => NoMatch,
|
||||
Token::Flag => NoMatch,
|
||||
// don't add a _ match here!
|
||||
};
|
||||
}
|
||||
};
|
||||
let input = input.trim();
|
||||
|
||||
// try actually matching stuff
|
||||
match self {
|
||||
Self::Empty => NoMatch,
|
||||
Self::Flag => unreachable!(), // matched upstream (dusk: i don't really like this tbh)
|
||||
Self::Any(tokens) => tokens
|
||||
.iter()
|
||||
.map(|t| t.try_match(Some(input.into())))
|
||||
.find(|r| !matches!(r, NoMatch))
|
||||
.unwrap_or(NoMatch),
|
||||
Self::Value(values) => values
|
||||
.iter()
|
||||
.any(|v| v.eq(input))
|
||||
.then(|| TokenMatchResult::new_match(input))
|
||||
.unwrap_or(NoMatch),
|
||||
Self::FullString(param_name) => TokenMatchResult::new_match_param(
|
||||
input,
|
||||
param_name,
|
||||
Parameter::OpaqueString { raw: input.into() },
|
||||
),
|
||||
Self::SystemRef(param_name) => TokenMatchResult::new_match_param(
|
||||
input,
|
||||
param_name,
|
||||
Parameter::SystemRef {
|
||||
system: input.into(),
|
||||
},
|
||||
),
|
||||
Self::MemberRef(param_name) => TokenMatchResult::new_match_param(
|
||||
input,
|
||||
param_name,
|
||||
Parameter::MemberRef {
|
||||
member: input.into(),
|
||||
},
|
||||
),
|
||||
Self::MemberPrivacyTarget(param_name) => match MemberPrivacyTarget::from_str(input) {
|
||||
Ok(target) => TokenMatchResult::new_match_param(
|
||||
input,
|
||||
param_name,
|
||||
Parameter::MemberPrivacyTarget {
|
||||
target: target.as_ref().into(),
|
||||
},
|
||||
),
|
||||
Err(_) => NoMatch,
|
||||
},
|
||||
Self::PrivacyLevel(param_name) => match PrivacyLevel::from_str(input) {
|
||||
Ok(level) => TokenMatchResult::new_match_param(
|
||||
input,
|
||||
param_name,
|
||||
Parameter::PrivacyLevel {
|
||||
level: level.as_ref().into(),
|
||||
},
|
||||
),
|
||||
Err(_) => NoMatch,
|
||||
},
|
||||
|
||||
Self::Toggle(param_name) => match Toggle::from_str(input) {
|
||||
Ok(t) => TokenMatchResult::new_match_param(
|
||||
input,
|
||||
param_name,
|
||||
Parameter::Toggle { toggle: t.0 },
|
||||
),
|
||||
Err(_) => NoMatch,
|
||||
},
|
||||
Self::Reset(param_name) => match Reset::from_str(input) {
|
||||
Ok(_) => TokenMatchResult::new_match_param(input, param_name, Parameter::Reset),
|
||||
Err(_) => NoMatch,
|
||||
},
|
||||
// don't add a _ match here!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience trait to convert types into [`Token`]s.
|
||||
pub trait ToToken {
|
||||
fn to_token(&self) -> Token;
|
||||
}
|
||||
|
|
@ -109,3 +207,107 @@ impl ToToken for [&str] {
|
|||
Token::Value(self.into_iter().map(|s| s.to_smolstr()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToToken for [Token] {
|
||||
fn to_token(&self) -> Token {
|
||||
Token::Any(self.into_iter().map(|s| s.clone()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub enum MemberPrivacyTarget {
|
||||
Visibility,
|
||||
Name,
|
||||
// todo
|
||||
}
|
||||
|
||||
impl AsRef<str> for MemberPrivacyTarget {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::Visibility => "visibility",
|
||||
Self::Name => "name",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MemberPrivacyTarget {
|
||||
// todo: figure out how to represent these errors best
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"visibility" => Ok(Self::Visibility),
|
||||
"name" => Ok(Self::Name),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub enum PrivacyLevel {
|
||||
Public,
|
||||
Private,
|
||||
}
|
||||
|
||||
impl AsRef<str> for PrivacyLevel {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::Public => "public",
|
||||
Self::Private => "private",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PrivacyLevel {
|
||||
type Err = (); // todo
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"public" => Ok(Self::Public),
|
||||
"private" => Ok(Self::Private),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct Toggle(bool);
|
||||
|
||||
impl AsRef<str> for Toggle {
|
||||
fn as_ref(&self) -> &str {
|
||||
// on / off better than others for docs and stuff?
|
||||
self.0.then_some("on").unwrap_or("off")
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Toggle {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"on" | "yes" | "true" | "enable" | "enabled" => Ok(Self(true)),
|
||||
"off" | "no" | "false" | "disable" | "disabled" => Ok(Self(false)),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct Reset;
|
||||
|
||||
impl AsRef<str> for Reset {
|
||||
fn as_ref(&self) -> &str {
|
||||
"reset"
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Reset {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"reset" | "clear" | "default" => Ok(Self),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue