1use std::path::{Path, PathBuf};
16
17use crate::container::ContainerError;
18use crate::handle::{Clock, Store};
19use crate::storage::{Any, Name, Native, native::documents_dir};
20
21pub const TRIES: usize = 16;
25
26pub fn candidates(mut draw: impl FnMut() -> u32) -> impl Iterator<Item = Name> {
33 (0..TRIES).filter_map(move |_| Name::of_document(&name(&mut draw)))
34}
35
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
38pub enum Documents {
39 #[default]
43 Nowhere,
44 In(PathBuf),
45}
46
47#[derive(Debug, thiserror::Error)]
51pub enum BirthFailure {
52 #[error("there is no documents directory to create it in")]
53 Nowhere,
54 #[error("no unused name in {TRIES} tries")]
55 Unnamed,
56 #[error(transparent)]
57 Create(#[from] ContainerError),
58}
59
60impl BirthFailure {
61 pub fn notice(&self) -> String {
64 format!("Failed to create a new diagram: {self}")
65 }
66}
67
68impl Documents {
69 pub fn platform() -> Self {
73 match documents_dir() {
74 Some(dir) => Documents::In(dir),
75 None => Documents::Nowhere,
76 }
77 }
78
79 pub fn at(dir: impl Into<PathBuf>) -> Self {
80 Documents::In(dir.into())
81 }
82
83 pub fn dir(&self) -> Option<&Path> {
84 match self {
85 Documents::Nowhere => None,
86 Documents::In(dir) => Some(dir),
87 }
88 }
89
90 pub fn create(&self, mut draw: impl FnMut() -> u32) -> Result<Store<Any>, BirthFailure> {
98 let Documents::In(dir) = self else {
99 return Err(BirthFailure::Nowhere);
100 };
101 std::fs::create_dir_all(dir).map_err(|e| BirthFailure::Create(e.into()))?;
102 for called in candidates(&mut draw) {
103 let root = dir.join(called.as_str());
104 if root.exists() {
105 continue;
106 }
107 match Store::create(Any::new(Native::at(&root)), Clock::System) {
108 Err(ContainerError::Exists(_)) => {}
110 Err(e) => return Err(BirthFailure::Create(e)),
111 Ok(store) => return Ok(store),
112 }
113 }
114 Err(BirthFailure::Unnamed)
115 }
116}
117
118pub fn name(draw: &mut impl FnMut() -> u32) -> String {
121 fn pick<'a>(list: &[&'a str], drawn: u32) -> &'a str {
122 list[drawn as usize % list.len()]
123 }
124 let adjective = pick(ADJECTIVES, draw());
125 let noun = pick(NOUNS, draw());
126 let creature = pick(CREATURES, draw());
127 format!("{adjective}-{noun}-{creature}")
128}
129
130pub fn entropy() -> u32 {
139 getrandom::u32().unwrap_or_else(|_| crate::history::now().unix_millis() as u32)
140}
141
142const ADJECTIVES: &[&str] = &[
149 "amber",
150 "bold",
151 "brave",
152 "brisk",
153 "calm",
154 "clever",
155 "cosmic",
156 "crimson",
157 "curious",
158 "dapper",
159 "eager",
160 "electric",
161 "fearless",
162 "gentle",
163 "gilded",
164 "golden",
165 "happy",
166 "hidden",
167 "jolly",
168 "keen",
169 "lucky",
170 "merry",
171 "mighty",
172 "noble",
173 "patient",
174 "plucky",
175 "polished",
176 "quick",
177 "quiet",
178 "radiant",
179 "rapid",
180 "rustic",
181 "serene",
182 "silent",
183 "silver",
184 "smooth",
185 "snowy",
186 "solar",
187 "spry",
188 "sunny",
189 "swift",
190 "tidy",
191 "tranquil",
192 "velvet",
193 "vivid",
194 "wandering",
195 "witty",
196 "zesty",
197];
198
199const NOUNS: &[&str] = &[
200 "anchor", "autumn", "beacon", "blossom", "breeze", "brook", "canyon", "cedar", "cinder",
201 "comet", "coral", "crescent", "dawn", "delta", "ember", "fern", "forest", "garnet", "glacier",
202 "harbor", "harvest", "horizon", "island", "lantern", "ledger", "lichen", "meadow", "meridian",
203 "mirage", "monsoon", "nebula", "orchard", "pebble", "prairie", "quartz", "ridge", "river",
204 "sable", "summit", "sunrise", "sunshine", "thicket", "thunder", "tundra", "valley", "willow",
205 "window", "zephyr",
206];
207
208const CREATURES: &[&str] = &[
209 "badger", "bison", "cricket", "dolphin", "falcon", "ferret", "finch", "fox", "gecko", "gibbon",
210 "heron", "ibis", "jackal", "jaguar", "kestrel", "koala", "lemur", "lynx", "magpie", "marlin",
211 "marmot", "mole", "narwhal", "newt", "ocelot", "osprey", "otter", "panda", "pelican",
212 "penguin", "puffin", "quail", "rabbit", "raven", "robin", "salmon", "sparrow", "stoat",
213 "tapir", "teal", "toad", "turtle", "viper", "walrus", "weasel", "wombat", "wren", "yak",
214];
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::container::MANIFEST;
220
221 fn counting() -> impl FnMut() -> u32 {
224 let mut n = 0;
225 move || {
226 n += 1;
227 n - 1
228 }
229 }
230
231 #[test]
232 fn a_name_is_three_words_drawn_one_per_word() {
233 let mut draw = counting();
234 assert_eq!(
235 name(&mut draw),
236 format!("{}-{}-{}", ADJECTIVES[0], NOUNS[1], CREATURES[2]),
237 "the draws are consumed left to right, one per list",
238 );
239 let next = name(&mut draw);
240 assert_eq!(next.split('-').count(), 3, "{next} is not three words");
241 assert!(
242 next.chars().all(|c| c.is_ascii_lowercase() || c == '-'),
243 "{next} is not a name a filesystem and a human both read easily",
244 );
245 }
246
247 #[test]
250 fn the_names_offered_are_the_drawn_ones_and_the_retry_is_bounded() {
251 let offered: Vec<String> = candidates(counting()).map(|n| n.to_string()).collect();
252 assert_eq!(offered.len(), TRIES);
253 assert_eq!(
254 offered[0],
255 format!("{}-{}-{}.bwx", ADJECTIVES[0], NOUNS[1], CREATURES[2]),
256 );
257 assert_eq!(
258 offered[1],
259 format!("{}-{}-{}.bwx", ADJECTIVES[3], NOUNS[4], CREATURES[5]),
260 "the second candidate is the next draw, not the first one again",
261 );
262 }
263
264 #[test]
267 fn a_draw_wraps_onto_the_lists() {
268 let drawn = name(&mut || u32::MAX);
269 assert_eq!(drawn.split('-').count(), 3);
270 }
271
272 #[test]
274 fn the_lists_are_distinct_and_sorted() {
275 let mut all: Vec<&str> = [ADJECTIVES, NOUNS, CREATURES].concat();
276 let before = all.len();
277 all.sort_unstable();
278 all.dedup();
279 assert_eq!(all.len(), before, "a word appears in two lists");
280 for list in [ADJECTIVES, NOUNS, CREATURES] {
281 let mut sorted = list.to_vec();
282 sorted.sort_unstable();
283 assert_eq!(list, sorted.as_slice(), "the list is not in order");
284 }
285 }
286
287 #[test]
288 fn a_document_is_born_in_the_documents_directory() {
289 let dir = crate::temp::TempDir::new("naming-born");
290 let documents = Documents::at(dir.join("Documents"));
291 let store = documents.create(counting()).expect("the container");
292
293 assert_eq!(
294 store.path().expect("a container on disk"),
295 dir.join("Documents").join(format!(
296 "{}-{}-{}.bwx",
297 ADJECTIVES[0], NOUNS[1], CREATURES[2]
298 )),
299 "the directory was not created under the drawn name",
300 );
301 assert!(
302 store
303 .path()
304 .expect("a container on disk")
305 .join(MANIFEST.as_str())
306 .is_file(),
307 "with a log to append to"
308 );
309 assert!(
310 store.read_only_reason().is_none(),
311 "a document born here must be writable from its first edit",
312 );
313 }
314
315 #[test]
318 fn a_name_already_taken_is_drawn_again() {
319 let dir = crate::temp::TempDir::new("naming-collision");
320 let documents = Documents::at(dir.path());
321 let taken = dir.join(&format!(
322 "{}-{}-{}.bwx",
323 ADJECTIVES[0], NOUNS[1], CREATURES[2]
324 ));
325 std::fs::create_dir_all(&taken).expect("the name is taken");
326 assert!(taken.exists(), "precondition: the first name is in use");
327
328 let store = documents.create(counting()).expect("the container");
329 assert_ne!(
330 store.path().expect("a container on disk"),
331 taken,
332 "the taken name was opened over"
333 );
334 assert_eq!(
335 store.path().expect("a container on disk"),
336 dir.join(&format!(
337 "{}-{}-{}.bwx",
338 ADJECTIVES[3], NOUNS[4], CREATURES[5]
339 )),
340 "the retry did not draw the next name",
341 );
342 }
343
344 #[test]
347 fn the_retry_gives_up_rather_than_spinning() {
348 let dir = crate::temp::TempDir::new("naming-exhausted");
349 let documents = Documents::at(dir.path());
350 let only = dir.join(&format!(
351 "{}-{}-{}.bwx",
352 ADJECTIVES[0], NOUNS[0], CREATURES[0]
353 ));
354 std::fs::create_dir_all(&only).expect("the one name is taken");
355
356 let failure = documents
357 .create(|| 0)
358 .err()
359 .expect("a draw with one name to give must give up");
360 assert!(matches!(failure, BirthFailure::Unnamed), "{failure}");
361 }
362
363 #[test]
364 fn nowhere_to_be_born_is_a_failure_and_not_a_guess() {
365 let failure = Documents::Nowhere
366 .create(counting())
367 .err()
368 .expect("nowhere cannot hold a document");
369 assert!(matches!(failure, BirthFailure::Nowhere), "{failure}");
370 assert_eq!(Documents::Nowhere.dir(), None);
371 }
372
373 #[test]
376 fn the_platform_names_a_directory() {
377 let Some(dir) = Documents::platform().dir().map(Path::to_path_buf) else {
378 panic!("the platform named neither documents, home, nor a working directory");
379 };
380 assert!(
381 dir.is_absolute(),
382 "{} is not somewhere findable",
383 dir.display()
384 );
385 }
386}