1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
use std::collections::HashMap;
use std::fs;
use std::ops::Deref;
use std::path::Path;
use crate::config::Config;
use crate::error::{Error, Result};
use crate::utils;
use super::api::{Api, Site};
use super::Question;
/// This structure allows interacting with locally cached StackExchange metadata.
pub struct LocalStorage {
pub sites: Vec<Site>,
}
impl LocalStorage {
fn fetch_local_sites(filename: &Path) -> Result<Option<Vec<Site>>> {
if let Some(file) = utils::open_file(filename)? {
return serde_json::from_reader(file)
.map_err(|_| Error::MalformedFile(filename.to_path_buf()));
}
Ok(None)
}
fn store_local_sites(filename: &Path, sites: &[Site]) -> Result<()> {
let file = utils::create_file(filename)?;
serde_json::to_writer(file, sites)?;
Ok(())
}
async fn init_sites(filename: &Path, update: bool) -> Result<Vec<Site>> {
if !update {
if let Some(sites) = Self::fetch_local_sites(filename)? {
return Ok(sites);
}
}
let sites = Api::new(None).sites().await?;
Self::store_local_sites(filename, &sites)?;
Ok(sites)
}
pub async fn new(update: bool) -> Result<Self> {
let project = Config::project_dir()?;
let dir = project.cache_dir();
fs::create_dir_all(&dir)?;
let sites_filename = dir.join("sites.json");
let sites = Self::init_sites(&sites_filename, update).await?;
Ok(LocalStorage { sites })
}
// TODO is this HM worth it? Probably only will ever have < 10 site codes to search...
// maybe store this as Option<HM> on self if other methods use it...
pub async fn find_invalid_site<'a, 'b>(
&'b self,
site_codes: &'a [String],
) -> Option<&'a String> {
let hm: HashMap<&str, ()> = self
.sites
.iter()
.map(|site| (site.api_site_parameter.as_str(), ()))
.collect();
site_codes.iter().find(|s| !hm.contains_key(&s.as_str()))
}
pub fn get_site_map(&self, site_codes: &[String]) -> SiteMap {
let inner = self
.sites
.iter()
.filter_map(move |site| {
let _ = site_codes
.iter()
.find(|&sc| *sc == site.api_site_parameter)?;
Some((site.api_site_parameter.to_owned(), site.site_url.to_owned()))
})
.collect();
SiteMap { inner }
}
}
/// Just a map of site codes to site URLs, shareable across the app. These are
/// only the sites relevant to the configuration / query, not all cached SE
/// sites.
#[derive(Debug)]
pub struct SiteMap {
inner: HashMap<String, String>,
}
impl Deref for SiteMap {
type Target = HashMap<String, String>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl SiteMap {
/// Get SE answer url. Panics if site was not set on the question, or
/// site code not found in the map.
pub fn answer_url<S>(&self, question: &Question<S>, answer_id: u32) -> String {
// answer link actually doesn't need question id
let site_url = self.site_url(question);
format!("https://{site_url}/a/{answer_id}")
}
/// Get SE question url. Panics if site was not set on the question, or
/// site code not found in the map.
pub fn question_url<S>(&self, question: &Question<S>) -> String {
// answer link actually doesn't need question id
let question_id = question.id;
let site_url = self.site_url(question);
format!("https://{site_url}/q/{question_id}")
}
fn site_url<S>(&self, question: &Question<S>) -> String {
self.inner
.get(
question
.site
.as_ref()
.expect("bug: site not attached to question"),
)
.cloned()
.expect("bug: lost a site")
}
}
|