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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
use std::any::Any;
use std::convert::Infallible;

use futures::Future;
use serde::{Deserialize, Serialize};
use tokio::fs::{create_dir_all, File};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

/// Runs the given function once and then caches the result to the filesystem
/// for future execution. Think of this as filesystem-level memoizing. In
/// future, this will be broken out into its own crate and wrapped by Perseus.
/// The second parameter to this allows forcing the function to re-fetch data
/// every time, which is useful if you want to revalidate data or test
/// your fetching logic again. Note that a change to the logic will not trigger
/// a reload unless you make it do so. For this reason, it's recommended to only
/// use this wrapper once you've tested your fetching logic.
///
/// When running automated tests, you may wish to set `force_run` to the result
/// of an environment variable check that you'll use when testing.
///
/// # Panics
/// If this filesystem operations fail, this function will panic. It can't
/// return a graceful error since it's expected to return the type you
/// requested.
pub async fn cache_res<D, F, Ft>(name: &str, f: F, force_run: bool) -> D
where
    // By making this `Any`, we can downcast it to manage errors intuitively
    D: Serialize + for<'de> Deserialize<'de> + Any,
    F: Fn() -> Ft,
    Ft: Future<Output = D>,
{
    let f_res = || async { Ok::<D, Infallible>(f().await) };
    // This can't fail, we just invented an error type for an infallible function
    cache_fallible_res(name, f_res, force_run).await.unwrap()
}

/// Same as `cache_res`, but takes a function that returns a `Result`, allowing
/// you to use `?` and the like inside your logic.
pub async fn cache_fallible_res<D, E, F, Ft>(name: &str, f: F, force_run: bool) -> Result<D, E>
where
    // By making this `Any`, we can downcast it to manage errors intuitively
    D: Serialize + for<'de> Deserialize<'de>,
    E: std::error::Error,
    F: Fn() -> Ft,
    Ft: Future<Output = Result<D, E>>,
{
    // Replace any slashes with dashes to keep a flat directory structure
    let name = name.replace('/', "-");
    // In production, we'll just run the function directly
    if cfg!(debug_assertions) {
        // Check if the cache file exists
        let filename = format!("dist/cache/{}.json", &name);
        match File::open(&filename).await {
            Ok(mut file) => {
                if force_run {
                    let res = f().await?;
                    // Now cache the result
                    let str_res = serde_json::to_string(&res).unwrap_or_else(|err| {
                        panic!(
                            "couldn't serialize result of entry '{}' for caching: {}",
                            &filename, err
                        )
                    });
                    let mut file = File::create(&filename).await.unwrap_or_else(|err| {
                        panic!(
                            "couldn't create cache file for entry '{}': {}",
                            &filename, err
                        )
                    });
                    file.write_all(str_res.as_bytes())
                        .await
                        .unwrap_or_else(|err| {
                            panic!(
                                "couldn't write cache to file for entry '{}': {}",
                                &filename, err
                            )
                        });

                    Ok(res)
                } else {
                    let mut contents = String::new();
                    file.read_to_string(&mut contents)
                        .await
                        .unwrap_or_else(|err| {
                            panic!(
                                "couldn't read cache from file for entry '{}': {}",
                                &filename, err
                            )
                        });
                    let res = match serde_json::from_str(&contents) {
                        Ok(cached_res) => cached_res,
                        // If the stuff in the cache can't be deserialized, we'll force a recreation
                        // (we don't recurse because that requires boxing the future)
                        Err(_) => {
                            let res = f().await?;
                            // Now cache the result
                            let str_res = serde_json::to_string(&res).unwrap_or_else(|err| {
                                panic!(
                                    "couldn't serialize result of entry '{}' for caching: {}",
                                    &filename, err
                                )
                            });
                            let mut file = File::create(&filename).await.unwrap_or_else(|err| {
                                panic!(
                                    "couldn't create cache file for entry '{}': {}",
                                    &filename, err
                                )
                            });
                            file.write_all(str_res.as_bytes())
                                .await
                                .unwrap_or_else(|err| {
                                    panic!(
                                        "couldn't write cache to file for entry '{}': {}",
                                        &filename, err
                                    )
                                });

                            res
                        }
                    };

                    Ok(res)
                }
            }
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
                // The file doesn't exist yet, create the parent cache directory
                create_dir_all("dist/cache")
                    .await
                    .unwrap_or_else(|err| panic!("couldn't create cache directory: {}", err));
                // We have no cache, so we'll have to run the function
                let res = f().await?;
                // Now cache the result
                let str_res = serde_json::to_string(&res).unwrap_or_else(|err| {
                    panic!(
                        "couldn't serialize result of entry '{}' for caching: {}",
                        &filename, err
                    )
                });
                let mut file = File::create(&filename).await.unwrap_or_else(|err| {
                    panic!(
                        "couldn't create cache file for entry '{}': {}",
                        &filename, err
                    )
                });
                file.write_all(str_res.as_bytes())
                    .await
                    .unwrap_or_else(|err| {
                        panic!(
                            "couldn't write cache to file for entry '{}': {}",
                            &filename, err
                        )
                    });

                Ok(res)
            }
            // Any other filesystem errors are unacceptable
            Err(err) => panic!(
                "filesystem error occurred while trying to read cache file for entry '{}': {}",
                &filename, err
            ),
        }
    } else {
        f().await
    }
}