From 07e8a4851a03d639362be2c7fa238ac552af4398 Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 21 Jan 2025 12:36:54 +0900 Subject: [PATCH] feat(commands): add cs codegen to statically use params and flags in bot code, remove Any --- .gitignore | 1 + PluralKit.Bot/CommandMeta/CommandTree.cs | 30 +-- PluralKit.Bot/Commands/Config.cs | 54 +++-- PluralKit.Bot/Commands/Member.cs | 10 +- PluralKit.Bot/Handlers/MessageCreated.cs | 14 +- crates/command_definitions/src/config.rs | 36 ++- crates/command_definitions/src/fun.rs | 4 +- crates/command_definitions/src/help.rs | 6 +- crates/command_definitions/src/lib.rs | 4 +- crates/command_definitions/src/member.rs | 41 ++-- crates/command_definitions/src/system.rs | 10 +- crates/command_parser/src/command.rs | 28 ++- crates/command_parser/src/flag.rs | 16 +- crates/command_parser/src/lib.rs | 22 +- crates/command_parser/src/parameter.rs | 293 ++++++++--------------- crates/command_parser/src/token.rs | 105 ++------ crates/command_parser/src/tree.rs | 12 +- crates/commands/Cargo.toml | 5 + crates/commands/src/main.rs | 21 ++ flake.nix | 2 + 20 files changed, 297 insertions(+), 417 deletions(-) diff --git a/.gitignore b/.gitignore index a4f9031e..31332235 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ logs/ recipe.json .docker-bin/ PluralKit.Bot/commands.cs +PluralKit.Bot/commandtypes.cs # nix .nix-process-compose diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index edbc8428..71b6fd71 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -4,24 +4,26 @@ namespace PluralKit.Bot; public partial class CommandTree { - public Task ExecuteCommand(Context ctx) + public Task ExecuteCommand(Context ctx, Commands command) { - return ctx.Parameters.Callback() switch + return command switch { - "help" => ctx.Execute(Help, m => m.HelpRoot(ctx)), - "help_commands" => ctx.Reply( + Commands.Help => ctx.Execute(Help, m => m.HelpRoot(ctx)), + Commands.HelpCommands => ctx.Reply( "For the list of commands, see the website: "), - "help_proxy" => ctx.Reply( + Commands.HelpProxy => ctx.Reply( "The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"), - "member_show" => ctx.Execute(MemberInfo, m => m.ViewMember(ctx)), - "member_new" => ctx.Execute(MemberNew, m => m.NewMember(ctx)), - "member_soulscream" => ctx.Execute(MemberInfo, m => m.Soulscream(ctx)), - "cfg_ap_account_show" => ctx.Execute(null, m => m.ViewAutoproxyAccount(ctx)), - "cfg_ap_account_update" => ctx.Execute(null, m => m.EditAutoproxyAccount(ctx)), - "cfg_ap_timeout_show" => ctx.Execute(null, m => m.ViewAutoproxyTimeout(ctx)), - "cfg_ap_timeout_update" => ctx.Execute(null, m => m.EditAutoproxyTimeout(ctx)), - "fun_thunder" => ctx.Execute(null, m => m.Thunder(ctx)), - "fun_meow" => ctx.Execute(null, m => m.Meow(ctx)), + Commands.MemberShow(MemberShowParams param, _) => ctx.Execute(MemberInfo, m => m.ViewMember(ctx, param.target)), + Commands.MemberNew(MemberNewParams param, _) => ctx.Execute(MemberNew, m => m.NewMember(ctx, param.name)), + Commands.MemberSoulscream(MemberSoulscreamParams param, _) => ctx.Execute(MemberInfo, m => m.Soulscream(ctx, param.target)), + Commands.CfgApAccountShow => ctx.Execute(null, m => m.ViewAutoproxyAccount(ctx)), + Commands.CfgApAccountUpdate(CfgApAccountUpdateParams param, _) => ctx.Execute(null, m => m.EditAutoproxyAccount(ctx, param.toggle)), + Commands.CfgApTimeoutShow => ctx.Execute(null, m => m.ViewAutoproxyTimeout(ctx)), + Commands.CfgApTimeoutOff => ctx.Execute(null, m => m.DisableAutoproxyTimeout(ctx)), + Commands.CfgApTimeoutReset => ctx.Execute(null, m => m.ResetAutoproxyTimeout(ctx)), + Commands.CfgApTimeoutUpdate(CfgApTimeoutUpdateParams param, _) => ctx.Execute(null, m => m.EditAutoproxyTimeout(ctx, param.timeout)), + Commands.FunThunder => ctx.Execute(null, m => m.Thunder(ctx)), + Commands.FunMeow => ctx.Execute(null, m => m.Meow(ctx)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( diff --git a/PluralKit.Bot/Commands/Config.cs b/PluralKit.Bot/Commands/Config.cs index 4acd00dc..d4267d5b 100644 --- a/PluralKit.Bot/Commands/Config.cs +++ b/PluralKit.Bot/Commands/Config.cs @@ -197,10 +197,9 @@ public class Config await ctx.Reply($"Autoproxy is currently **{EnabledDisabled(allowAutoproxy)}** for account <@{ctx.Author.Id}>."); } - public async Task EditAutoproxyAccount(Context ctx) + public async Task EditAutoproxyAccount(Context ctx, bool allow) { var allowAutoproxy = await ctx.Repository.GetAutoproxyEnabled(ctx.Author.Id); - var allow = await ctx.ParamResolveToggle("toggle") ?? throw new PKSyntaxError("You need to specify whether to enable or disable autoproxy for this account."); var statusString = EnabledDisabled(allow); if (allowAutoproxy == allow) @@ -227,41 +226,44 @@ public class Config await ctx.Reply($"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize(4)}."); } - public async Task EditAutoproxyTimeout(Context ctx) + public async Task DisableAutoproxyTimeout(Context ctx) { - var _newTimeout = await ctx.ParamResolveOpaque("timeout"); - var _reset = await ctx.ParamResolveToggle("reset"); - var _toggle = await ctx.ParamResolveToggle("toggle"); + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = (int)Duration.Zero.TotalSeconds }); - Duration? newTimeout; + await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out."); + } + + public async Task ResetAutoproxyTimeout(Context ctx) + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = null }); + + await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)})."); + } + + public async Task EditAutoproxyTimeout(Context ctx, string timeout) + { + Duration newTimeout; Duration overflow = Duration.Zero; - if (_toggle == false) newTimeout = Duration.Zero; - else if (_reset == true) newTimeout = null; - else + // todo: we should parse date in the command parser + var timeoutStr = timeout; + var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr) + ?? throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes)."); + if (timeoutPeriod.TotalHours > 100000) { - // todo: we should parse date in the command parser - var timeoutStr = _newTimeout; - var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr); - if (timeoutPeriod == null) throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes)."); - if (timeoutPeriod.Value.TotalHours > 100000) - { - // sanity check to prevent seconds overflow if someone types in 999999999 - overflow = timeoutPeriod.Value; - newTimeout = Duration.Zero; - } - else newTimeout = timeoutPeriod; + // sanity check to prevent seconds overflow if someone types in 999999999 + overflow = timeoutPeriod; + newTimeout = Duration.Zero; } + else newTimeout = timeoutPeriod; - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = (int?)newTimeout?.TotalSeconds }); + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = (int?)newTimeout.TotalSeconds }); - if (newTimeout == null) - await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)})."); - else if (newTimeout == Duration.Zero && overflow != Duration.Zero) + if (newTimeout == Duration.Zero && overflow != Duration.Zero) await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out. ({overflow.ToTimeSpan().Humanize(4)} is too long)"); else if (newTimeout == Duration.Zero) await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out."); else - await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.Value!.ToTimeSpan().Humanize(4)}."); + await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.ToTimeSpan().Humanize(4)}."); } public async Task SystemTimezone(Context ctx) diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index a8fd478f..a3fbefbf 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -28,10 +28,8 @@ public class Member _avatarHosting = avatarHosting; } - public async Task NewMember(Context ctx) + public async Task NewMember(Context ctx, string? memberName) { - var memberName = await ctx.ParamResolveOpaque("name"); - if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix); memberName = memberName ?? throw new PKSyntaxError("You must pass a member name."); @@ -122,19 +120,17 @@ public class Member await ctx.Reply(replyStr); } - public async Task ViewMember(Context ctx) + public async Task ViewMember(Context ctx, PKMember target) { - var target = await ctx.ParamResolveMember("target"); var system = await ctx.Repository.GetSystem(target.System); await ctx.Reply( embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone)); } - public async Task Soulscream(Context ctx) + public async Task Soulscream(Context ctx, PKMember target) { // this is for a meme, please don't take this code seriously. :) - var target = await ctx.ParamResolveMember("target"); var name = target.NameFor(ctx.LookupContextFor(target.System)); var encoded = HttpUtility.UrlEncode(name); diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 594b178a..cd16d6c9 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -159,7 +159,19 @@ public class MessageCreated: IEventHandler } var ctx = new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes, parameters); - await _tree.ExecuteCommand(ctx); + + Commands command; + try + { + command = await Commands.FromContext(ctx); + } + catch (PKError e) + { + await ctx.Reply($"{Emojis.Error} {e.Message}"); + throw; + } + + await _tree.ExecuteCommand(ctx, command); } catch (PKError) { diff --git a/crates/command_definitions/src/config.rs b/crates/command_definitions/src/config.rs index b2f5b80d..ab185663 100644 --- a/crates/command_definitions/src/config.rs +++ b/crates/command_definitions/src/config.rs @@ -1,29 +1,25 @@ +use command_parser::parameter; + use super::*; pub fn cmds() -> impl Iterator { - let cfg = ["config", "cfg"]; - let autoproxy = ["autoproxy", "ap"]; + let ap = tokens!(["config", "cfg"], ["autoproxy", "ap"]); + + let ap_account = concat_tokens!(ap, [["account", "ac"]]); + let ap_timeout = concat_tokens!(ap, [["timeout", "tm"]]); [ - command!([cfg, autoproxy, ["account", "ac"]], "cfg_ap_account_show") + command!(ap_account => "cfg_ap_account_show") .help("Shows autoproxy status for the account"), - command!( - [cfg, autoproxy, ["account", "ac"], Toggle], - "cfg_ap_account_update" - ) - .help("Toggles autoproxy for the account"), - command!([cfg, autoproxy, ["timeout", "tm"]], "cfg_ap_timeout_show") - .help("Shows the autoproxy timeout"), - command!( - [ - cfg, - autoproxy, - ["timeout", "tm"], - any!(Disable, Reset, ("timeout", OpaqueString::SINGLE)) // todo: we should parse duration / time values - ], - "cfg_ap_timeout_update" - ) - .help("Sets the autoproxy timeout"), + command!(ap_account, Toggle => "cfg_ap_account_update") + .help("Toggles autoproxy for the account"), + command!(ap_timeout => "cfg_ap_timeout_show").help("Shows the autoproxy timeout"), + command!(ap_timeout, parameter::RESET => "cfg_ap_timeout_reset") + .help("Resets the autoproxy timeout"), + command!(ap_timeout, parameter::DISABLE => "cfg_ap_timeout_off") + .help("Disables the autoproxy timeout"), + command!(ap_timeout, ("timeout", OpaqueString) => "cfg_ap_timeout_update") + .help("Sets the autoproxy timeout"), ] .into_iter() } diff --git a/crates/command_definitions/src/fun.rs b/crates/command_definitions/src/fun.rs index 720eadc0..63ec0054 100644 --- a/crates/command_definitions/src/fun.rs +++ b/crates/command_definitions/src/fun.rs @@ -2,8 +2,8 @@ use super::*; pub fn cmds() -> impl Iterator { [ - command!(["thunder"], "fun_thunder"), - command!(["meow"], "fun_meow"), + command!(["thunder"] => "fun_thunder"), + command!(["meow"] => "fun_meow"), ] .into_iter() } diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index 411b3565..2719fa9d 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -3,9 +3,9 @@ use super::*; pub fn cmds() -> impl Iterator { let help = ["help", "h"]; [ - command!([help], "help").help("Shows the help command"), - command!([help, "commands"], "help_commands").help("help commands"), - command!([help, "proxy"], "help_proxy").help("help proxy"), + command!([help] => "help").help("Shows the help command"), + command!([help, "commands"] => "help_commands").help("help commands"), + command!([help, "proxy"] => "help_proxy").help("help proxy"), ] .into_iter() } diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 9e8adb49..96df8091 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -18,7 +18,9 @@ pub mod server_config; pub mod switch; pub mod system; -use command_parser::{any, command, command::Command, parameter::*}; +use command_parser::{ + command, command::Command, concat_tokens, parameter::ParameterKind::*, tokens, +}; pub fn all() -> impl Iterator { (help::cmds()) diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 5346bd53..5ab968b9 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -6,38 +6,27 @@ pub fn cmds() -> impl Iterator { let privacy = ["privacy", "priv"]; let new = ["new", "n"]; + let member_target = tokens!(member, MemberRef); + let member_desc = concat_tokens!(member_target, [description]); + let member_privacy = concat_tokens!(member_target, [privacy]); + [ - command!([member, new, ("name", OpaqueString::SINGLE)], "member_new") + command!([member, new, ("name", OpaqueString)] => "member_new") .help("Creates a new system member"), - command!([member, MemberRef], "member_show") - .help("Shows information about a member") - .value_flag("pt", Disable), - command!([member, MemberRef, description], "member_desc_show") - .help("Shows a member's description"), - command!( - [ - member, - MemberRef, - description, - ("description", OpaqueString::REMAINDER) - ], - "member_desc_update" - ) - .help("Changes a member's description"), - command!([member, MemberRef, privacy], "member_privacy_show") + command!(member_target => "member_show") + .flag("pt") + .help("Shows information about a member"), + command!(member_desc => "member_desc_show").help("Shows a member's description"), + command!(member_desc, ("description", OpaqueStringRemainder) => "member_desc_update") + .help("Changes a member's description"), + command!(member_privacy => "member_privacy_show") .help("Displays a member's current privacy settings"), command!( - [ - member, - MemberRef, - privacy, - MemberPrivacyTarget, - ("new_privacy_level", PrivacyLevel) - ], - "member_privacy_update" + member_privacy, MemberPrivacyTarget, ("new_privacy_level", PrivacyLevel) + => "member_privacy_update" ) .help("Changes a member's privacy settings"), - command!([member, MemberRef, "soulscream"], "member_soulscream").show_in_suggestions(false), + command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false), ] .into_iter() } diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index fd2148a3..6093f6f1 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -4,11 +4,13 @@ pub fn cmds() -> impl Iterator { let system = ["system", "s"]; let new = ["new", "n"]; + let system_new = tokens!(system, new); + [ - command!([system], "system_show").help("Shows information about your system"), - command!([system, new], "system_new").help("Creates a new system"), - command!([system, new, ("name", OpaqueString::SINGLE)], "system_new") - .help("Creates a new system"), + command!([system] => "system_show").help("Shows information about your system"), + command!(system_new => "system_new").help("Creates a new system"), + command!(system_new, ("name", OpaqueString) => "system_new_name") + .help("Creates a new system (using the provided name)"), ] .into_iter() } diff --git a/crates/command_parser/src/command.rs b/crates/command_parser/src/command.rs index 0410834b..35bd9b10 100644 --- a/crates/command_parser/src/command.rs +++ b/crates/command_parser/src/command.rs @@ -26,7 +26,7 @@ impl Command { for (idx, token) in tokens.iter().enumerate().rev() { match token { // we want flags to go before any parameters - Token::Parameter(_, _) | Token::Any(_) => { + Token::Parameter(_) => { parse_flags_before = idx; was_parameter = true; } @@ -62,7 +62,7 @@ impl Command { self } - pub fn value_flag(mut self, name: impl Into, value: impl Parameter + 'static) -> Self { + pub fn value_flag(mut self, name: impl Into, value: ParameterKind) -> Self { self.flags.push(Flag::new(name).with_value(value)); self } @@ -95,7 +95,27 @@ impl Display for Command { // (and something like &dyn Trait would require everything to be referenced which doesnt look nice anyway) #[macro_export] macro_rules! command { - ([$($v:expr),+], $cb:expr$(,)*) => { - $crate::command::Command::new([$($crate::token::Token::from($v)),*], $cb) + ([$($v:expr),+] => $cb:expr$(,)*) => { + $crate::command::Command::new($crate::tokens!($($v),+), $cb) + }; + ($tokens:expr => $cb:expr$(,)*) => { + $crate::command::Command::new($tokens.clone(), $cb) + }; + ($tokens:expr, $($v:expr),+ => $cb:expr$(,)*) => { + $crate::command::Command::new($crate::concat_tokens!($tokens.clone(), [$($v),+]), $cb) + }; +} + +#[macro_export] +macro_rules! tokens { + ($($v:expr),+$(,)*) => { + [$($crate::token::Token::from($v)),+] + }; +} + +#[macro_export] +macro_rules! concat_tokens { + ($tokens:expr, [$($v:expr),+]$(,)*) => { + $tokens.clone().into_iter().chain($crate::tokens!($($v),+).into_iter()).collect::>() }; } diff --git a/crates/command_parser/src/flag.rs b/crates/command_parser/src/flag.rs index 6bd12f2b..e0ff236c 100644 --- a/crates/command_parser/src/flag.rs +++ b/crates/command_parser/src/flag.rs @@ -1,8 +1,8 @@ -use std::{fmt::Display, sync::Arc}; +use std::fmt::Display; use smol_str::SmolStr; -use crate::parameter::{Parameter, ParameterValue}; +use crate::parameter::{ParameterKind, ParameterValue}; #[derive(Debug)] pub enum FlagValueMatchError { @@ -13,7 +13,7 @@ pub enum FlagValueMatchError { #[derive(Debug, Clone)] pub struct Flag { name: SmolStr, - value: Option>, + value: Option, } impl Display for Flag { @@ -42,8 +42,8 @@ impl Flag { } } - pub fn with_value(mut self, param: impl Parameter + 'static) -> Self { - self.value = Some(Arc::new(param)); + pub fn with_value(mut self, param: ParameterKind) -> Self { + self.value = Some(param); self } @@ -51,13 +51,17 @@ impl Flag { &self.name } + pub fn value_kind(&self) -> Option { + self.value + } + pub fn try_match(&self, input_name: &str, input_value: Option<&str>) -> TryMatchFlagResult { // if not matching flag then skip anymore matching if self.name != input_name { return None; } // get token to try matching with, if flag doesn't have one then that means it is matched (it is without any value) - let Some(value) = self.value.as_deref() else { + let Some(value) = self.value.as_ref() else { return Some(Ok(None)); }; // check if we have a non-empty flag value, we return error if not (because flag requested a value) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 5da79c36..d152a581 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -77,21 +77,6 @@ pub fn parse_command( 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}.") } @@ -254,12 +239,9 @@ fn next_token<'a>( // 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()); + |token: &Token| matches!(token, Token::Parameter(param) if param.kind().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)); + let match_remaining = is_match_remaining_token(token); // either use matched param or rest of the input if matching remaining let input_to_match = matched.as_ref().map(|v| { match_remaining diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index ff67ee7e..c1ad77b7 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -2,89 +2,108 @@ use std::{fmt::Debug, str::FromStr}; use smol_str::SmolStr; -use crate::token::ParamName; - #[derive(Debug, Clone)] pub enum ParameterValue { + OpaqueString(String), MemberRef(String), SystemRef(String), MemberPrivacyTarget(String), PrivacyLevel(String), - OpaqueString(String), Toggle(bool), } -pub trait Parameter: Debug + Send + Sync { - fn remainder(&self) -> bool { - false - } - 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; +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Parameter { + name: SmolStr, + kind: ParameterKind, } -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct OpaqueString(bool); - -impl OpaqueString { - pub const SINGLE: Self = Self(false); - pub const REMAINDER: Self = Self(true); -} - -impl Parameter for OpaqueString { - fn remainder(&self) -> bool { - self.0 +impl Parameter { + pub fn name(&self) -> &str { + &self.name } - fn default_name(&self) -> ParamName { - "string" - } - - fn format(&self, f: &mut std::fmt::Formatter, name: &str) -> std::fmt::Result { - write!(f, "[{name}]") - } - - fn match_value(&self, input: &str) -> Result { - Ok(ParameterValue::OpaqueString(input.into())) + pub fn kind(&self) -> ParameterKind { + self.kind } } -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct MemberRef; - -impl Parameter for MemberRef { - fn default_name(&self) -> ParamName { - "member" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "") - } - - fn match_value(&self, input: &str) -> Result { - Ok(ParameterValue::MemberRef(input.into())) +impl From for Parameter { + fn from(value: ParameterKind) -> Self { + Parameter { + name: value.default_name().into(), + kind: value, + } } } -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct SystemRef; - -impl Parameter for SystemRef { - fn default_name(&self) -> ParamName { - "system" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "") - } - - fn match_value(&self, input: &str) -> Result { - Ok(ParameterValue::SystemRef(input.into())) +impl From<(&str, ParameterKind)> for Parameter { + fn from((name, kind): (&str, ParameterKind)) -> Self { + Parameter { + name: name.into(), + kind, + } } } -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct MemberPrivacyTarget; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ParameterKind { + OpaqueString, + OpaqueStringRemainder, + MemberRef, + SystemRef, + MemberPrivacyTarget, + PrivacyLevel, + Toggle, +} + +impl ParameterKind { + pub(crate) fn default_name(&self) -> &str { + match self { + ParameterKind::OpaqueString => "string", + ParameterKind::OpaqueStringRemainder => "string", + ParameterKind::MemberRef => "target", + ParameterKind::SystemRef => "target", + ParameterKind::MemberPrivacyTarget => "member_privacy_target", + ParameterKind::PrivacyLevel => "privacy_level", + ParameterKind::Toggle => "toggle", + } + } + + pub(crate) fn remainder(&self) -> bool { + matches!(self, ParameterKind::OpaqueStringRemainder) + } + + pub(crate) fn format(&self, f: &mut std::fmt::Formatter, param_name: &str) -> std::fmt::Result { + match self { + ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { + write!(f, "[{param_name}]") + } + ParameterKind::MemberRef => write!(f, ""), + ParameterKind::SystemRef => write!(f, ""), + ParameterKind::MemberPrivacyTarget => write!(f, ""), + ParameterKind::PrivacyLevel => write!(f, "[privacy level]"), + ParameterKind::Toggle => write!(f, "on/off"), + } + } + + pub(crate) fn match_value(&self, input: &str) -> Result { + match self { + ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { + Ok(ParameterValue::OpaqueString(input.into())) + } + ParameterKind::MemberRef => Ok(ParameterValue::MemberRef(input.into())), + ParameterKind::SystemRef => Ok(ParameterValue::SystemRef(input.into())), + ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input) + .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())), + ParameterKind::PrivacyLevel => PrivacyLevelKind::from_str(input) + .map(|level| ParameterValue::PrivacyLevel(level.as_ref().into())), + ParameterKind::Toggle => { + Toggle::from_str(input).map(|t| ParameterValue::Toggle(t.into())) + } + } + } +} pub enum MemberPrivacyTargetKind { Visibility, @@ -135,24 +154,6 @@ impl FromStr for MemberPrivacyTargetKind { } } -impl Parameter for MemberPrivacyTarget { - fn default_name(&self) -> ParamName { - "member_privacy_target" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "") - } - - fn match_value(&self, input: &str) -> Result { - MemberPrivacyTargetKind::from_str(input) - .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())) - } -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct PrivacyLevel; - pub enum PrivacyLevelKind { Public, Private, @@ -179,140 +180,34 @@ impl FromStr for PrivacyLevelKind { } } -impl Parameter for PrivacyLevel { - fn default_name(&self) -> ParamName { - "privacy_level" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "[privacy level]") - } - - fn match_value(&self, input: &str) -> Result { - PrivacyLevelKind::from_str(input) - .map(|level| ParameterValue::PrivacyLevel(level.as_ref().into())) - } -} +pub const ENABLE: [&str; 5] = ["on", "yes", "true", "enable", "enabled"]; +pub const DISABLE: [&str; 5] = ["off", "no", "false", "disable", "disabled"]; #[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Reset; - -impl AsRef for Reset { - fn as_ref(&self) -> &str { - "reset" - } +pub enum Toggle { + On, + Off, } -impl FromStr for Reset { +impl FromStr for Toggle { type Err = SmolStr; fn from_str(s: &str) -> Result { match s { - "reset" | "clear" | "default" => Ok(Self), - _ => Err("not reset".into()), + ref s if ENABLE.contains(s) => Ok(Self::On), + ref s if DISABLE.contains(s) => Ok(Self::Off), + _ => Err("invalid toggle, must be on/off".into()), } } } -impl Parameter for Reset { - fn default_name(&self) -> ParamName { - "reset" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "reset") - } - - fn match_value(&self, input: &str) -> Result { - Self::from_str(input).map(|_| ParameterValue::Toggle(true)) - } -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Toggle; - -impl Parameter for Toggle { - fn default_name(&self) -> ParamName { - "toggle" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "on/off") - } - - fn match_value(&self, input: &str) -> Result { - Enable::from_str(input) - .map(Into::::into) - .or_else(|_| Disable::from_str(input).map(Into::::into)) - .map(ParameterValue::Toggle) - .map_err(|_| "invalid toggle".into()) - } -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Enable; - -impl FromStr for Enable { - type Err = SmolStr; - - fn from_str(s: &str) -> Result { - match s { - "on" | "yes" | "true" | "enable" | "enabled" => Ok(Self), - _ => Err("invalid enable".into()), - } - } -} - -impl Parameter for Enable { - fn default_name(&self) -> ParamName { - "enable" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "on") - } - - fn match_value(&self, input: &str) -> Result { - Self::from_str(input).map(|e| ParameterValue::Toggle(e.into())) - } -} - -impl Into for Enable { +impl Into for Toggle { fn into(self) -> bool { - true - } -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Disable; - -impl FromStr for Disable { - type Err = SmolStr; - - fn from_str(s: &str) -> Result { - match s { - "off" | "no" | "false" | "disable" | "disabled" => Ok(Self), - _ => Err("invalid disable".into()), + match self { + Toggle::On => true, + Toggle::Off => false, } } } -impl Into for Disable { - fn into(self) -> bool { - false - } -} - -impl Parameter for Disable { - fn default_name(&self) -> ParamName { - "disable" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "off") - } - - fn match_value(&self, input: &str) -> Result { - Self::from_str(input).map(|e| ParameterValue::Toggle(e.into())) - } -} +pub const RESET: [&str; 3] = ["reset", "clear", "default"]; diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index aae2f06e..4d90c2ec 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -1,76 +1,35 @@ use std::{ fmt::{Debug, Display}, - hash::Hash, ops::Not, - sync::Arc, }; use smol_str::SmolStr; -use crate::parameter::{Parameter, ParameterValue}; +use crate::parameter::{Parameter, ParameterKind, ParameterValue}; -pub type ParamName = &'static str; - -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Token { /// Token used to represent a finished command (i.e. no more parameters required) // todo: this is likely not the right way to represent this Empty, - /// multi-token matching - /// todo: FullString tokens don't work properly in this (they don't get passed the rest of the input) - Any(Vec), - /// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`) Value(Vec), /// A parameter that must be provided a value - Parameter(ParamName, Arc), -} - -#[macro_export] -macro_rules! any { - ($($t:expr),+) => { - $crate::token::Token::Any(vec![$($crate::token::Token::from($t)),+]) - }; -} - -impl PartialEq for Token { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Any(l0), Self::Any(r0)) => l0 == r0, - (Self::Value(l0), Self::Value(r0)) => l0 == r0, - (Self::Parameter(l0, _), Self::Parameter(r0, _)) => l0 == r0, - (Self::Empty, Self::Empty) => true, - _ => false, - } - } -} -impl Eq for Token {} - -impl Hash for Token { - fn hash(&self, state: &mut H) { - core::mem::discriminant(self).hash(state); - match self { - Token::Empty => {} - Token::Any(vec) => vec.hash(state), - Token::Value(vec) => vec.hash(state), - Token::Parameter(name, _) => name.hash(state), - } - } + Parameter(Parameter), } #[derive(Debug)] pub enum TokenMatchError { ParameterMatchError { input: SmolStr, msg: SmolStr }, - MissingParameter { name: ParamName }, - MissingAny { tokens: Vec }, + MissingParameter { name: SmolStr }, } #[derive(Debug)] pub(super) struct TokenMatchValue { pub raw: SmolStr, - pub param: Option<(ParamName, ParameterValue)>, + pub param: Option<(SmolStr, ParameterValue)>, } impl TokenMatchValue { @@ -83,12 +42,12 @@ impl TokenMatchValue { fn new_match_param( raw: impl Into, - param_name: ParamName, + param_name: impl Into, param: ParameterValue, ) -> TryMatchResult { Some(Ok(Some(Self { raw: raw.into(), - param: Some((param_name, param)), + param: Some((param_name.into(), param)), }))) } } @@ -113,14 +72,9 @@ impl Token { // empty token Self::Empty => Some(Ok(None)), // missing paramaters - Self::Parameter(name, _) => { - Some(Err(TokenMatchError::MissingParameter { name })) - } - Self::Any(tokens) => tokens.is_empty().then_some(None).unwrap_or_else(|| { - Some(Err(TokenMatchError::MissingAny { - tokens: tokens.clone(), - })) - }), + Self::Parameter(param) => Some(Err(TokenMatchError::MissingParameter { + name: param.name().into(), + })), // everything else doesnt match if no input anyway Self::Value(_) => None, // don't add a _ match here! @@ -132,18 +86,13 @@ impl Token { // try actually matching stuff match self { Self::Empty => None, - Self::Any(tokens) => tokens - .iter() - .map(|t| t.try_match(Some(input))) - .find(|r| !matches!(r, None)) - .unwrap_or(None), Self::Value(values) => values .iter() .any(|v| v.eq(input)) .then(|| TokenMatchValue::new_match(input)) .unwrap_or(None), - Self::Parameter(name, param) => match param.match_value(input) { - Ok(matched) => TokenMatchValue::new_match_param(input, name, matched), + Self::Parameter(param) => match param.kind().match_value(input) { + Ok(matched) => TokenMatchValue::new_match_param(input, param.name(), matched), Err(err) => Some(Err(TokenMatchError::ParameterMatchError { input: input.into(), msg: err, @@ -157,19 +106,9 @@ impl Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Token::Empty => write!(f, ""), - Token::Any(vec) => { - write!(f, "(")?; - for (i, token) in vec.iter().enumerate() { - if i != 0 { - write!(f, "|")?; - } - write!(f, "{}", token)?; - } - write!(f, ")") - } Token::Value(vec) if vec.is_empty().not() => write!(f, "{}", vec.first().unwrap()), Token::Value(_) => Ok(()), // if value token has no values (lol), don't print anything - Token::Parameter(name, param) => param.format(f, name), + Token::Parameter(param) => param.kind().format(f, param.name()), } } } @@ -186,14 +125,20 @@ impl From<[&str; L]> for Token { } } -impl From

for Token { - fn from(value: P) -> Self { - Token::Parameter(value.default_name(), Arc::new(value)) +impl From for Token { + fn from(value: Parameter) -> Self { + Token::Parameter(value) } } -impl From<(ParamName, P)> for Token { - fn from(value: (ParamName, P)) -> Self { - Token::Parameter(value.0, Arc::new(value.1)) +impl From for Token { + fn from(value: ParameterKind) -> Self { + Token::from(Parameter::from(value)) + } +} + +impl From<(&str, ParameterKind)> for Token { + fn from(value: (&str, ParameterKind)) -> Self { + Token::from(Parameter::from(value)) } } diff --git a/crates/command_parser/src/tree.rs b/crates/command_parser/src/tree.rs index 5f78eff6..53057a69 100644 --- a/crates/command_parser/src/tree.rs +++ b/crates/command_parser/src/tree.rs @@ -38,15 +38,15 @@ impl TreeBranch { ); } - pub(super) fn command(&self) -> Option { + pub fn command(&self) -> Option { self.current_command.clone() } - pub(super) fn possible_tokens(&self) -> impl Iterator { + pub fn possible_tokens(&self) -> impl Iterator { self.branches.keys() } - pub(super) fn possible_commands(&self, max_depth: usize) -> impl Iterator { + pub 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, @@ -69,7 +69,11 @@ impl TreeBranch { commands } - pub(super) fn get_branch(&self, token: &Token) -> Option<&TreeBranch> { + pub fn get_branch(&self, token: &Token) -> Option<&Self> { self.branches.get(token) } + + pub fn branches(&self) -> impl Iterator { + self.branches.iter() + } } diff --git a/crates/commands/Cargo.toml b/crates/commands/Cargo.toml index 46bca6a7..e853ff83 100644 --- a/crates/commands/Cargo.toml +++ b/crates/commands/Cargo.toml @@ -2,6 +2,11 @@ name = "commands" version = "0.1.0" edition = "2021" +default-run = "commands" + +[[bin]] +name = "write_cs_glue" +path = "src/bin/write_cs_glue.rs" [lib] crate-type = ["cdylib", "lib"] diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index 313c6d74..2c863447 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -1,5 +1,8 @@ #![feature(iter_intersperse)] +use command_parser::{token::Token, Tree}; +use commands::COMMAND_TREE; + fn main() { let cmd = std::env::args() .skip(1) @@ -18,3 +21,21 @@ fn main() { } } } + +fn print_tree(tree: &Tree, depth: usize) { + println!(); + for (token, branch) in tree.branches() { + for _ in 0..depth { + print!(" "); + } + for _ in 0..depth { + print!("-"); + } + print!("> {token:?}"); + if matches!(token, Token::Empty) { + println!(": {}", branch.command().unwrap().cb) + } else { + print_tree(branch, depth + 1) + } + } +} diff --git a/flake.nix b/flake.nix index 6eed4bb1..5debf1e1 100644 --- a/flake.nix +++ b/flake.nix @@ -96,6 +96,8 @@ cp -f "$commandslib" obj/ fi uniffi-bindgen-cs "$commandslib" --library --out-dir="''${2:-./PluralKit.Bot}" + cargo run --package commands --bin write_cs_glue -- "''${2:-./PluralKit.Bot}"/commandtypes.cs + dotnet format ./PluralKit.Bot/PluralKit.Bot.csproj ''; }; };