1use std::path::{Path, PathBuf};
14
15use crate::file::CONTAINER_EXTENSION;
16use crate::store::container::ContainerError;
17use crate::store::handle::{Clock, Store};
18
19#[derive(Clone, Debug, Default, PartialEq, Eq)]
21pub enum Documents {
22 #[default]
26 Nowhere,
27 In(PathBuf),
28}
29
30#[derive(Debug, thiserror::Error)]
34pub enum BirthFailure {
35 #[error("there is no documents directory to create it in")]
36 Nowhere,
37 #[error("no unused name in {} tries", Documents::TRIES)]
38 Unnamed,
39 #[error(transparent)]
40 Create(#[from] ContainerError),
41}
42
43impl BirthFailure {
44 pub fn notice(&self) -> String {
47 format!("Failed to create a new diagram: {self}")
48 }
49}
50
51impl Documents {
52 const TRIES: usize = 16;
56
57 pub fn platform() -> Self {
61 match dirs::document_dir()
62 .or_else(dirs::home_dir)
63 .or_else(|| std::env::current_dir().ok())
64 {
65 Some(dir) => Documents::In(dir),
66 None => Documents::Nowhere,
67 }
68 }
69
70 pub fn at(dir: impl Into<PathBuf>) -> Self {
71 Documents::In(dir.into())
72 }
73
74 pub fn dir(&self) -> Option<&Path> {
75 match self {
76 Documents::Nowhere => None,
77 Documents::In(dir) => Some(dir),
78 }
79 }
80
81 pub fn create(&self, mut draw: impl FnMut() -> u32) -> Result<Store, BirthFailure> {
89 let Documents::In(dir) = self else {
90 return Err(BirthFailure::Nowhere);
91 };
92 std::fs::create_dir_all(dir).map_err(|e| BirthFailure::Create(e.into()))?;
93 for _ in 0..Self::TRIES {
94 let root = dir.join(format!("{}.{CONTAINER_EXTENSION}", name(&mut draw)));
95 if root.exists() {
96 continue;
97 }
98 match Store::create(&root, Clock::System) {
99 Err(ContainerError::Exists(_)) => {}
101 Err(e) => return Err(BirthFailure::Create(e)),
102 Ok(store) => return Ok(store),
103 }
104 }
105 Err(BirthFailure::Unnamed)
106 }
107}
108
109pub fn name(draw: &mut impl FnMut() -> u32) -> String {
112 fn pick<'a>(list: &[&'a str], drawn: u32) -> &'a str {
113 list[drawn as usize % list.len()]
114 }
115 let adjective = pick(ADJECTIVES, draw());
116 let noun = pick(NOUNS, draw());
117 let creature = pick(CREATURES, draw());
118 format!("{adjective}-{noun}-{creature}")
119}
120
121pub fn entropy() -> u32 {
126 let id = uuid::Uuid::new_v4();
127 let bytes = id.as_bytes();
128 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
129}
130
131const ADJECTIVES: &[&str] = &[
138 "amber",
139 "bold",
140 "brave",
141 "brisk",
142 "calm",
143 "clever",
144 "cosmic",
145 "crimson",
146 "curious",
147 "dapper",
148 "eager",
149 "electric",
150 "fearless",
151 "gentle",
152 "gilded",
153 "golden",
154 "happy",
155 "hidden",
156 "jolly",
157 "keen",
158 "lucky",
159 "merry",
160 "mighty",
161 "noble",
162 "patient",
163 "plucky",
164 "polished",
165 "quick",
166 "quiet",
167 "radiant",
168 "rapid",
169 "rustic",
170 "serene",
171 "silent",
172 "silver",
173 "smooth",
174 "snowy",
175 "solar",
176 "spry",
177 "sunny",
178 "swift",
179 "tidy",
180 "tranquil",
181 "velvet",
182 "vivid",
183 "wandering",
184 "witty",
185 "zesty",
186];
187
188const NOUNS: &[&str] = &[
189 "anchor", "autumn", "beacon", "blossom", "breeze", "brook", "canyon", "cedar", "cinder",
190 "comet", "coral", "crescent", "dawn", "delta", "ember", "fern", "forest", "garnet", "glacier",
191 "harbor", "harvest", "horizon", "island", "lantern", "ledger", "lichen", "meadow", "meridian",
192 "mirage", "monsoon", "nebula", "orchard", "pebble", "prairie", "quartz", "ridge", "river",
193 "sable", "summit", "sunrise", "sunshine", "thicket", "thunder", "tundra", "valley", "willow",
194 "window", "zephyr",
195];
196
197const CREATURES: &[&str] = &[
198 "badger", "bison", "cricket", "dolphin", "falcon", "ferret", "finch", "fox", "gecko", "gibbon",
199 "heron", "ibis", "jackal", "jaguar", "kestrel", "koala", "lemur", "lynx", "magpie", "marlin",
200 "marmot", "mole", "narwhal", "newt", "ocelot", "osprey", "otter", "panda", "pelican",
201 "penguin", "puffin", "quail", "rabbit", "raven", "robin", "salmon", "sparrow", "stoat",
202 "tapir", "teal", "toad", "turtle", "viper", "walrus", "weasel", "wombat", "wren", "yak",
203];
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::store::container::MANIFEST;
209
210 fn counting() -> impl FnMut() -> u32 {
213 let mut n = 0;
214 move || {
215 n += 1;
216 n - 1
217 }
218 }
219
220 #[test]
221 fn a_name_is_three_words_drawn_one_per_word() {
222 let mut draw = counting();
223 assert_eq!(
224 name(&mut draw),
225 format!("{}-{}-{}", ADJECTIVES[0], NOUNS[1], CREATURES[2]),
226 "the draws are consumed left to right, one per list",
227 );
228 let next = name(&mut draw);
229 assert_eq!(next.split('-').count(), 3, "{next} is not three words");
230 assert!(
231 next.chars().all(|c| c.is_ascii_lowercase() || c == '-'),
232 "{next} is not a name a filesystem and a human both read easily",
233 );
234 }
235
236 #[test]
239 fn a_draw_wraps_onto_the_lists() {
240 let drawn = name(&mut || u32::MAX);
241 assert_eq!(drawn.split('-').count(), 3);
242 }
243
244 #[test]
246 fn the_lists_are_distinct_and_sorted() {
247 let mut all: Vec<&str> = [ADJECTIVES, NOUNS, CREATURES].concat();
248 let before = all.len();
249 all.sort_unstable();
250 all.dedup();
251 assert_eq!(all.len(), before, "a word appears in two lists");
252 for list in [ADJECTIVES, NOUNS, CREATURES] {
253 let mut sorted = list.to_vec();
254 sorted.sort_unstable();
255 assert_eq!(list, sorted.as_slice(), "the list is not in order");
256 }
257 }
258
259 #[test]
260 fn a_document_is_born_in_the_documents_directory() {
261 let dir = crate::atomic::tests::TempDir::new("naming-born");
262 let documents = Documents::at(dir.join("Documents"));
263 let store = documents.create(counting()).expect("the container");
264
265 assert_eq!(
266 store.root(),
267 dir.join("Documents").join(format!(
268 "{}-{}-{}.bwx",
269 ADJECTIVES[0], NOUNS[1], CREATURES[2]
270 )),
271 "the directory was not created under the drawn name",
272 );
273 assert!(
274 store.root().join(MANIFEST).is_file(),
275 "with a log to append to"
276 );
277 assert!(
278 store.read_only_reason().is_none(),
279 "a document born here must be writable from its first edit",
280 );
281 }
282
283 #[test]
286 fn a_name_already_taken_is_drawn_again() {
287 let dir = crate::atomic::tests::TempDir::new("naming-collision");
288 let documents = Documents::at(dir.path());
289 let taken = dir.join(&format!(
290 "{}-{}-{}.bwx",
291 ADJECTIVES[0], NOUNS[1], CREATURES[2]
292 ));
293 std::fs::create_dir_all(&taken).expect("the name is taken");
294 assert!(taken.exists(), "precondition: the first name is in use");
295
296 let store = documents.create(counting()).expect("the container");
297 assert_ne!(store.root(), taken, "the taken name was opened over");
298 assert_eq!(
299 store.root(),
300 dir.join(&format!(
301 "{}-{}-{}.bwx",
302 ADJECTIVES[3], NOUNS[4], CREATURES[5]
303 )),
304 "the retry did not draw the next name",
305 );
306 }
307
308 #[test]
311 fn the_retry_gives_up_rather_than_spinning() {
312 let dir = crate::atomic::tests::TempDir::new("naming-exhausted");
313 let documents = Documents::at(dir.path());
314 let only = dir.join(&format!(
315 "{}-{}-{}.bwx",
316 ADJECTIVES[0], NOUNS[0], CREATURES[0]
317 ));
318 std::fs::create_dir_all(&only).expect("the one name is taken");
319
320 let failure = documents
321 .create(|| 0)
322 .err()
323 .expect("a draw with one name to give must give up");
324 assert!(matches!(failure, BirthFailure::Unnamed), "{failure}");
325 }
326
327 #[test]
328 fn nowhere_to_be_born_is_a_failure_and_not_a_guess() {
329 let failure = Documents::Nowhere
330 .create(counting())
331 .err()
332 .expect("nowhere cannot hold a document");
333 assert!(matches!(failure, BirthFailure::Nowhere), "{failure}");
334 assert_eq!(Documents::Nowhere.dir(), None);
335 }
336
337 #[test]
340 fn the_platform_names_a_directory() {
341 let Some(dir) = Documents::platform().dir().map(Path::to_path_buf) else {
342 panic!("the platform named neither documents, home, nor a working directory");
343 };
344 assert!(
345 dir.is_absolute(),
346 "{} is not somewhere findable",
347 dir.display()
348 );
349 }
350}