blob: 81da7a59a49d9b5d620df9d17110478275667e1a [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{Context as _, Error};
use handlebars::Handlebars;
use log::info;
use serde_json::Value;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use crate::templates::{FidldocTemplate, HandlebarsHelper};
pub struct MarkdownTemplate {
handlebars: Handlebars,
output_path: PathBuf,
}
impl MarkdownTemplate {
pub fn new(output_path: &PathBuf) -> MarkdownTemplate {
// Handlebars
let mut handlebars = Handlebars::new();
// Register partials
for &(name, template, expect) in &[
("header", include_str!("partials/header.hbs"), "Failed to include header"),
("header_dir", include_str!("partials/header_dir.hbs"), "Failed to include header_dir"),
("footer", include_str!("partials/footer.hbs"), "Failed to include footer"),
] {
handlebars.register_template_string(name, template).expect(expect);
}
// Register FIDL declarations partials
for &(name, template, expect) in &[
(
"protocols",
include_str!("partials/declarations/protocols.hbs"),
"Failed to include protocols",
),
(
"structs",
include_str!("partials/declarations/structs.hbs"),
"Failed to include structs",
),
("enums", include_str!("partials/declarations/enums.hbs"), "Failed to include enums"),
(
"tables",
include_str!("partials/declarations/tables.hbs"),
"Failed to include tables",
),
(
"unions",
include_str!("partials/declarations/unions.hbs"),
"Failed to include unions",
),
("bits", include_str!("partials/declarations/bits.hbs"), "Failed to include bits"),
(
"constants",
include_str!("partials/declarations/constants.hbs"),
"Failed to include constants",
),
(
"type_aliases",
include_str!("partials/declarations/type_aliases.hbs"),
"Failed to include type_aliases",
),
] {
handlebars.register_template_string(name, template).expect(expect);
}
// Register FIDL type partials
for &(name, template, expect) in &[
("doc", include_str!("partials/types/doc.hbs"), "Failed to include doc"),
("filename", include_str!("partials/types/filename.hbs"), "Failed to include filename"),
("type", include_str!("partials/types/type.hbs"), "Failed to include type"),
("vector", include_str!("partials/types/vector.hbs"), "Failed to include vector"),
] {
handlebars.register_template_string(name, template).expect(expect);
}
// Register core templates
for &(name, template, expect) in &[
("main", include_str!("main.hbs"), "Failed to include main"),
("interface", include_str!("interface.hbs"), "Failed to include interface"),
("toc", include_str!("toc.hbs"), "Failed to include toc"),
] {
handlebars.register_template_string(name, template).expect(expect);
}
// Register helpers
let helpers: &[(&str, HandlebarsHelper)] = &[
("getLink", crate::templates::get_link_helper),
("rpn", crate::templates::remove_package_name),
("eq", crate::templates::eq),
("pl", crate::templates::package_link),
("rpf", crate::templates::remove_parent_folders),
("sl", crate::templates::source_link),
("docLink", crate::templates::doc_link),
("oneline", crate::templates::one_line),
("pulldown", crate::templates::pulldown),
("methodId", crate::templates::method_id),
];
for &(name, helper) in helpers {
handlebars.register_helper(name, Box::new(helper));
}
MarkdownTemplate { handlebars: handlebars, output_path: output_path.to_path_buf() }
}
}
impl FidldocTemplate for MarkdownTemplate {
fn render_main_page(&self, main_fidl_json: &Value) -> Result<(), Error> {
// Render main page
let main_page_path = self.output_path.join("README.md");
info!("Generating main page documentation {}", main_page_path.display());
let mut main_page_file = File::create(&main_page_path)
.with_context(|| format!("Can't create {}", main_page_path.display()))?;
let main_page_content =
render_template(&self.handlebars, "main".to_string(), &main_fidl_json)
.with_context(|| format!("Can't render main page {}", main_page_path.display()))?;
main_page_file.write_all(main_page_content.as_bytes())?;
// Render TOC
let toc_path = self.output_path.join("_toc.yaml");
info!("Generating TOC {}", toc_path.display());
let mut toc_file = File::create(&toc_path)
.with_context(|| format!("Can't create TOC {}", toc_path.display()))?;
let toc_content = render_template(&self.handlebars, "toc".to_string(), &main_fidl_json)
.with_context(|| format!("Can't render TOC {}", toc_path.display()))?;
toc_file.write_all(toc_content.as_bytes())?;
Ok(())
}
fn render_interface(&self, package: &str, fidl_json: &Value) -> Result<(), Error> {
// Create a directory for interface files
let output_dir = self.output_path.join(package);
fs::create_dir(&output_dir)?;
info!("Created output directory {}", output_dir.display());
let package_index = output_dir.join("README.md");
info!("Generating package documentation {}", package_index.display());
let mut output_file = File::create(&package_index)
.with_context(|| format!("Can't create {}", package_index.display()))?;
// Render files
let package_content =
render_template(&self.handlebars, "interface".to_string(), &fidl_json)
.with_context(|| format!("Can't render interface {}", package))?;
output_file.write_all(package_content.as_bytes())?;
Ok(())
}
fn name(&self) -> String {
return "Markdown".to_string();
}
}
fn render_template(
handlebars: &Handlebars,
template_name: String,
fidl_json: &Value,
) -> Result<String, Error> {
let content = handlebars
.render(&template_name, &fidl_json)
.with_context(|| format!("Unable to render template '{}'", template_name))?;
Ok(content)
}
#[cfg(test)]
mod test {
use super::*;
use glob::glob;
use serde_json::json;
use std::{env, fs};
#[test]
fn render_main_page_test() {
let source = include_str!("testdata/README.md");
let mut table_of_contents: Vec<crate::TableOfContentsItem> = vec![];
table_of_contents.push(crate::TableOfContentsItem {
name: "fuchsia.auth".to_string(),
link: "fuchsia.auth/README.md".to_string(),
description: "Fuchsia Auth API".to_string(),
});
table_of_contents.push(crate::TableOfContentsItem {
name: "fuchsia.media".to_string(),
link: "fuchsia.media/README.md".to_string(),
description: "Fuchsia Media API".to_string(),
});
let fidl_config = json!(null);
let declarations: Vec<String> = Vec::new();
let main_fidl_doc = json!({
"table_of_contents": table_of_contents,
"fidldoc_version": "0.0.4",
"config": fidl_config,
"search": declarations,
"url_path": "/",
});
let template = MarkdownTemplate::new(&PathBuf::new());
let result = render_template(&template.handlebars, "main".to_string(), &main_fidl_doc)
.expect("Unable to render main template");
assert_eq!(result, source);
}
#[test]
fn render_toc_test() {
let source = include_str!("testdata/_toc.yaml");
let mut table_of_contents: Vec<crate::TableOfContentsItem> = vec![];
table_of_contents.push(crate::TableOfContentsItem {
name: "fuchsia.auth".to_string(),
link: "fuchsia.auth/README.md".to_string(),
description: "Fuchsia Auth API".to_string(),
});
table_of_contents.push(crate::TableOfContentsItem {
name: "fuchsia.media".to_string(),
link: "fuchsia.media/README.md".to_string(),
description: "Fuchsia Media API".to_string(),
});
let fidl_config = json!(null);
let declarations: Vec<String> = Vec::new();
let main_fidl_doc = json!({
"table_of_contents": table_of_contents,
"fidldoc_version": "0.0.4",
"config": fidl_config,
"search": declarations,
"url_path": "/",
});
let template = MarkdownTemplate::new(&PathBuf::new());
let result = render_template(&template.handlebars, "toc".to_string(), &main_fidl_doc)
.expect("Unable to render toc template");
assert_eq!(result, source);
}
#[test]
fn golden_test() {
let template = MarkdownTemplate::new(&PathBuf::new());
// Load and parse fidldoc config file
let fidl_config: Value =
serde_json::from_str(include_str!("../../fidldoc.config.json")).unwrap();
let goldens_path = goldens_path();
let glob_pattern = goldens_path.join("*.json.golden");
let mut num_goldens = 0;
for entry in glob(&glob_pattern.to_str().unwrap()).unwrap() {
if let Ok(ir_golden_path) = entry {
num_goldens = num_goldens + 1;
let golden = ir_golden_path.file_name().unwrap();
let golden_name = golden.clone().to_str().unwrap();
let ir_golden_data = fs::read_to_string(&ir_golden_path).unwrap();
// For each golden JSON IR at <filename>.json.golden
// there's a corresponding markdown at <filename>.json.golden.md
let md_golden_path = goldens_path.join(format!("{}.md", golden_name));
// Read and parse declarations from the JSON IR golden
let mut declarations: Value = serde_json::from_str(&ir_golden_data)
.with_context(|| format!("Unable to parse testdata for {}", golden_name))
.unwrap();
// Merge declarations with config file
declarations["config"] = fidl_config.clone();
declarations["tag"] = json!("HEAD");
let result =
render_template(&template.handlebars, "interface".to_string(), &declarations)
.with_context(|| {
format!("Unable to render interface template for {}", golden_name)
})
.unwrap();
// Set the environment variable REGENERATE_GOLDENS_FOLDER=<FOLDER>
// when running tests to regenerate goldens in <FOLDER>
match env::var("REGENERATE_GOLDENS_FOLDER") {
Ok(folder) => {
let mut path = PathBuf::from(folder);
// Create the destination folder if needed
fs::create_dir_all(&path).unwrap();
path.push(format!("{}.md", golden_name));
fs::write(path, result)
.with_context(|| {
format!("Unable to regenerate golden for {}", golden_name)
})
.unwrap();
}
Err(_) => {
let md_golden_data = fs::read_to_string(&md_golden_path)
.with_context(|| {
format!("Unable to read golden from {}", &md_golden_path.display())
})
.unwrap();
// Running regular test
assert_eq!(
result, md_golden_data,
"Generated output for template {} doesn't match goldenset",
golden_name
);
}
}
}
}
assert_ne!(num_goldens, 0, "Found 0 goldens in {}", &goldens_path.display());
}
fn goldens_path() -> PathBuf {
let mut path = env::current_exe().unwrap();
path.pop();
path = path.join("test_data/fidldoc");
path
}
}