perseus/utils/
cache_res.rs

1use std::any::Any;
2use std::convert::Infallible;
3
4use futures::Future;
5use serde::{Deserialize, Serialize};
6use tokio::fs::{create_dir_all, File};
7use tokio::io::{AsyncReadExt, AsyncWriteExt};
8
9/// Runs the given function once and then caches the result to the filesystem
10/// for future execution. Think of this as filesystem-level memoizing. In
11/// future, this will be broken out into its own crate and wrapped by Perseus.
12/// The second parameter to this allows forcing the function to re-fetch data
13/// every time, which is useful if you want to revalidate data or test
14/// your fetching logic again. Note that a change to the logic will not trigger
15/// a reload unless you make it do so. For this reason, it's recommended to only
16/// use this wrapper once you've tested your fetching logic.
17///
18/// When running automated tests, you may wish to set `force_run` to the result
19/// of an environment variable check that you'll use when testing.
20///
21/// # Panics
22/// If this filesystem operations fail, this function will panic. It can't
23/// return a graceful error since it's expected to return the type you
24/// requested.
25pub async fn cache_res<D, F, Ft>(name: &str, f: F, force_run: bool) -> D
26where
27    // By making this `Any`, we can downcast it to manage errors intuitively
28    D: Serialize + for<'de> Deserialize<'de> + Any,
29    F: Fn() -> Ft,
30    Ft: Future<Output = D>,
31{
32    let f_res = || async { Ok::<D, Infallible>(f().await) };
33    // This can't fail, we just invented an error type for an infallible function
34    cache_fallible_res(name, f_res, force_run).await.unwrap()
35}
36
37/// Same as `cache_res`, but takes a function that returns a `Result`, allowing
38/// you to use `?` and the like inside your logic.
39pub async fn cache_fallible_res<D, E, F, Ft>(name: &str, f: F, force_run: bool) -> Result<D, E>
40where
41    // By making this `Any`, we can downcast it to manage errors intuitively
42    D: Serialize + for<'de> Deserialize<'de>,
43    E: std::error::Error,
44    F: Fn() -> Ft,
45    Ft: Future<Output = Result<D, E>>,
46{
47    // Replace any slashes with dashes to keep a flat directory structure
48    let name = name.replace('/', "-");
49    // In production, we'll just run the function directly
50    if cfg!(debug_assertions) {
51        // Check if the cache file exists
52        let filename = format!("dist/cache/{}.json", &name);
53        match File::open(&filename).await {
54            Ok(mut file) => {
55                if force_run {
56                    let res = f().await?;
57                    // Now cache the result
58                    let str_res = serde_json::to_string(&res).unwrap_or_else(|err| {
59                        panic!(
60                            "couldn't serialize result of entry '{}' for caching: {}",
61                            &filename, err
62                        )
63                    });
64                    let mut file = File::create(&filename).await.unwrap_or_else(|err| {
65                        panic!(
66                            "couldn't create cache file for entry '{}': {}",
67                            &filename, err
68                        )
69                    });
70                    file.write_all(str_res.as_bytes())
71                        .await
72                        .unwrap_or_else(|err| {
73                            panic!(
74                                "couldn't write cache to file for entry '{}': {}",
75                                &filename, err
76                            )
77                        });
78
79                    Ok(res)
80                } else {
81                    let mut contents = String::new();
82                    file.read_to_string(&mut contents)
83                        .await
84                        .unwrap_or_else(|err| {
85                            panic!(
86                                "couldn't read cache from file for entry '{}': {}",
87                                &filename, err
88                            )
89                        });
90                    let res = match serde_json::from_str(&contents) {
91                        Ok(cached_res) => cached_res,
92                        // If the stuff in the cache can't be deserialized, we'll force a recreation
93                        // (we don't recurse because that requires boxing the future)
94                        Err(_) => {
95                            let res = f().await?;
96                            // Now cache the result
97                            let str_res = serde_json::to_string(&res).unwrap_or_else(|err| {
98                                panic!(
99                                    "couldn't serialize result of entry '{}' for caching: {}",
100                                    &filename, err
101                                )
102                            });
103                            let mut file = File::create(&filename).await.unwrap_or_else(|err| {
104                                panic!(
105                                    "couldn't create cache file for entry '{}': {}",
106                                    &filename, err
107                                )
108                            });
109                            file.write_all(str_res.as_bytes())
110                                .await
111                                .unwrap_or_else(|err| {
112                                    panic!(
113                                        "couldn't write cache to file for entry '{}': {}",
114                                        &filename, err
115                                    )
116                                });
117
118                            res
119                        }
120                    };
121
122                    Ok(res)
123                }
124            }
125            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
126                // The file doesn't exist yet, create the parent cache directory
127                create_dir_all("dist/cache")
128                    .await
129                    .unwrap_or_else(|err| panic!("couldn't create cache directory: {}", err));
130                // We have no cache, so we'll have to run the function
131                let res = f().await?;
132                // Now cache the result
133                let str_res = serde_json::to_string(&res).unwrap_or_else(|err| {
134                    panic!(
135                        "couldn't serialize result of entry '{}' for caching: {}",
136                        &filename, err
137                    )
138                });
139                let mut file = File::create(&filename).await.unwrap_or_else(|err| {
140                    panic!(
141                        "couldn't create cache file for entry '{}': {}",
142                        &filename, err
143                    )
144                });
145                file.write_all(str_res.as_bytes())
146                    .await
147                    .unwrap_or_else(|err| {
148                        panic!(
149                            "couldn't write cache to file for entry '{}': {}",
150                            &filename, err
151                        )
152                    });
153
154                Ok(res)
155            }
156            // Any other filesystem errors are unacceptable
157            Err(err) => panic!(
158                "filesystem error occurred while trying to read cache file for entry '{}': {}",
159                &filename, err
160            ),
161        }
162    } else {
163        f().await
164    }
165}