diff options
author | Sam Tay <sam.chong.tay@gmail.com> | 2020-06-05 13:59:34 -0700 |
---|---|---|
committer | Sam Tay <sam.chong.tay@gmail.com> | 2020-06-06 19:56:12 -0700 |
commit | b06f305db319b90ff55e159a8538bac853ca2168 (patch) | |
tree | ac7e6559346f28c5cb4852a66b213732ab8b6d8e /src/stackexchange.rs | |
parent | ca754e6254d9e0453dd0e02d700ea0ac3dd9d6a5 (diff) |
Fetch and cache SE sites
Diffstat (limited to 'src/stackexchange.rs')
-rw-r--r-- | src/stackexchange.rs | 88 |
1 files changed, 81 insertions, 7 deletions
diff --git a/src/stackexchange.rs b/src/stackexchange.rs index 79b389f..d98643d 100644 --- a/src/stackexchange.rs +++ b/src/stackexchange.rs @@ -2,10 +2,13 @@ use anyhow; use flate2::read::GzDecoder; use reqwest::blocking::Client; use reqwest::Url; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::fs; +use std::fs::File; +use std::path::PathBuf; -use crate::config::Config; +use crate::config::{project_dir, Config}; /// StackExchange API v2.2 URL const SE_URL: &str = "http://api.stackexchange.com/2.2/"; @@ -15,13 +18,22 @@ const SE_URL: &str = "http://api.stackexchange.com/2.2/"; /// [create filter](https://api.stackexchange.com/docs/create-filter). const SE_FILTER: &str = ".DND5X2VHHUH8HyJzpjo)5NvdHI3w6auG"; -/// This structure allows intercting with parts of the StackExchange +/// Pagesize when fetching all SE sites. Should be good for many years... +const SE_SITES_PAGESIZE: u16 = 10000; + +/// This structure allows interacting with parts of the StackExchange /// API, using the `Config` struct to determine certain API settings and options. pub struct StackExchange { client: Client, config: Config, } +/// This structure allows interacting with locally cached StackExchange metadata. +pub struct LocalStorage { + sites: Option<Vec<Site>>, + filename: PathBuf, +} + #[derive(Deserialize, Serialize, Debug)] pub struct Site { api_site_parameter: String, @@ -55,8 +67,8 @@ pub struct Question { /// Internal struct that represents the boilerplate response wrapper from SE API. #[derive(Deserialize, Debug)] -struct ResponseWrapper { - items: Vec<Question>, +struct ResponseWrapper<T> { + items: Vec<T>, } impl StackExchange { @@ -85,7 +97,7 @@ impl StackExchange { ]) .send()?; let gz = GzDecoder::new(resp_body); - let wrapper: ResponseWrapper = serde_json::from_reader(gz)?; + let wrapper: ResponseWrapper<Question> = serde_json::from_reader(gz)?; let qs = wrapper .items .into_iter() @@ -100,12 +112,74 @@ impl StackExchange { fn get_default_opts(&self) -> HashMap<&str, &str> { let mut params = HashMap::new(); params.insert("site", self.config.site.as_str()); - params.insert("key", self.config.api_key.as_str()); params.insert("filter", &SE_FILTER); + if let Some(key) = &self.config.api_key { + params.insert("key", key.as_str()); + } params } } +impl LocalStorage { + pub fn new() -> Self { + let project = project_dir(); + let dir = project.cache_dir(); + fs::create_dir_all(&dir); + LocalStorage { + sites: None, + filename: dir.join("sites.json"), + } + } + + // TODO this function is disgusting; how do in idiomatic rust? + // TODO make this async, inform user if we are downloading + pub fn sites(&mut self) -> &Vec<Site> { + if let Some(ref sites) = self.sites { + return sites; + } + self.fetch_local_sites(); + if let Some(ref sites) = self.sites { + return sites; + } + self.fetch_remote_sites(); + self.sites.as_ref().unwrap() + } + + pub fn update_sites(&mut self) { + self.fetch_remote_sites() + } + + fn fetch_local_sites(&mut self) { + if let Ok(file) = File::open(&self.filename) { + self.sites = serde_json::from_reader(file) + .expect("Local cache corrupted; try running `so --update-sites`") + } + } + + // TODO decide whether or not I should give LocalStorage an api key.. + // TODO cool loading animation? + fn fetch_remote_sites(&mut self) { + let resp_body = Client::new() + .get(stackechange_url("sites")) + .header("Accepts", "application/json") + .query(&[ + ("pagesize", SE_SITES_PAGESIZE.to_string()), + ("page", "1".to_string()), + ]) + .send() + .unwrap(); // TODO inspect response for errors e.g. throttle + let gz = GzDecoder::new(resp_body); + let wrapper: ResponseWrapper<Site> = serde_json::from_reader(gz).unwrap(); // TODO + self.sites = Some(wrapper.items); + self.store_local_sites(); + } + + fn store_local_sites(&self) { + let file = File::create(&self.filename).unwrap(); + serde_json::to_writer(file, &self.sites).unwrap(); + } +} + /// Creates url from const string; can technically panic fn stackechange_url(path: &str) -> Url { let mut url = Url::parse(SE_URL).unwrap(); |