init rust command parser

This commit is contained in:
alyssa 2024-09-13 16:02:30 +09:00
parent 32a6e97342
commit c3cc5c9d03
15 changed files with 968 additions and 27 deletions

3
.gitignore vendored
View file

@ -28,4 +28,5 @@ pluralkit.*.conf
logs/
.version
recipe.json
.docker-bin/
.docker-bin/
PluralKit.Bot/commands.cs

381
Cargo.lock generated
View file

@ -112,6 +112,47 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f907281554a3d0312bb7aab855a8e0ef6cbf1614d06de54105039ca8b34460e"
[[package]]
name = "askama"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
dependencies = [
"askama_derive",
"askama_escape",
]
[[package]]
name = "askama_derive"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
dependencies = [
"askama_parser",
"basic-toml",
"mime",
"mime_guess",
"proc-macro2",
"quote",
"serde",
"syn 2.0.77",
]
[[package]]
name = "askama_escape"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
[[package]]
name = "askama_parser"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
dependencies = [
"nom",
]
[[package]]
name = "async-trait"
version = "0.1.80"
@ -120,7 +161,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
"syn 2.0.77",
]
[[package]]
@ -293,6 +334,24 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "basic-toml"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8"
dependencies = [
"serde",
]
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -354,6 +413,38 @@ dependencies = [
"either",
]
[[package]]
name = "camino"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
dependencies = [
"serde",
]
[[package]]
name = "cargo-platform"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc"
dependencies = [
"serde",
]
[[package]]
name = "cargo_metadata"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a"
dependencies = [
"camino",
"cargo-platform",
"semver",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "cc"
version = "1.0.98"
@ -378,6 +469,19 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "command_system_macros"
version = "0.1.0"
[[package]]
name = "commands"
version = "0.1.0"
dependencies = [
"command_system_macros",
"lazy_static",
"uniffi",
]
[[package]]
name = "config"
version = "0.13.3"
@ -572,7 +676,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.66",
"syn 2.0.77",
]
[[package]]
@ -697,6 +801,15 @@ dependencies = [
"url",
]
[[package]]
name = "fs-err"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
dependencies = [
"autocfg",
]
[[package]]
name = "futures"
version = "0.3.30"
@ -764,7 +877,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
"syn 2.0.77",
]
[[package]]
@ -834,6 +947,23 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "goblin"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d6b4de4a8eb6c46a8c77e1d3be942cb9a8bf073c22374578e5ba4b08ed0ff68"
dependencies = [
"log",
"plain",
"scroll",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@ -1374,9 +1504,9 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.5.0"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memoffset"
@ -1451,6 +1581,16 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -1585,6 +1725,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "oneshot-uniffi"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c548d5c78976f6955d72d0ced18c48ca07030f7a1d4024529fedd7c1c01b29c"
[[package]]
name = "opaque-debug"
version = "0.3.0"
@ -1795,6 +1941,12 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "portable-atomic"
version = "0.3.19"
@ -1824,7 +1976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e"
dependencies = [
"proc-macro2",
"syn 2.0.66",
"syn 2.0.77",
]
[[package]]
@ -1863,7 +2015,7 @@ dependencies = [
"prost",
"prost-types",
"regex",
"syn 2.0.66",
"syn 2.0.77",
"tempfile",
]
@ -1877,7 +2029,7 @@ dependencies = [
"itertools",
"proc-macro2",
"quote",
"syn 2.0.66",
"syn 2.0.77",
]
[[package]]
@ -2051,14 +2203,14 @@ dependencies = [
[[package]]
name = "regex"
version = "1.9.4"
version = "1.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.3.7",
"regex-syntax 0.7.5",
"regex-automata 0.4.7",
"regex-syntax 0.8.4",
]
[[package]]
@ -2072,13 +2224,13 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.3.7"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.5",
"regex-syntax 0.8.4",
]
[[package]]
@ -2089,9 +2241,9 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "regex-syntax"
version = "0.7.5"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "reqwest"
@ -2289,11 +2441,34 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "scroll"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da"
dependencies = [
"scroll_derive",
]
[[package]]
name = "scroll_derive"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "semver"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
dependencies = [
"serde",
]
[[package]]
name = "serde"
@ -2312,7 +2487,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
"syn 2.0.77",
]
[[package]]
@ -2410,6 +2585,12 @@ dependencies = [
"rand_core",
]
[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "sketches-ddsketch"
version = "0.2.0"
@ -2684,6 +2865,12 @@ dependencies = [
"urlencoding",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "stringprep"
version = "0.1.5"
@ -2714,9 +2901,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.66"
version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [
"proc-macro2",
"quote",
@ -2829,7 +3016,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
"syn 2.0.77",
]
[[package]]
@ -3036,6 +3223,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
[[package]]
name = "unicase"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.10"
@ -3075,6 +3271,134 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "uniffi"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21345172d31092fd48c47fd56c53d4ae9e41c4b1f559fb8c38c1ab1685fd919f"
dependencies = [
"anyhow",
"uniffi_build",
"uniffi_core",
"uniffi_macros",
]
[[package]]
name = "uniffi_bindgen"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd992f2929a053829d5875af1eff2ee3d7a7001cb3b9a46cc7895f2caede6940"
dependencies = [
"anyhow",
"askama",
"camino",
"cargo_metadata",
"fs-err",
"glob",
"goblin",
"heck 0.4.1",
"once_cell",
"paste",
"serde",
"toml",
"uniffi_meta",
"uniffi_testing",
"uniffi_udl",
]
[[package]]
name = "uniffi_build"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "001964dd3682d600084b3aaf75acf9c3426699bc27b65e96bb32d175a31c74e9"
dependencies = [
"anyhow",
"camino",
"uniffi_bindgen",
]
[[package]]
name = "uniffi_checksum_derive"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55137c122f712d9330fd985d66fa61bdc381752e89c35708c13ce63049a3002c"
dependencies = [
"quote",
"syn 2.0.77",
]
[[package]]
name = "uniffi_core"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6121a127a3af1665cd90d12dd2b3683c2643c5103281d0fed5838324ca1fad5b"
dependencies = [
"anyhow",
"bytes",
"camino",
"log",
"once_cell",
"oneshot-uniffi",
"paste",
"static_assertions",
]
[[package]]
name = "uniffi_macros"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11cf7a58f101fcedafa5b77ea037999b88748607f0ef3a33eaa0efc5392e92e4"
dependencies = [
"bincode",
"camino",
"fs-err",
"once_cell",
"proc-macro2",
"quote",
"serde",
"syn 2.0.77",
"toml",
"uniffi_build",
"uniffi_meta",
]
[[package]]
name = "uniffi_meta"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71dc8573a7b1ac4b71643d6da34888273ebfc03440c525121f1b3634ad3417a2"
dependencies = [
"anyhow",
"bytes",
"siphasher",
"uniffi_checksum_derive",
]
[[package]]
name = "uniffi_testing"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "118448debffcb676ddbe8c5305fb933ab7e0123753e659a71dc4a693f8d9f23c"
dependencies = [
"anyhow",
"camino",
"cargo_metadata",
"fs-err",
"once_cell",
]
[[package]]
name = "uniffi_udl"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "889edb7109c6078abe0e53e9b4070cf74a6b3468d141bdf5ef1bd4d1dc24a1c3"
dependencies = [
"anyhow",
"uniffi_meta",
"uniffi_testing",
"weedle2",
]
[[package]]
name = "untrusted"
version = "0.9.0"
@ -3165,7 +3489,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.66",
"syn 2.0.77",
"wasm-bindgen-shared",
]
@ -3199,7 +3523,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
"syn 2.0.77",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -3229,6 +3553,15 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "weedle2"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741"
dependencies = [
"nom",
]
[[package]]
name = "whoami"
version = "1.5.1"
@ -3555,7 +3888,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
"syn 2.0.77",
]
[[package]]

View file

@ -1,6 +1,7 @@
[workspace]
members = [
"./lib/libpk",
"./lib/commands",
"./services/api",
"./services/dispatch"
]

View file

@ -45,9 +45,19 @@ public class Context
_provider = provider;
_commandMessageService = provider.Resolve<CommandMessageService>();
CommandPrefix = message.Content?.Substring(0, commandParseOffset);
Parameters = new Parameters(message.Content?.Substring(commandParseOffset));
Rest = provider.Resolve<DiscordApiClient>();
Cluster = provider.Resolve<Cluster>();
try
{
Parameters = new ParametersFFI(message.Content?.Substring(commandParseOffset));
}
catch (PKError e)
{
// todo: not this
Reply($"{Emojis.Error} {e.Message}");
throw;
}
}
public readonly IDiscordCache Cache;
@ -71,7 +81,7 @@ public class Context
public DateTimeZone Zone => Config?.Zone ?? DateTimeZone.Utc;
public readonly string CommandPrefix;
public readonly Parameters Parameters;
public readonly ParametersFFI Parameters;
internal readonly IDatabase Database;
internal readonly ModelRepository Repository;

View file

@ -0,0 +1,66 @@
using uniffi.commands;
namespace PluralKit.Bot;
public class ParametersFFI
{
private string _cb { get; init; }
private List<string> _args { get; init; }
public int _ptr = -1;
private Dictionary<string, string?> _flags { get; init; }
// just used for errors, temporarily
public string FullCommand { get; init; }
public ParametersFFI(string cmd)
{
FullCommand = cmd;
var result = CommandsMethods.ParseCommand(cmd);
if (result is CommandResult.Ok)
{
var command = ((CommandResult.Ok)result).@command;
_cb = command.@commandRef;
_args = command.@args;
_flags = command.@flags;
}
else
{
throw new PKError(((CommandResult.Err)result).@error);
}
}
public string Pop()
{
if (_args.Count > _ptr + 1) Console.WriteLine($"pop: {_ptr + 1}, {_args[_ptr + 1]}");
else Console.WriteLine("pop: no more arguments");
if (_args.Count() == _ptr + 1) return "";
_ptr++;
return _args[_ptr];
}
public string Peek()
{
if (_args.Count > _ptr + 1) Console.WriteLine($"peek: {_ptr + 1}, {_args[_ptr + 1]}");
else Console.WriteLine("peek: no more arguments");
if (_args.Count() == _ptr + 1) return "";
return _args[_ptr + 1];
}
// this might not work quite right
public string PeekWithPtr(ref int ptr)
{
return _args[ptr];
}
public ISet<string> Flags()
{
return new HashSet<string>(_flags.Keys);
}
// parsed differently in new commands, does this work right?
// note: skipFlags here does nothing
public string Remainder(bool skipFlags = false)
{
return Pop();
}
}

View file

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>annotations</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

View file

@ -0,0 +1,7 @@
[package]
name = "command_system_macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true

View file

@ -0,0 +1,109 @@
use proc_macro::{Delimiter, TokenStream, TokenTree};
fn make_command(tokens: Vec<String>, help: String, cb: String) -> String {
let tokens = tokens
.iter()
.map(|v| format!("Token::{v}"))
.collect::<Vec<String>>()
.join(",");
format!(
r#"Command {{ tokens: vec![{tokens}], help: {help}.to_string(), cb: "{cb}".to_string() }}"#
)
}
fn command_from_stream(stream: TokenStream) -> String {
let mut part = 0;
let mut found_tokens: Vec<String> = Vec::new();
let mut found_cb: Option<String> = None;
let mut found_help: Option<String> = None;
let mut is_token_lit = false;
let mut tokens = stream.clone().into_iter();
'a: loop {
let cur_token = tokens.next();
match cur_token {
None if part == 2 && found_help.is_some() => break 'a,
Some(TokenTree::Ident(ident)) if part == 0 => {
found_tokens.push(if is_token_lit {
format!("{ident}")
} else {
format!("Value(vec![\"{ident}\".to_string()])")
});
// reset this
is_token_lit = false;
}
Some(TokenTree::Punct(punct)) if part == 0 && format!("{punct}") == "@" => {
is_token_lit = true
}
Some(TokenTree::Punct(punct))
if ((part == 0 && found_tokens.len() > 0) || (part == 1 && found_cb.is_some()))
&& format!("{punct}") == "," =>
{
part += 1
}
Some(TokenTree::Ident(ident)) if part == 1 => found_cb = Some(format!("{ident}")),
Some(TokenTree::Literal(lit)) if part == 2 => found_help = Some(format!("{lit}")),
_ => panic!("invalid command definition: {stream}"),
}
}
make_command(found_tokens, found_help.unwrap(), found_cb.unwrap())
}
#[proc_macro]
pub fn commands(stream: TokenStream) -> TokenStream {
let mut commands: Vec<String> = Vec::new();
let mut top_level_tokens = stream.into_iter();
'a: loop {
// "command"
match top_level_tokens.next() {
Some(TokenTree::Ident(ident)) if format!("{ident}") == "command" => {}
None => break 'a,
_ => panic!("contents of commands! macro is invalid"),
}
//
match top_level_tokens.next() {
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => {
commands.push(command_from_stream(group.stream()));
}
_ => panic!("contents of commands! macro is invalid"),
}
// ;
match top_level_tokens.next() {
Some(TokenTree::Punct(punct)) if format!("{punct}") == ";" => {}
_ => panic!("contents of commands! macro is invalid"),
}
}
let command_registrations = commands
.iter()
.map(|v| format!("tree.register_command({v});"))
.collect::<Vec<String>>()
.join("\n");
let res = format!(
r#"
lazy_static::lazy_static! {{
static ref COMMAND_TREE: TreeBranch = {{
let mut tree = TreeBranch {{
current_command_key: None,
possible_tokens: vec![],
branches: HashMap::new(),
}};
{command_registrations}
tree.sort_tokens();
// println!("{{tree:#?}}");
tree
}};
}}
"#
);
// panic!("{res}");
res.parse().unwrap()
}

16
lib/commands/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "commands"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
lazy_static = { workspace = true }
command_system_macros = { path = "../command_system_macros" }
uniffi = { version = "0.25" }
[build-dependencies]
uniffi = { version = "0.25", features = [ "build" ] }

3
lib/commands/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
uniffi::generate_scaffolding("src/commands.udl").unwrap();
}

View file

@ -0,0 +1,13 @@
namespace commands {
CommandResult parse_command(string input);
};
[Enum]
interface CommandResult {
Ok(ParsedCommand command);
Err(string error);
};
dictionary ParsedCommand {
string command_ref;
sequence<string> args;
record<string, string?> flags;
};

208
lib/commands/src/lib.rs Normal file
View file

@ -0,0 +1,208 @@
#![feature(let_chains)]
use core::panic;
use std::{cmp::Ordering, collections::HashMap};
uniffi::include_scaffolding!("commands");
mod string;
mod token;
use token::*;
use command_system_macros::commands;
// todo!: move all this stuff into a different file
// lib.rs should just have exported symbols and command definitions
#[derive(Debug, Clone)]
struct TreeBranch {
current_command_key: Option<String>,
/// branches.keys(), but sorted by specificity
possible_tokens: Vec<Token>,
branches: HashMap<Token, TreeBranch>,
}
impl TreeBranch {
fn register_command(&mut self, command: Command) {
let mut current_branch = self;
// iterate over tokens in command
for token in command.tokens {
// recursively get or create a sub-branch for each token
current_branch = current_branch.branches.entry(token).or_insert(TreeBranch {
current_command_key: None,
possible_tokens: vec![],
branches: HashMap::new(),
})
}
// when we're out of tokens, add an Empty branch with the callback and no sub-branches
current_branch.branches.insert(
Token::Empty,
TreeBranch {
current_command_key: Some(command.cb),
possible_tokens: vec![],
branches: HashMap::new(),
},
);
}
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
}
});
}
}
struct Command {
tokens: Vec<Token>,
help: String,
cb: String,
}
// todo: aliases
// todo: categories
commands! {
command(help, help, "Shows the help command");
command(member new, member_new, "Creates a new system member");
command(member @MemberRef, member_show, "Shows information about a member");
command(member @MemberRef description, member_desc_show, "Shows a member's description");
command(member @MemberRef description @FullString, member_desc_update, "Changes a member's description");
command(member @MemberRef privacy, member_privacy_show, "Displays a member's current privacy settings");
command(member @MemberRef privacy @MemberPrivacyTarget @PrivacyLevel, member_privacy_update, "Changes a member's privacy settings");
}
pub enum CommandResult {
Ok { command: ParsedCommand },
Err { error: String },
}
pub struct ParsedCommand {
pub command_ref: String,
pub args: Vec<String>,
pub flags: HashMap<String, Option<String>>,
}
/// Find the next token from an either raw or partially parsed command string
///
/// Returns:
/// - 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
/// - optionally a short-circuit error
fn next_token(
possible_tokens: Vec<Token>,
input: String,
current_pos: usize,
) -> Result<(Token, Option<String>, usize), Option<String>> {
// get next parameter, matching quotes
let param = crate::string::next_param(input.clone(), current_pos);
println!("matched: {param:?}\n---");
// try checking if this is a flag
// todo!: this breaks full text matching if the full text starts with a flag
// (but that's kinda already broken anyway)
if let Some((value, new_pos)) = param.clone()
&& value.starts_with('-')
{
return Ok((
Token::Flag,
Some(value.trim_start_matches('-').to_string()),
new_pos,
));
}
// 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)));
}
}
Err(None)
}
fn parse_command(input: String) -> CommandResult {
let mut local_tree: TreeBranch = COMMAND_TREE.clone();
// end position of all currently matched tokens
let mut current_pos = 0;
let mut args: Vec<String> = Vec::new();
let mut flags: HashMap<String, Option<String>> = HashMap::new();
loop {
match next_token(
local_tree.possible_tokens.clone(),
input.clone(),
current_pos,
) {
Ok((found_token, arg, new_pos)) => {
current_pos = new_pos;
if let Token::Flag = found_token {
flags.insert(arg.unwrap(), None);
// don't try matching flags as tree elements
continue;
}
if let Some(arg) = arg {
args.push(arg);
}
if let Some(next_tree) = local_tree.branches.get(&found_token) {
local_tree = next_tree.clone();
} else {
panic!("found token could not match tree, at {input}");
}
}
Err(None) => {
if let Some(command_ref) = local_tree.current_command_key {
return CommandResult::Ok {
command: ParsedCommand {
command_ref,
args,
flags,
},
};
}
// 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: "Command not found.".to_string(),
};
}
Err(Some(short_circuit)) => {
return CommandResult::Err {
error: short_circuit,
};
}
}
}
}

View file

@ -0,0 +1,87 @@
use std::collections::HashMap;
lazy_static::lazy_static! {
// Dictionary of (left, right) quote pairs
// Each char in the string is an individual quote, multi-char strings imply "one of the following chars"
pub static ref QUOTE_PAIRS: HashMap<String, String> = {
let mut pairs = HashMap::new();
macro_rules! insert_pair {
($a:literal, $b:literal) => {
pairs.insert($a.to_string(), $b.to_string());
// make it easier to look up right quotes
for char in $a.chars() {
pairs.insert(char.to_string(), $b.to_string());
}
}
}
// Basic
insert_pair!( "'", "'" ); // ASCII single quotes
insert_pair!( "\"", "\"" ); // ASCII double quotes
// "Smart quotes"
// Specifically ignore the left/right status of the quotes and match any combination of them
// Left string also includes "low" quotes to allow for the low-high style used in some locales
insert_pair!( "\u{201C}\u{201D}\u{201F}\u{201E}", "\u{201C}\u{201D}\u{201F}" ); // double quotes
insert_pair!( "\u{2018}\u{2019}\u{201B}\u{201A}", "\u{2018}\u{2019}\u{201B}" ); // single quotes
// Chevrons (normal and "fullwidth" variants)
insert_pair!( "\u{00AB}\u{300A}", "\u{00BB}\u{300B}" ); // double chevrons, pointing away (<<text>>)
insert_pair!( "\u{00BB}\u{300B}", "\u{00AB}\u{300A}" ); // double chevrons, pointing together (>>text<<)
insert_pair!( "\u{2039}\u{3008}", "\u{203A}\u{3009}" ); // single chevrons, pointing away (<text>)
insert_pair!( "\u{203A}\u{3009}", "\u{2039}\u{3008}" ); // single chevrons, pointing together (>text<)
// Other
insert_pair!( "\u{300C}\u{300E}", "\u{300D}\u{300F}" ); // corner brackets (Japanese/Chinese)
pairs
};
}
// very very simple quote matching
// quotes need to be at start/end of words, and are ignored if a closing quote is not present
// WTB POSIX quoting: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html
pub fn next_param(input: String, current_pos: usize) -> Option<(String, usize)> {
if input.len() == current_pos {
return None;
}
let leading_whitespace_count =
input[..current_pos].len() - input[..current_pos].trim_start().len();
let substr_to_match = input[current_pos + leading_whitespace_count..].to_string();
println!("stuff: {input} {current_pos} {leading_whitespace_count}");
println!("to match: {substr_to_match}");
// try matching end quote
if let Some(right) = QUOTE_PAIRS.get(&substr_to_match[0..1]) {
for possible_quote in right.chars() {
for (pos, _) in substr_to_match.match_indices(possible_quote) {
if substr_to_match.len() == pos + 1
|| substr_to_match
.chars()
.nth(pos + 1)
.unwrap()
.is_whitespace()
{
// return quoted string, without quotes
return Some((
substr_to_match[1..pos - 1].to_string(),
current_pos + pos + 1,
));
}
}
}
}
// find next whitespace character
for (pos, char) in substr_to_match.clone().char_indices() {
if char.is_whitespace() {
return Some((substr_to_match[..pos].to_string(), current_pos + pos + 1));
}
}
// if we're here, we went to EOF and didn't match any whitespace
// so we return the whole string
Some((substr_to_match.clone(), current_pos + substr_to_match.len()))
}

84
lib/commands/src/token.rs Normal file
View file

@ -0,0 +1,84 @@
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
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,
/// A bot-defined value ("member" in `pk;member MyName`)
Value(Vec<String>),
/// A command defined by multiple values
// todo!
MultiValue(Vec<Vec<String>>),
FullString,
/// Member reference (hid or member name)
MemberRef,
MemberPrivacyTarget,
PrivacyLevel,
// currently not included in command definitions
// todo: flags with values
Flag,
}
pub enum TokenMatchResult {
NoMatch,
/// Token matched, optionally with a value.
Match(Option<String>),
}
// move this somewhere else
lazy_static::lazy_static!(
static ref MEMBER_PRIVACY_TARGETS: Vec<String> = vec![
"visibility".to_string(),
"name".to_string(),
"todo".to_string()
];
);
impl Token {
pub fn try_match(&self, input: Option<String>) -> TokenMatchResult {
// short circuit on empty things
if matches!(self, Self::Empty) && input.is_none() {
return TokenMatchResult::Match(None);
} else if input.is_none() {
return TokenMatchResult::NoMatch;
}
let input = input.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::MultiValue(_) => todo!(),
Self::FullString => return TokenMatchResult::Match(Some(input)),
Self::MemberRef => return TokenMatchResult::Match(Some(input)),
Self::MemberPrivacyTarget
if MEMBER_PRIVACY_TARGETS.contains(&input.trim().to_string()) =>
{
return TokenMatchResult::Match(Some(input))
}
Self::MemberPrivacyTarget => {}
Self::PrivacyLevel if input == "public" || input == "private" => {
return TokenMatchResult::Match(Some(input))
}
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;
}
}

2
lib/commands/uniffi.toml Normal file
View file

@ -0,0 +1,2 @@
[bindings.csharp]
cdylib_name = "commands"