concinnity_host/store/paths/
tree.rs1use std::path::{Path, PathBuf};
9
10const ASSETS_DIR: &str = "assets";
14const DATA_DIR: &str = "data";
15const WORLD_LOCK_FILE: &str = "world-lock.json";
16const WORLDS_DIR: &str = "worlds";
17const SAVES_DIR: &str = "saves";
18const PREVIEW_SAVES_DIR: &str = "preview-saves";
19const SETTINGS_FILE: &str = "settings";
20const CRASHES_DIR: &str = "crashes";
21const EDITOR_SESSION_FILE: &str = "editor";
22const CACHE_DIR: &str = "cache";
23const RUNTIME_CACHE_SEGMENT: &str = "0";
24const BUILD_CACHE_SEGMENT: &str = "1";
25
26#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct StateTree {
49 content: PathBuf,
50 build: Option<PathBuf>,
51 writable: Option<PathBuf>,
52 cache: Option<PathBuf>,
53}
54
55impl StateTree {
56 pub fn at<P: Into<PathBuf>>(content: P) -> Self {
58 Self {
59 content: content.into(),
60 build: None,
61 writable: None,
62 cache: None,
63 }
64 }
65
66 #[must_use]
70 pub fn with_build<P: Into<PathBuf>>(mut self, dir: P) -> Self {
71 self.build = Some(dir.into());
72 self
73 }
74
75 #[must_use]
78 pub fn with_writable<P: Into<PathBuf>>(mut self, dir: P) -> Self {
79 self.writable = Some(dir.into());
80 self
81 }
82
83 #[must_use]
87 pub fn with_cache<P: Into<PathBuf>>(mut self, dir: P) -> Self {
88 self.cache = Some(dir.into());
89 self
90 }
91
92 pub fn content_root(&self) -> &Path {
94 &self.content
95 }
96
97 pub fn build_root(&self) -> &Path {
99 self.build.as_deref().unwrap_or(&self.content)
100 }
101
102 pub fn writable_root(&self) -> &Path {
104 self.writable.as_deref().unwrap_or(&self.content)
105 }
106
107 pub fn assets_dir(&self) -> PathBuf {
109 self.content.join(ASSETS_DIR)
110 }
111
112 pub fn worlds_dir(&self) -> PathBuf {
114 self.content.join(WORLDS_DIR)
115 }
116
117 pub fn data_dir(&self) -> PathBuf {
120 self.build_root().join(DATA_DIR)
121 }
122
123 pub fn world_lock_path(&self) -> PathBuf {
126 self.build_root().join(WORLD_LOCK_FILE)
127 }
128
129 pub fn saves_dir(&self) -> PathBuf {
132 self.writable_root().join(SAVES_DIR)
133 }
134
135 pub fn preview_saves_dir(&self) -> PathBuf {
139 self.writable_root().join(PREVIEW_SAVES_DIR)
140 }
141
142 pub fn settings_path(&self) -> PathBuf {
145 self.writable_root().join(SETTINGS_FILE)
146 }
147
148 pub fn crashes_dir(&self) -> PathBuf {
151 self.writable_root().join(CRASHES_DIR)
152 }
153
154 pub fn editor_session_path(&self) -> PathBuf {
157 self.writable_root().join(EDITOR_SESSION_FILE)
158 }
159
160 pub fn runtime_cache_path(&self) -> PathBuf {
168 self.cache_root_for_runtime()
169 .join(CACHE_DIR)
170 .join(RUNTIME_CACHE_SEGMENT)
171 }
172
173 pub fn bundled_runtime_cache_path(&self) -> PathBuf {
184 self.build_root()
185 .join(CACHE_DIR)
186 .join(RUNTIME_CACHE_SEGMENT)
187 }
188
189 pub fn build_cache_path(&self) -> PathBuf {
195 self.cache_root_for_build()
196 .join(CACHE_DIR)
197 .join(BUILD_CACHE_SEGMENT)
198 }
199
200 fn cache_root_for_runtime(&self) -> &Path {
203 self.cache
204 .as_deref()
205 .unwrap_or_else(|| self.writable_root())
206 }
207
208 fn cache_root_for_build(&self) -> &Path {
212 self.cache.as_deref().unwrap_or_else(|| self.build_root())
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
223 fn one_root_resolves_the_whole_layout() {
224 let tree = StateTree::at("/flat");
225 let root = Path::new("/flat");
226
227 assert_eq!(tree.content_root(), root);
228 assert_eq!(tree.build_root(), root);
229 assert_eq!(tree.writable_root(), root);
230 assert_eq!(tree.assets_dir(), root.join("assets"));
231 assert_eq!(tree.data_dir(), root.join("data"));
232 assert_eq!(tree.world_lock_path(), root.join("world-lock.json"));
233 assert_eq!(tree.worlds_dir(), root.join("worlds"));
234 assert_eq!(tree.saves_dir(), root.join("saves"));
235 assert_eq!(tree.preview_saves_dir(), root.join("preview-saves"));
236 assert_eq!(tree.settings_path(), root.join("settings"));
237 assert_eq!(tree.crashes_dir(), root.join("crashes"));
238 assert_eq!(tree.editor_session_path(), root.join("editor"));
239 assert_eq!(tree.runtime_cache_path(), root.join("cache").join("0"));
240 assert_eq!(tree.build_cache_path(), root.join("cache").join("1"));
241 assert_eq!(tree.bundled_runtime_cache_path(), tree.runtime_cache_path());
244 }
245
246 #[test]
249 fn a_writable_root_moves_only_what_the_application_writes() {
250 let content = Path::new("/opt/MyGame");
251 let writable = Path::new("/home/u/.local/share/MyGame");
252 let tree = StateTree::at(content).with_writable(writable);
253
254 assert_eq!(tree.content_root(), content);
255 assert_eq!(tree.writable_root(), writable);
256 assert_eq!(tree.saves_dir(), writable.join("saves"));
257 assert_eq!(tree.preview_saves_dir(), writable.join("preview-saves"));
258 assert_eq!(tree.settings_path(), writable.join("settings"));
259 assert_eq!(tree.crashes_dir(), writable.join("crashes"));
260 assert_eq!(tree.editor_session_path(), writable.join("editor"));
261 assert_eq!(tree.runtime_cache_path(), writable.join("cache").join("0"));
262
263 assert_eq!(tree.data_dir(), content.join("data"));
264 assert_eq!(tree.world_lock_path(), content.join("world-lock.json"));
265 assert_eq!(tree.assets_dir(), content.join("assets"));
266 assert_eq!(tree.worlds_dir(), content.join("worlds"));
267 assert_eq!(tree.build_cache_path(), content.join("cache").join("1"));
268 assert_eq!(
271 tree.bundled_runtime_cache_path(),
272 content.join("cache").join("0")
273 );
274 }
275
276 #[test]
280 fn a_cache_root_moves_both_segments_and_nothing_else() {
281 let content = Path::new("/build/content");
282 let cache = Path::new("/var/cache/mygame");
283 let tree = StateTree::at(content).with_cache(cache);
284
285 assert_eq!(tree.runtime_cache_path(), cache.join("cache").join("0"));
286 assert_eq!(tree.build_cache_path(), cache.join("cache").join("1"));
287 assert_eq!(
289 tree.bundled_runtime_cache_path(),
290 content.join("cache").join("0")
291 );
292 assert_eq!(tree.data_dir(), content.join("data"));
293 assert_eq!(tree.saves_dir(), content.join("saves"));
294 }
295
296 #[test]
299 fn all_four_roots_split_independently() {
300 let tree = StateTree::at("/opt/app")
301 .with_build("/opt/app/out")
302 .with_writable("/home/u/app")
303 .with_cache("/var/cache/app");
304
305 assert_eq!(tree.assets_dir(), Path::new("/opt/app/assets"));
306 assert_eq!(tree.data_dir(), Path::new("/opt/app/out/data"));
307 assert_eq!(tree.saves_dir(), Path::new("/home/u/app/saves"));
308 assert_eq!(
309 tree.runtime_cache_path(),
310 Path::new("/var/cache/app/cache/0")
311 );
312 assert_eq!(tree.build_cache_path(), Path::new("/var/cache/app/cache/1"));
313 }
314
315 #[test]
318 fn a_build_root_hides_the_output_and_leaves_the_authored_content_visible() {
319 let project = Path::new("/proj");
320 let hidden = Path::new("/proj/.concinnity");
321 let tree = StateTree::at(project)
322 .with_writable(hidden)
323 .with_build(hidden);
324
325 assert_eq!(tree.assets_dir(), project.join("assets"));
326 assert_eq!(tree.worlds_dir(), project.join("worlds"));
327
328 assert_eq!(tree.data_dir(), hidden.join("data"));
329 assert_eq!(tree.world_lock_path(), hidden.join("world-lock.json"));
330 assert_eq!(tree.settings_path(), hidden.join("settings"));
331 assert_eq!(tree.saves_dir(), hidden.join("saves"));
332 assert_eq!(tree.editor_session_path(), hidden.join("editor"));
333 assert_eq!(tree.runtime_cache_path(), hidden.join("cache").join("0"));
336 assert_eq!(tree.build_cache_path(), hidden.join("cache").join("1"));
337 assert_eq!(
338 tree.bundled_runtime_cache_path(),
339 hidden.join("cache").join("0")
340 );
341 }
342
343 #[test]
346 fn a_cache_root_outranks_the_build_root() {
347 let tree = StateTree::at("/proj")
348 .with_build("/proj/.concinnity")
349 .with_cache("/var/cache/app");
350
351 assert_eq!(tree.build_cache_path(), Path::new("/var/cache/app/cache/1"));
352 assert_eq!(tree.data_dir(), Path::new("/proj/.concinnity/data"));
353 }
354
355 #[test]
358 fn builders_do_not_disturb_each_other() {
359 let base = StateTree::at("/root");
360 assert_eq!(
361 base.clone().with_cache("/c").saves_dir(),
362 base.saves_dir(),
363 "a cache root leaves the writable state alone"
364 );
365 assert_eq!(
366 base.clone().with_writable("/w").build_cache_path(),
367 base.build_cache_path(),
368 "a writable root leaves the build segment with the content"
369 );
370 assert_eq!(
371 base.clone().with_build("/b").assets_dir(),
372 base.assets_dir(),
373 "a build root leaves the authored content where it is"
374 );
375 assert_eq!(
376 base.clone().with_build("/b").saves_dir(),
377 base.saves_dir(),
378 "a build root leaves the writable state alone"
379 );
380 }
381}