From 59cb407eae1dc4b35e1f8dd67f06c2920c059ad2 Mon Sep 17 00:00:00 2001 From: "Taylor C. Richberger" Date: Sun, 19 Apr 2020 16:07:33 -0600 Subject: [PATCH] Add dicebot-roll dice expression parser --- Cargo.lock | 37 ++++++++ Cargo.toml | 3 +- axfive-matrix-dicebot.code-workspace | 7 ++ src/bin/dicebot-roll.rs | 128 +++++++++++++++++++++++++++ src/{main.rs => bin/dicebot.rs} | 2 +- src/bot.rs | 35 +++++--- src/lib.rs | 2 +- src/matrix.rs | 2 +- 8 files changed, 199 insertions(+), 17 deletions(-) create mode 100644 axfive-matrix-dicebot.code-workspace create mode 100644 src/bin/dicebot-roll.rs rename src/{main.rs => bin/dicebot.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index 9edc30c..bacfa52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,14 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" +dependencies = [ + "memchr", +] + [[package]] name = "arc-swap" version = "0.4.5" @@ -16,6 +25,7 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" name = "axfive-matrix-dicebot" version = "0.1.0" dependencies = [ + "regex", "reqwest", "serde", "serde_json", @@ -581,6 +591,24 @@ version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +[[package]] +name = "regex" +version = "1.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" + [[package]] name = "remove_dir_all" version = "0.5.2" @@ -754,6 +782,15 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + [[package]] name = "time" version = "0.1.42" diff --git a/Cargo.toml b/Cargo.toml index 8f0e274..440832a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,8 @@ edition = "2018" [dependencies] reqwest = "^0.10" serde_json = "^1" -toml = "0.5" +toml = "^0.5" +regex = "^1.3" [dependencies.serde] version = "^1" diff --git a/axfive-matrix-dicebot.code-workspace b/axfive-matrix-dicebot.code-workspace new file mode 100644 index 0000000..362d7c2 --- /dev/null +++ b/axfive-matrix-dicebot.code-workspace @@ -0,0 +1,7 @@ +{ + "folders": [ + { + "path": "." + } + ] +} \ No newline at end of file diff --git a/src/bin/dicebot-roll.rs b/src/bin/dicebot-roll.rs new file mode 100644 index 0000000..ac15fb9 --- /dev/null +++ b/src/bin/dicebot-roll.rs @@ -0,0 +1,128 @@ +use regex::Regex; +use std::error::Error; +use std::fmt; +use std::str::FromStr; + +#[derive(Debug)] +struct ParseError { + reason: String, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ParseError: {}", self.reason) + } +} + +impl Error for ParseError {} + +#[derive(Debug)] +struct Dice { + count: u32, + sides: u32, +} + +#[derive(Debug)] +enum Roll { + Dice(Dice), + Bonus(u32), +} + +impl From for Roll { + fn from(value: u32) -> Roll { + Roll::Bonus(value) + } +} + +impl From for Roll { + fn from(value: Dice) -> Roll { + Roll::Dice(value) + } +} + +impl FromStr for Roll { + type Err = Box; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + let regex = Regex::new(r"(?i)^(\d+)\s*(d\s*(\d+))?$")?; + let captures = match regex.captures(s) { + Some(captures) => captures, + None => { + return Err(ParseError { + reason: format!("{:?} is not a legal Roll Part expression", s), + } + .into()) + } + }; + + match captures.get(2) { + Some(_) => Ok(Dice { + count: captures.get(1).unwrap().as_str().parse()?, + sides: captures.get(3).unwrap().as_str().parse()?, + } + .into()), + None => Ok(Roll::Bonus(captures.get(1).unwrap().as_str().parse()?)), + } + } +} + +#[derive(Debug)] +enum Part { + Plus(Roll), + Minus(Roll), +} + +impl FromStr for Part { + type Err = Box; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + + match s.chars().next() { + Some('+') => Ok(Part::Plus(s[1..].parse()?)), + Some('-') => Ok(Part::Minus(s[1..].parse()?)), + Some(_) => Ok(Part::Plus(s.parse()?)), + None => Err(ParseError { + reason: format!("{:?} is not a legal Roll Part expression", s), + } + .into()), + } + } +} + +#[derive(Debug)] +struct Expression(Vec); + +impl FromStr for Expression { + type Err = Box; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + let validation_regex = Regex::new( + r"(?xi)^ + ([-\+])?\s*(\d+)\s*(d\s*(\d+))? + (\s*([-\+])\s*(\d+)\s*(d\s*(\d+))?)* + $", + )?; + if !validation_regex.is_match(s) { + return Err(ParseError { + reason: format!("{:?} is not a legal dice expression", s), + } + .into()); + } + + let part = Regex::new(r"(?i)[-\+]?\s*\d+\s*(d\s*(\d+))?")?; + + let results: Result<_, _> = part.find_iter(s).map(|p| p.as_str().parse()).collect(); + Ok(Expression(results?)) + } +} + +fn main() -> Result<(), Box> { + let roll_string = std::env::args().skip(1).collect::>().join(" "); + // first regex needs to be different because the sign is mandatory for the rest + let expression: Expression = roll_string.parse()?; + println!("{:?}", expression); + Ok(()) +} diff --git a/src/main.rs b/src/bin/dicebot.rs similarity index 100% rename from src/main.rs rename to src/bin/dicebot.rs index 349281d..96c6d54 100644 --- a/src/main.rs +++ b/src/bin/dicebot.rs @@ -1,6 +1,6 @@ +use axfive_matrix_dicebot::bot::DiceBot; use tokio::select; use tokio::signal::unix::{signal, SignalKind}; -use axfive_matrix_dicebot::bot::DiceBot; #[tokio::main] async fn main() -> Result<(), Box> { diff --git a/src/bot.rs b/src/bot.rs index 3c91d3d..6bd80d3 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,10 +1,11 @@ -use serde::{self, Deserialize, Serialize}; -use reqwest::{Client, Url}; use crate::matrix::SyncCommand; -use std::path::PathBuf; +use reqwest::{Client, Url}; +use serde::{self, Deserialize, Serialize}; use std::fs; +use std::path::PathBuf; -const USER_AGENT: &str = "AxFive Matrix DiceBot/0.1.0 (+https://gitlab.com/Taywee/axfive-matrix-dicebot)"; +const USER_AGENT: &str = + "AxFive Matrix DiceBot/0.1.0 (+https://gitlab.com/Taywee/axfive-matrix-dicebot)"; /// The "matrix" section of the config, which gives home server, login information, and etc. #[derive(Serialize, Deserialize, Debug)] @@ -24,7 +25,7 @@ pub struct Config { } /// The actual dicebot structure, which drives the entire operation. -/// +/// /// This is the core of the dicebot program. pub struct DiceBot { config_path: Option, @@ -42,20 +43,24 @@ struct LoginResponse { impl DiceBot { /// Create a new dicebot from the given config path and config - pub async fn new(config_path: Option, config: Config) -> Result> { + pub async fn new( + config_path: Option, + config: Config, + ) -> Result> { let home_server: Url = format!("https://{}", config.matrix.home_server).parse()?; let client = Client::new(); let request = serde_json::to_string(&config.matrix.login)?; let mut login_url = home_server.clone(); login_url.set_path("/_matrix/client/r0/login"); - let response = client.post(login_url) + let response = client + .post(login_url) .header("user-agent", USER_AGENT) .body(request) .send() .await?; let body: LoginResponse = serde_json::from_str(&response.text().await?)?; let next_batch = config.matrix.next_batch.clone(); - Ok(DiceBot{ + Ok(DiceBot { home_server, config_path, client, @@ -66,7 +71,9 @@ impl DiceBot { } /// Create a new dicebot, storing the config path to write it out - pub async fn from_path>(config_path: P) -> Result> { + pub async fn from_path>( + config_path: P, + ) -> Result> { let config_path = config_path.into(); let config = { let contents = fs::read_to_string(&config_path)?; @@ -98,10 +105,11 @@ impl DiceBot { // TODO: handle http 429 if let Some(since) = &self.next_batch { - sync_url.query_pairs_mut() - .append_pair("since", since); + sync_url.query_pairs_mut().append_pair("since", since); } - let body = self.client.get(sync_url) + let body = self + .client + .get(sync_url) .header("user-agent", USER_AGENT) .send() .await? @@ -117,7 +125,8 @@ impl DiceBot { /// construction pub async fn logout(mut self) -> Result<(), Box> { let logout_url = self.url("/_matrix/client/r0/logout", &[]); - self.client.post(logout_url) + self.client + .post(logout_url) .header("user-agent", USER_AGENT) .body("{}") .send() diff --git a/src/lib.rs b/src/lib.rs index 41a394f..54a5004 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,2 @@ -pub mod matrix; pub mod bot; +pub mod matrix; diff --git a/src/matrix.rs b/src/matrix.rs index 0766fcb..4d5c8ba 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; #[serde(tag = "msgtype")] #[serde(rename = "m.text")] pub struct TextMessage { - body: String + body: String, } // Need untagged because redactions are blank