From e10882a290c1df7a35ac448777d1c0207e92cabb Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 23 Mar 2024 00:30:53 +0100 Subject: [PATCH] unit tests: add frontmatter unit tests --- Cargo.lock | 70 ++++----- Cargo.toml | 3 +- src/frontmatter.rs | 134 ++++++++++++------ src/main.rs | 23 ++- ...c__test__frontmatter_doc_location_e2e.snap | 18 +++ src/test.rs | 80 ++++++++++- test/docs.md | 3 + test/frontmatter-doc-location.nix | 18 +++ 8 files changed, 260 insertions(+), 89 deletions(-) create mode 100644 src/snapshots/nixdoc__test__frontmatter_doc_location_e2e.snap create mode 100644 test/docs.md create mode 100644 test/frontmatter-doc-location.nix diff --git a/Cargo.lock b/Cargo.lock index 80cbef3..65f95b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,10 +133,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] -name = "hashbrown" -version = "0.12.3" +name = "gray_matter" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "1cf2fb99fac0b821a4e61c61abff076324bb0e5c3b4a83815bbc3518a38971ad" +dependencies = [ + "serde", + "serde_json", + "toml", + "yaml-rust", +] [[package]] name = "hashbrown" @@ -150,16 +156,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.2.5" @@ -167,7 +163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown", ] [[package]] @@ -221,14 +217,15 @@ name = "nixdoc" version = "3.0.2" dependencies = [ "clap", + "gray_matter", "insta", + "relative-path", "rnix", "rowan", "serde", "serde_json", - "serde_yaml 0.9.33", + "serde_yaml", "textwrap", - "yaml-front-matter", ] [[package]] @@ -249,6 +246,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "relative-path" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" + [[package]] name = "rnix" version = "0.11.0" @@ -265,7 +268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a58fa8a7ccff2aec4f39cc45bf5f985cec7125ab271cf681c279fd00192b49" dependencies = [ "countme", - "hashbrown 0.14.3", + "hashbrown", "memoffset", "rustc-hash", "text-size", @@ -314,25 +317,13 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_yaml" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" -dependencies = [ - "indexmap 1.9.3", - "ryu", - "serde", - "yaml-rust", -] - [[package]] name = "serde_yaml" version = "0.9.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0623d197252096520c6f2a5e1171ee436e5af99a5d7caa2891e55e61950e6d9" dependencies = [ - "indexmap 2.2.5", + "indexmap", "itoa", "ryu", "serde", @@ -385,6 +376,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -481,16 +481,6 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" -[[package]] -name = "yaml-front-matter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a94fb32d2b438e3fddf901fbfe9eb87b34d63853ca6c6da5d2ab7e27031e0bae" -dependencies = [ - "serde", - "serde_yaml 0.8.26", -] - [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 9e51288..9d383dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,8 @@ serde_json = "1.0" textwrap = "0.16" clap = { version = "4.4.4", features = ["derive"] } serde_yaml = "0.9.33" -yaml-front-matter = "0.1.0" +gray_matter = "0.2.6" +relative-path = "1.9.2" [dev-dependencies] insta = "1.36.1" diff --git a/src/frontmatter.rs b/src/frontmatter.rs index 03fdccc..c9ce91b 100644 --- a/src/frontmatter.rs +++ b/src/frontmatter.rs @@ -1,61 +1,109 @@ use std::{ - collections::HashMap, fs, path::{Path, PathBuf}, - process::Command, }; +use gray_matter::engine::YAML; +use gray_matter::Matter; +use std::fmt; + +use relative_path::RelativePath; use serde::{Deserialize, Serialize}; -use yaml_front_matter::YamlFrontMatter; -fn find_repo_root() -> Option { - let output = Command::new("git") - .args(["rev-parse", "--show-toplevel"]) - .output() - .ok()? - .stdout; +#[derive(Deserialize, Serialize, Debug)] +pub struct Frontmatter { + pub doc_location: Option, +} - let path_str = String::from_utf8(output).ok()?.trim().to_string(); - Some(PathBuf::from(path_str)) +#[derive(Debug, PartialEq)] +pub enum FrontmatterErrorKind { + InvalidYaml, + DocLocationFileNotFound, + DocLocationNotRelativePath, } -#[derive(Deserialize, Serialize, Debug)] -pub struct Matter { - #[serde(flatten)] - pub content: HashMap, +#[derive(Debug, PartialEq)] +pub struct FrontmatterError { + pub message: String, + pub kind: FrontmatterErrorKind, +} + +impl fmt::Display for FrontmatterError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Write the error message to the formatter + write!(f, "FrontmatterError: {}", self.message) + } } -/// Returns the actual content of a markdown file, if the frontmatter has an import field. -pub fn get_imported_content(file_path: &Path, markdown: Option<&String>) -> Option { - markdown?; - - match YamlFrontMatter::parse::(markdown.unwrap()) { - Ok(document) => { - let metadata = document.metadata.content; - - let abs_import = metadata.get("import").map(|field| { - let import_val = field - .as_str() - .expect("Frontmatter: import field must be a string"); - match PathBuf::from(import_val).is_relative() { - true => PathBuf::from_iter(vec![ - // Cannot fail because every file has a parent directory - file_path.parent().unwrap().to_path_buf(), - PathBuf::from(import_val), - ]), - false => PathBuf::from_iter(vec![ - find_repo_root() - .expect("Could not find root directory of repository. Make sure you have git installed and are in a git repository"), - PathBuf::from(format!(".{import_val}")), - ]), +/// Returns the actual content of a markdown file, if the frontmatter has a doc_location field. +/// It returns None if the frontmatter is not present. +/// It returns an error if the frontmatter is present but invalid. This includes: +/// - Invalid yaml frontmatter +/// - Invalid doc_location type +/// - doc_location file is not readable or not found +/// - doc_location field is not a relative path +/// - doc_location file is not utf8 +pub fn get_imported_content( + file_path: &Path, + markdown: &str, +) -> Result, FrontmatterError> { + let matter = Matter::::new(); + + let result = matter.parse(markdown); + + // If the frontmatter is not present, we return None + if result.data.is_none() { + return Ok(None); + } + + let pod = result.data.unwrap(); + match pod.deserialize::() { + Ok(metadata) => { + let abs_import = match metadata.doc_location { + Some(doc_location) => { + let import_path: PathBuf = PathBuf::from(&doc_location); + let relative_path = RelativePath::from_path(&import_path); + + match relative_path { + Ok(rel) => Ok(Some(rel.to_path(file_path.parent().unwrap()))), + Err(e) => Err(FrontmatterError { + message: format!("{:?}: doc_location: field must be a path relative to the current file. Error: {} - {}", file_path, doc_location, e), + kind: FrontmatterErrorKind::DocLocationNotRelativePath, + }), + } } - }); + // doc_location: field doesn't exist. Since it is optional, we return None + None => Ok(None), + }; + + match abs_import { + Ok(Some(path)) => match fs::read_to_string(&path) { + Ok(content) => Ok(Some(content)), + Err(e) => Err(FrontmatterError { + message: format!( + "{:?}: Failed to read doc_location file: {:?} {}", + file_path, path, e + ), + kind: FrontmatterErrorKind::DocLocationFileNotFound, + }), + }, + Ok(None) => Ok(None), + Err(e) => Err(e), + } + } - abs_import.map(|path| { - fs::read_to_string(&path) - .expect(format!("Could not read file: {:?}", &path).as_str()) + Err(e) => { + let message = format!( + "{:?}: Failed to parse frontmatter metadata - {} YAML:{}:{}", + file_path, + e, + e.line(), + e.column() + ); + Err(FrontmatterError { + message, + kind: FrontmatterErrorKind::InvalidYaml, }) } - Err(_) => None, } } diff --git a/src/main.rs b/src/main.rs index ee8055e..518f53d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,7 +41,7 @@ use rnix::{ SyntaxKind, SyntaxNode, }; use rowan::{ast::AstNode, WalkEvent}; -use std::{fs, path::Path}; +use std::{fs, path::Path, process::exit}; use std::collections::HashMap; use std::io; @@ -109,11 +109,26 @@ pub fn retrieve_doc_comment( let doc_comment = get_expr_docs(node); // Doc comments can import external file via "import" in frontmatter - let content = get_imported_content(file, doc_comment.as_ref()).or(doc_comment); - content.map(|inner| { + doc_comment.map(|inner| { + let content = handle_indentation(&inner).unwrap_or_default(); + + let final_content = match get_imported_content(file, &content) { + // Use the imported content instead of the original content + Ok(Some(imported_content)) => imported_content, + + // Use the original content + Ok(None) => content, + + // Abort if we failed to read the frontmatter + Err(e) => { + eprintln!("{}", e); + exit(1); + } + }; + shift_headings( - &handle_indentation(&inner).unwrap_or(String::new()), + &handle_indentation(&final_content).unwrap_or(String::new()), // H1 to H4 can be used in the doc-comment with the current rendering. // They will be shifted to H3, H6 // H1 and H2 are currently used by the outer rendering. (category and function name) diff --git a/src/snapshots/nixdoc__test__frontmatter_doc_location_e2e.snap b/src/snapshots/nixdoc__test__frontmatter_doc_location_e2e.snap new file mode 100644 index 0000000..eeaa068 --- /dev/null +++ b/src/snapshots/nixdoc__test__frontmatter_doc_location_e2e.snap @@ -0,0 +1,18 @@ +--- +source: src/test.rs +expression: output +--- +# Debug {#sec-functions-library-debug} +## Imported + +This is be the documentation + +## `lib.debug.item` {#function-library-lib.debug.item} + +### Imported + +This is be the documentation + +## `lib.debug.optional` {#function-library-lib.debug.optional} + +No frontmatter diff --git a/src/test.rs b/src/test.rs index 96e2a26..fd89475 100644 --- a/src/test.rs +++ b/src/test.rs @@ -4,7 +4,12 @@ use std::path::PathBuf; use std::io::Write; -use crate::{collect_entries, format::shift_headings, retrieve_description}; +use crate::{ + collect_entries, + format::shift_headings, + frontmatter::{get_imported_content, FrontmatterError, FrontmatterErrorKind}, + retrieve_description, +}; #[test] fn test_main() { @@ -210,3 +215,76 @@ fn test_doc_comment_no_duplicate_arguments() { insta::assert_snapshot!(output); } + +#[test] +fn test_frontmatter_doc_location_e2e() { + let mut output = Vec::new(); + let src_path = PathBuf::from("test/frontmatter-doc-location.nix"); + let src = fs::read_to_string(&src_path).unwrap(); + let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); + let prefix = "lib"; + let category = "debug"; + let desc = retrieve_description(&nix, &"Debug", category, &src_path); + writeln!(output, "{}", desc).expect("Failed to write header"); + + for entry in collect_entries(nix, prefix, category, &src_path) { + entry + .write_section(&Default::default(), &mut output) + .expect("Failed to write section") + } + + let output = String::from_utf8(output).expect("not utf8"); + + insta::assert_snapshot!(output); +} + +const NOT_RELATIVE: &str = r#"--- +doc_location: /tmp/not-relative.md +--- +Other stuff +"#; + +#[test] +fn test_frontmatter_doc_location_relative() { + let base_file = PathBuf::from("test/frontmatter-doc-location.nix"); + + let result = get_imported_content(&base_file, NOT_RELATIVE); + + assert_eq!( + result.unwrap_err().kind, + FrontmatterErrorKind::DocLocationNotRelativePath + ); +} + +const INVALID_TYPE: &str = r#"--- +doc_location: 1 +--- +Other stuff +"#; + +#[test] +fn test_frontmatter_doc_location_type() { + let base_file = PathBuf::from("test/frontmatter-doc-location.nix"); + + let result = get_imported_content(&base_file, INVALID_TYPE); + + assert_eq!(result.unwrap_err().kind, FrontmatterErrorKind::InvalidYaml); +} + +const FILE_NOT_FOUND: &str = r#"--- +doc_location: ./does-not-exist.md +--- +Other stuff +"#; + +#[test] +fn test_frontmatter_doc_location_file_not_found() { + let base_file = PathBuf::from("test/frontmatter-doc-location.nix"); + + let result = get_imported_content(&base_file, FILE_NOT_FOUND); + + assert_eq!( + result.unwrap_err().kind, + FrontmatterErrorKind::DocLocationFileNotFound + ); +} diff --git a/test/docs.md b/test/docs.md new file mode 100644 index 0000000..99a1a67 --- /dev/null +++ b/test/docs.md @@ -0,0 +1,3 @@ +# Imported + +This is be the documentation diff --git a/test/frontmatter-doc-location.nix b/test/frontmatter-doc-location.nix new file mode 100644 index 0000000..a4d4c15 --- /dev/null +++ b/test/frontmatter-doc-location.nix @@ -0,0 +1,18 @@ +/** + --- + doc_location: docs.md + --- +*/ +{}: { + /** + --- + doc_location: docs.md + --- + */ + item = x: x; + + /** + No frontmatter + */ + optional = x: x; +} \ No newline at end of file