diff options
| author | Lorenz Leitner <lrnz.ltnr@gmail.com> | 2021-10-10 15:12:41 +0200 |
|---|---|---|
| committer | Lorenz Leitner <lrnz.ltnr@gmail.com> | 2021-10-12 12:06:57 +0200 |
| commit | ed1c228094188d872ceb8407fb6f46ff698937c2 (patch) | |
| tree | 7d97b610220ff57cfcbf874b45936a88571de8a7 | |
| parent | Basic (re)implementation of gonzobot wolfram alpha (diff) | |
Add test
Put WA api response JSON into test resource file
Add short url, increase concurrency
Move shorten_url to util dir
| -rw-r--r-- | Cargo.lock | 61 | ||||
| -rw-r--r-- | Cargo.toml | 5 | ||||
| -rw-r--r-- | src/hooks/wa.rs | 107 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/util/mod.rs | 1 | ||||
| -rw-r--r-- | src/util/web.rs | 20 | ||||
| -rw-r--r-- | tests/resources/wolfram_alpha_api_response.json | 244 |
7 files changed, 404 insertions, 35 deletions
@@ -22,9 +22,30 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" + +[[package]] +name = "assert-json-diff" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f1c3703dd33532d7f0ca049168930e9099ecac238e23cf932f3a69c42f06da" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] [[package]] name = "autocfg" @@ -81,6 +102,7 @@ dependencies = [ "futures", "irc", "irc-proto", + "mockito", "rand", "regex", "reqwest", @@ -137,6 +159,17 @@ dependencies = [ ] [[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + +[[package]] name = "core-foundation" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -172,6 +205,12 @@ dependencies = [ ] [[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + +[[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -680,6 +719,24 @@ dependencies = [ ] [[package]] +name = "mockito" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d10030163d67f681db11810bc486df3149e6d91c8b4f3f96fa8b62b546c2cef8" +dependencies = [ + "assert-json-diff", + "colored", + "difference", + "httparse", + "lazy_static", + "log", + "rand", + "regex", + "serde_json", + "serde_urlencoded", +] + +[[package]] name = "native-tls" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -16,7 +16,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.5" -anyhow = "1" +anyhow = "1.0.44" futures = "0.3" tokio = { version = "1.5.0", features = ["full", "rt-multi-thread"] } @@ -32,6 +32,9 @@ rand = "0.8.3" reqwest = "0.11" +[dev-dependencies] +mockito = "0.30.0" + [features] default = [] diff --git a/src/hooks/wa.rs b/src/hooks/wa.rs index b6d4eff..7d087ea 100644 --- a/src/hooks/wa.rs +++ b/src/hooks/wa.rs @@ -1,11 +1,12 @@ -use anyhow::{bail, Context, Error, Result}; +use crate::util::web::shorten_url; +use anyhow::{Context, Error, Result}; +use futures::try_join; use reqwest::{get, Url}; use serde::{Deserialize, Serialize}; use serde_json::Result as SerdeJsonResult; -use tracing::trace; #[derive(Serialize, Deserialize, Debug)] -struct WaResult { +struct WaResponse { queryresult: QueryResult, } @@ -18,41 +19,28 @@ struct QueryResult { struct Pod { title: String, id: String, + primary: Option<bool>, subpods: Vec<SubPod>, } #[derive(Serialize, Deserialize, Debug)] struct SubPod { - title: String, plaintext: String, } -fn parse_json(str_data: &str) -> SerdeJsonResult<WaResult> { - let w: WaResult = serde_json::from_str(str_data)?; +fn parse_json(str_data: &str) -> SerdeJsonResult<WaResponse> { + let w: WaResponse = serde_json::from_str(str_data)?; Ok(w) } -#[tracing::instrument] -async fn wa_query(query_str: &str) -> Result<String, Error> { - let app_id = "XXX"; // TODO: Get from env - let api_url = format!( - "http://api.wolframalpha.com/v2/query?input={}&appid={}&output=json", - query_str, app_id - ); - - let body = get(Url::parse(&api_url).context("Failed to parse url")?) - .await - .context("Failed to make request")? - .text() - .await - .context("failed to get request response text")?; - - let full_wa_res = parse_json(&body)?; - trace!("got full_wa_res: {:?}", full_wa_res); - - let pod_plaintexts = full_wa_res.queryresult.pods +/// Reduces all 'pod' plaintexts to a single string. +/// Same as gonzobot does it. +fn to_single_string(wa_res: WaResponse) -> String { + wa_res + .queryresult + .pods .iter() - .filter(|it| it.id.to_lowercase() != "input") + .filter(|it| it.id.to_lowercase() != "input" && it.primary.is_some()) .map(|pod| { let subpod_texts = pod .subpods @@ -64,9 +52,55 @@ async fn wa_query(query_str: &str) -> Result<String, Error> { format!("{}: {}", &pod.title, subpod_texts) }) .collect::<Vec<String>>() - .join(" - "); + .join(" - ") +} - Ok(pod_plaintexts) +fn get_url(query_str: &str, base_url: Option<&str>) -> String { + let app_id = "XXX"; // TODO: Get from env + let wa_url = "http://api.wolframalpha.com"; + let api_url = format!( + "{}/v2/query?input={}&appid={}&output=json", + base_url.unwrap_or(wa_url), + query_str, + app_id + ); + api_url +} + +async fn send_wa_req(url: &str) -> Result<String, Error> { + let body = get(Url::parse(url).context("Failed to parse url")?) + .await + .context("Failed to make request")? + .text() + .await + .context("failed to get request response text")?; + Ok(body) +} + +async fn handle_wa_req(url: &str) -> Result<WaResponse, Error> { + let res_body = send_wa_req(url).await?; + let parsed = parse_json(&res_body)?; + Ok(parsed) +} + +/// Sends a request to the Wolfram Alpha API, returns a plain text response. +#[tracing::instrument] +async fn wa_query(query_str: &str, base_url: Option<&str>) -> Result<String, Error> { + let user_url = format!("http://www.wolframalpha.com/input/?i={}", query_str); + let user_url_shortened_fut = shorten_url(&user_url); + + let url = get_url(query_str, base_url); + let wa_res_fut = handle_wa_req(&url); + + // Can't just (foo.await, bar.await), smh + // https://rust-lang.github.io/async-book/06_multiple_futures/02_join.html + let (wa_res, user_url_shortened) = try_join!(wa_res_fut, user_url_shortened_fut)?; + + Ok(format!( + "{} - {}", + &user_url_shortened, + to_single_string(wa_res) + )) } #[cfg(test)] @@ -74,11 +108,20 @@ mod tests { use super::wa_query; use anyhow::{Error, Result}; + use mockito::{self, Matcher}; #[tokio::test] - // async fn test_query_result_json_parsing() -> Result<(), Error> { - async fn test_query_result_json_parsing() { - let res = wa_query("weather graz").await.unwrap(); - assert_eq!(res, "asdf"); + async fn test_query_result_json_parsing() -> Result<(), Error> { + let body = include_str!("../../tests/resources/wolfram_alpha_api_response.json"); + let _m = mockito::mock("GET", Matcher::Any) + // Trimmed down version of a full WA response: + .with_body(body) + .create(); + mockito::start(); + + let res = wa_query("5/10", Some(&mockito::server_url())).await?; + let res_without_link = res.splitn(2, "-").collect::<Vec<&str>>()[1].trim(); + assert_eq!(res_without_link, "Exact result: 1/2 - Decimal form: 0.5"); + Ok(()) } } @@ -10,6 +10,7 @@ use tracing::info; pub mod config; pub mod hooks; +pub mod util; pub use macros::catinator; diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..fdd4603 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1 @@ +pub mod web; diff --git a/src/util/web.rs b/src/util/web.rs new file mode 100644 index 0000000..77e20a3 --- /dev/null +++ b/src/util/web.rs @@ -0,0 +1,20 @@ +use anyhow::{Context, Error, Result}; +use reqwest::{get, Url}; + +// TODO: Either catinator should have a URL shortening utility module, +// or we should start our own service +pub(crate) async fn shorten_url(url: &str) -> Result<String, Error> { + // This just uses the first service gonzobot uses too + let short_url = get(Url::parse(&format!( + "https://is.gd/create.php?format=simple&url={}", + url + )) + .context("Failed to parse url")?) + .await + .context("Failed to make request")? + .text() + .await + .context("failed to get request response text")?; + + Ok(short_url) +} diff --git a/tests/resources/wolfram_alpha_api_response.json b/tests/resources/wolfram_alpha_api_response.json new file mode 100644 index 0000000..c18b30b --- /dev/null +++ b/tests/resources/wolfram_alpha_api_response.json @@ -0,0 +1,244 @@ +{ + "queryresult": { + "success": true, + "error": false, + "numpods": 7, + "datatypes": "Math", + "timedout": "", + "timedoutpods": "", + "timing": 0.934, + "parsetiming": 0.181, + "parsetimedout": false, + "recalculate": "", + "id": "MSP621911bg8chadg539i550000308ea58ih11600i1", + "host": "https://www4c.wolframalpha.com", + "server": "49", + "related": "https://www4c.wolframalpha.com/api/v1/relatedQueries.jsp?id=MSPa622011bg8chadg539i5500004eidb30h8gh28f0f5993178497346123851", + "version": "2.6", + "inputstring": "5/10", + "pods": [ + { + "title": "Input", + "scanner": "Identity", + "id": "Input", + "position": 100, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP622111bg8chadg539i5500000he65h3ca6bci37f?MSPStoreType=image/gif&s=49", + "alt": "5/10", + "title": "5/10", + "width": 21, + "height": 37, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "5/10" + } + ], + "expressiontypes": { + "name": "Default" + } + }, + { + "title": "Exact result", + "scanner": "Rational", + "id": "Result", + "position": 200, + "error": false, + "numsubpods": 1, + "primary": true, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP622211bg8chadg539i55000062chh995bc3f9362?MSPStoreType=image/gif&s=49", + "alt": "1/2", + "title": "1/2", + "width": 13, + "height": 37, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "1/2" + } + ], + "expressiontypes": { + "name": "Default" + }, + "states": [ + { + "name": "Step-by-step solution", + "input": "Result__Step-by-step solution", + "stepbystep": true + } + ] + }, + { + "title": "Decimal form", + "scanner": "Numeric", + "id": "DecimalApproximation", + "position": 300, + "error": false, + "numsubpods": 1, + "primary": true, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP622311bg8chadg539i55000069fi57493b7cabb6?MSPStoreType=image/gif&s=49", + "alt": "0.5", + "title": "0.5", + "width": 30, + "height": 21, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "0.5" + } + ], + "expressiontypes": { + "name": "Default" + } + }, + { + "title": "Number line", + "scanner": "NumberLine", + "id": "NumberLine", + "position": 400, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP622411bg8chadg539i5500004ddhf07f5g058fcc?MSPStoreType=image/gif&s=49", + "alt": "Number line", + "title": "", + "width": 330, + "height": 59, + "type": "1DMathPlot_1", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "" + } + ], + "expressiontypes": { + "name": "Default" + } + }, + { + "title": "Percentage", + "scanner": "Numeric", + "id": "Percentage", + "position": 500, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP622511bg8chadg539i550000612853id44f7fdg0?MSPStoreType=image/gif&s=49", + "alt": "50%", + "title": "50%", + "width": 28, + "height": 19, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "50%" + } + ], + "expressiontypes": { + "name": "Default" + } + }, + { + "title": "Pie chart", + "scanner": "Rational", + "id": "PieChart", + "position": 600, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP622611bg8chadg539i55000021861gdbbi1a4fhb?MSPStoreType=image/gif&s=49", + "alt": "Pie chart", + "title": "", + "width": 60, + "height": 62, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "" + } + ], + "expressiontypes": { + "name": "Default" + } + }, + { + "title": "Continued fraction", + "scanner": "ContinuedFraction", + "id": "ContinuedFraction", + "position": 700, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP622711bg8chadg539i550000405cheh4e44a1eb0?MSPStoreType=image/gif&s=49", + "alt": "[0; 2]", + "title": "[0; 2]", + "width": 548, + "height": 19, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "[0; 2]" + } + ], + "expressiontypes": { + "name": "Default" + }, + "states": [ + { + "name": "Fraction form", + "input": "ContinuedFraction__Fraction form" + } + ] + } + ], + "assumptions": { + "type": "Clash", + "word": "5/10", + "template": "Assuming \"${word}\" is ${desc1}. Use as ${desc2} instead", + "count": 2, + "values": [ + { + "name": "FracNumber", + "desc": " referring to math", + "input": "*C.5%2F10-_*FracNumber-" + }, + { + "name": "DateObject", + "desc": "a date", + "input": "*C.5%2F10-_*DateObject-" + } + ] + } + } +} |
