summaryrefslogtreecommitdiffstats
path: root/src/stackexchange/local_storage.rs
blob: 1d0af2cee1085bc388a4b8cbdecd4cd40decc128 (plain)
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")
    }
}