diff --git a/src/knowledge/mount.rs b/src/knowledge/mount.rs index 3c80feb..94cf9da 100644 --- a/src/knowledge/mount.rs +++ b/src/knowledge/mount.rs @@ -35,10 +35,11 @@ impl Mount { /// layer missing one or both subtrees contributes nothing from the /// missing side. A directory that holds an `entry.md` mounts that /// file at the directory's own address, and the directory's `log/` - /// stays out of the union. Fails on the first layer root that does - /// not exist or is not a directory, the first file that cannot be - /// read, whose frontmatter is malformed or missing, or whose - /// non-tombstone entry has no `name`, naming the offending path. + /// stays out of the union. An entry with no `name` in its + /// frontmatter takes its filename as its name. Fails on the first + /// layer root that does not exist or is not a directory, or the + /// first file that cannot be read or whose frontmatter is malformed + /// or missing, naming the offending path. pub fn open(layers: &[PathBuf]) -> Result { let mut entries: BTreeMap = BTreeMap::new(); for layer_root in layers { @@ -114,16 +115,24 @@ fn parse_entry(path: &Path, address: &str, layer_root: &Path) -> Result name_segment(address), + Some(value) => value + .as_str() + .ok_or_else(|| { + format!( + "{}: frontmatter field 'name' must be a string", + path.display() + ) + })? + .to_string(), + }; let kind = match mapping.get("type") { None => None, Some(value) => Some( @@ -226,6 +235,17 @@ fn find_md_files(dir: &Path) -> Vec { /// `rules/` or `lore/` directory), walks to: `root_name` joined with the /// file's path relative to `root_dir`, `/`-separated, extension stripped. /// An `entry.md` addresses the directory that holds it. +/// The name an entry has when its frontmatter declares none: the last +/// segment of its address, which is the filename without its extension, +/// or the directory's own name for a directory entity. +fn name_segment(address: &str) -> String { + address + .rsplit('/') + .next() + .expect("rsplit always yields at least one segment") + .to_string() +} + fn address_for(root_name: &str, root_dir: &Path, file_path: &Path) -> String { let relative = file_path .strip_prefix(root_dir) diff --git a/src/knowledge/mount_tests.rs b/src/knowledge/mount_tests.rs index d2382a6..9ec7044 100644 --- a/src/knowledge/mount_tests.rs +++ b/src/knowledge/mount_tests.rs @@ -257,14 +257,42 @@ fn frontmatter_that_is_not_a_mapping_fails_open() { } #[test] -fn a_missing_name_on_a_non_tombstone_entry_fails_open() { +fn a_missing_name_takes_the_filename_as_the_name() { let layer = Layer::empty(); - layer.write("rules/spells/fireball.md", "---\ntype: spell\n---\nbody\n"); + layer.write( + "lore/places/Blackwater Fen.md", + "---\nkind: lore/places\nstub: true\n---\nbody\n", + ); + + let mount = open(&[&layer]).unwrap(); + let entry = mount.get("lore/places/Blackwater Fen").unwrap(); + + assert_eq!(entry.name, "Blackwater Fen"); +} + +#[test] +fn a_missing_name_on_a_directory_entity_takes_the_directorys_name() { + let layer = Layer::empty(); + layer.write( + "lore/places/weatherford/entry.md", + "---\nkind: lore/places\n---\nbody\n", + ); + + let mount = open(&[&layer]).unwrap(); + let entry = mount.get("lore/places/weatherford").unwrap(); + + assert_eq!(entry.name, "weatherford"); +} + +#[test] +fn a_non_string_name_on_a_non_tombstone_entry_fails_open() { + let layer = Layer::empty(); + layer.write("rules/spells/fireball.md", "---\nname: 5\n---\nbody\n"); let error = open(&[&layer]).unwrap_err(); assert!(error.contains("fireball.md")); - assert!(error.contains("'name'")); + assert!(error.contains("'name' must be a string")); } #[test]