diff options
author | Sam Tay <sam.chong.tay@gmail.com> | 2020-06-17 23:30:33 -0700 |
---|---|---|
committer | Sam Tay <sam.chong.tay@gmail.com> | 2020-06-17 23:30:33 -0700 |
commit | ec92f930344d364e3be359a41aebea78f8205fa7 (patch) | |
tree | b041fa1404d383d44be46c9e1f4e8e94ef75e254 | |
parent | d422c8424ae76fc85e0fdf55257e0cee7fa38271 (diff) |
Use async http requests via tokio
-rw-r--r-- | Cargo.lock | 70 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | src/main.rs | 131 | ||||
-rw-r--r-- | src/stackexchange.rs | 79 |
4 files changed, 172 insertions, 113 deletions
@@ -49,6 +49,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" [[package]] +name = "async-compression" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae84766bab9f774e32979583ba56d6af8c701288c6dc99144819d5d2ee0b170f" +dependencies = [ + "bytes", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", +] + +[[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -884,13 +897,36 @@ dependencies = [ "kernel32-sys", "libc", "log", - "miow", + "miow 0.2.1", "net2", "slab", "winapi 0.2.8", ] [[package]] +name = "mio-named-pipes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e374eff525ce1c5b7687c4cef63943e7686524a387933ad27ca7ec43779cb3" +dependencies = [ + "log", + "mio", + "miow 0.3.5", + "winapi 0.3.8", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio", +] + +[[package]] name = "miow" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -903,6 +939,16 @@ dependencies = [ ] [[package]] +name = "miow" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b88fb9795d4d36d62a012dfbf49a8f5cf12751f36d31a9dbe66d528e58979e" +dependencies = [ + "socket2", + "winapi 0.3.8", +] + +[[package]] name = "native-tls" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1306,6 +1352,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b82c9238b305f26f53443e3a4bc8528d64b8d0bee408ec949eb7bf5635ec680" dependencies = [ + "async-compression", "base64 0.12.1", "bytes", "encoding_rs", @@ -1324,6 +1371,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "serde", + "serde_json", "serde_urlencoded", "tokio", "tokio-tls", @@ -1493,7 +1541,7 @@ dependencies = [ "crossterm", "cursive", "directories", - "flate2", + "futures", "lazy_static", "minimad", "phf", @@ -1504,6 +1552,7 @@ dependencies = [ "serde_yaml", "termimad", "thiserror", + "tokio", "unicode-width", ] @@ -1635,11 +1684,28 @@ dependencies = [ "futures-core", "iovec", "lazy_static", + "libc", "memchr", "mio", + "mio-named-pipes", + "mio-uds", "num_cpus", "pin-project-lite", + "signal-hook-registry", "slab", + "tokio-macros", + "winapi 0.3.8", +] + +[[package]] +name = "tokio-macros" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -14,8 +14,9 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.8" -reqwest = { version = "0.10", features = ["blocking"] } -flate2 = "1.0" +reqwest = { version = "0.10", features = ["gzip", "json"] } +futures = "0.3" +tokio = { version = "0.2", features = ["full"] } lazy_static = "1.4" minimad = "0.6" diff --git a/src/main.rs b/src/main.rs index 06cacd8..27c7109 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,76 +14,14 @@ use stackexchange::{LocalStorage, StackExchange}; use term::mk_print_error; use termimad::{CompoundStyle, MadSkin}; -fn main() { +#[tokio::main] +async fn main() -> Result<(), Error> { let mut skin = MadSkin::default(); // TODO style configuration skin.inline_code = CompoundStyle::with_fg(Color::Cyan); skin.code_block.set_fgbg(Color::Cyan, termimad::gray(20)); let mut print_error = mk_print_error(&skin); - (|| { - let opts = cli::get_opts()?; - let config = opts.config; - let site = &config.site; - let lucky = config.lucky; - let mut ls = LocalStorage::new()?; - - if let Some(key) = opts.set_api_key { - config::set_api_key(key)?; - } - - if opts.update_sites { - ls.update_sites()?; - } - - if opts.list_sites { - let sites = ls.sites()?; - let mut md = String::new(); - md.push_str("|:-:|:-:|\n"); - md.push_str("|Site Code|Site URL|\n"); - md.push_str("|-:|:-|\n"); - for s in sites.iter() { - md.push_str(&format!("|{}|{}\n", s.api_site_parameter, s.site_url)); - } - md.push_str("|-\n"); - termimad::print_text(&md); - return Ok(()); - } - - if !ls.validate_site(site)? { - print_error!(skin, "$0 is not a valid StackExchange site.\n\n", site)?; - // TODO should only use inline for single lines; use termimad::text stuff - print_notice!( - skin, - "If you think this is incorrect, try running\n\ - ```\n\ - so --update-sites\n\ - ```\n\ - to update the cached site listing. You can also run `so --list-sites` \ - to list all available sites.", - )?; - return Ok(()); - } - - if let Some(q) = opts.query { - let se = StackExchange::new(config); - // TODO get the rest of the results in the background - if lucky { - // TODO this needs preprocessing; all the more reason to do it at SE level - let md = se.search_lucky(&q)?; - skin.print_text(&md); - skin.print_text( - "\nPress **[SPACE]** to see more results, or any other key to exit", - ); - if !utils::wait_for_char(' ')? { - return Ok(()); - } - } - let qs = se.search(&q)?; - tui::run(qs)?; - } - Ok(()) - })() - .or_else(|e: Error| { + run(&mut skin).await.or_else(|e: Error| { print_error(&e.to_string())?; match e { Error::EmptySites => { @@ -92,5 +30,66 @@ fn main() { _ => Ok(()), } }) - .unwrap(); +} + +async fn run(skin: &mut MadSkin) -> Result<(), Error> { + let opts = cli::get_opts()?; + let config = opts.config; + let site = &config.site; + let lucky = config.lucky; + let mut ls = LocalStorage::new()?; + + if let Some(key) = opts.set_api_key { + config::set_api_key(key)?; + } + + if opts.update_sites { + ls.update_sites().await?; + } + + if opts.list_sites { + let sites = ls.sites().await?; + let mut md = String::new(); + md.push_str("|:-:|:-:|\n"); + md.push_str("|Site Code|Site URL|\n"); + md.push_str("|-:|:-|\n"); + for s in sites.iter() { + md.push_str(&format!("|{}|{}\n", s.api_site_parameter, s.site_url)); + } + md.push_str("|-\n"); + termimad::print_text(&md); + return Ok(()); + } + + if !ls.validate_site(site).await? { + print_error!(skin, "$0 is not a valid StackExchange site.\n\n", site)?; + // TODO should only use inline for single lines; use termimad::text stuff + print_notice!( + skin, + "If you think this is incorrect, try running\n\ + ```\n\ + so --update-sites\n\ + ```\n\ + to update the cached site listing. You can also run `so --list-sites` \ + to list all available sites.", + )?; + return Ok(()); + } + + if let Some(q) = opts.query { + let se = StackExchange::new(config); + // TODO get the rest of the results in the background + if lucky { + // TODO this needs preprocessing; all the more reason to do it at SE level + let md = se.search_lucky(&q).await?; + skin.print_text(&md); + skin.print_text("\nPress **[SPACE]** to see more results, or any other key to exit"); + if !utils::wait_for_char(' ')? { + return Ok(()); + } + } + let qs = se.search(&q).await?; + tui::run(qs)?; + } + Ok(()) } diff --git a/src/stackexchange.rs b/src/stackexchange.rs index e9bc6d9..2b4da67 100644 --- a/src/stackexchange.rs +++ b/src/stackexchange.rs @@ -1,5 +1,4 @@ -use flate2::read::GzDecoder; -use reqwest::blocking::Client; +use reqwest::Client; use reqwest::Url; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -83,9 +82,10 @@ impl StackExchange { // TODO also return a future with the rest of the questions /// Search query at stack exchange and get the top answer body - pub fn search_lucky(&self, q: &str) -> Result<String> { + pub async fn search_lucky(&self, q: &str) -> Result<String> { let ans = self - .search_advanced(q, 1)? + .search_advanced(q, 1) + .await? .into_iter() .next() .ok_or(Error::NoResults)? @@ -99,15 +99,15 @@ impl StackExchange { } /// Search query at stack exchange and get a list of relevant questions - pub fn search(&self, q: &str) -> Result<Vec<Question>> { - self.search_advanced(q, self.config.limit) + pub async fn search(&self, q: &str) -> Result<Vec<Question>> { + self.search_advanced(q, self.config.limit).await } /// Search against the search/advanced endpoint with a given query. /// Only fetches questions that have at least one answer. /// TODO async /// TODO parallel requests over multiple sites - fn search_advanced(&self, q: &str, limit: u16) -> Result<Vec<Question>> { - let resp_body = self + async fn search_advanced(&self, q: &str, limit: u16) -> Result<Vec<Question>> { + Ok(self .client .get(stackexchange_url("search/advanced")) .header("Accepts", "application/json") @@ -120,24 +120,18 @@ impl StackExchange { ("order", "desc"), ("sort", "relevance"), ]) - .send()?; - - let gz = GzDecoder::new(resp_body); - let wrapper: ResponseWrapper<Question> = serde_json::from_reader(gz).map_err(|e| { - Error::StackExchange(format!( - "Error decoding questions from the StackExchange API: {}", - e - )) - })?; - let qs = wrapper + .send() + .await? + .json::<ResponseWrapper<Question>>() + .await? .items .into_iter() .map(|mut q| { + // TODO parallelize this (and preprocess <kbd> stuff too) q.answers.sort_unstable_by_key(|a| -a.score); q }) - .collect(); - Ok(qs) + .collect()) } fn get_default_opts(&self) -> HashMap<&str, &str> { @@ -163,10 +157,10 @@ impl LocalStorage { } // TODO inform user if we are downloading - pub fn sites(&mut self) -> Result<&Vec<Site>> { + pub async fn sites(&mut self) -> Result<&Vec<Site>> { // Stop once Option ~ Some or Result ~ Err if self.sites.is_none() && !self.fetch_local_sites()? { - self.fetch_remote_sites()?; + self.fetch_remote_sites().await?; } match &self.sites { Some(sites) if sites.is_empty() => Err(Error::EmptySites), @@ -175,13 +169,14 @@ impl LocalStorage { } } - pub fn update_sites(&mut self) -> Result<()> { - self.fetch_remote_sites() + pub async fn update_sites(&mut self) -> Result<()> { + self.fetch_remote_sites().await } - pub fn validate_site(&mut self, site_code: &str) -> Result<bool> { + pub async fn validate_site(&mut self, site_code: &str) -> Result<bool> { Ok(self - .sites()? + .sites() + .await? .iter() .any(|site| site.api_site_parameter == *site_code)) } @@ -198,23 +193,21 @@ impl LocalStorage { } // TODO decide whether or not I should give LocalStorage an api key.. - fn fetch_remote_sites(&mut self) -> Result<()> { - let resp_body = Client::new() - .get(stackexchange_url("sites")) - .header("Accepts", "application/json") - .query(&[ - ("pagesize", SE_SITES_PAGESIZE.to_string()), - ("page", "1".to_string()), - ]) - .send()?; - let gz = GzDecoder::new(resp_body); - let wrapper: ResponseWrapper<Site> = serde_json::from_reader(gz).map_err(|e| { - Error::StackExchange(format!( - "Error decoding sites from the StackExchange API: {}", - e - )) - })?; - self.sites = Some(wrapper.items); + async fn fetch_remote_sites(&mut self) -> Result<()> { + self.sites = Some( + Client::new() + .get(stackexchange_url("sites")) + .header("Accepts", "application/json") + .query(&[ + ("pagesize", SE_SITES_PAGESIZE.to_string()), + ("page", "1".to_string()), + ]) + .send() + .await? + .json::<ResponseWrapper<Site>>() + .await? + .items, + ); self.store_local_sites() } |