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}