blob: 94562f94807a2a3a8889fb2d75f1ad5f7dcab262 [file] [log] [blame]
// Copyright 2022 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 crate::device_info::DeviceInfoImpl;
use anyhow::{anyhow, Error};
use handlebars::Handlebars;
use mockall::automock;
use std::path::Path;
const FILE_NAME_NOT_AVAILABLE_ERROR: &str = "file_name not available";
const FILE_NAME_NOT_UTF8_ERROR: &str = "file_name not UTF8";
const FILE_NAME_MISSING_SUFFIX_ERROR: &str = "file_name missing suffix";
/// Trait version of Handlebars useful for mocking.
#[automock]
pub trait TemplateEngine: Send + Sync {
fn render(&self, name: &str, data: &DeviceInfoImpl) -> Result<String, Error>;
fn register_resource(&mut self, name: &str, tpl_path: &str) -> Result<(), Error>;
}
impl<'_a> TemplateEngine for Handlebars<'_a> {
fn render(&self, name: &str, data: &DeviceInfoImpl) -> Result<String, Error> {
Ok(self.render(name, data)?)
}
fn register_resource(&mut self, name: &str, tpl_path: &str) -> Result<(), Error> {
Ok(self.register_template_file(name, tpl_path)?)
}
}
/// Returns the "template name" for a path (eg "foo" for "/bar/foo.hbs.html").
fn template_name_from_path(path_str: &str) -> Result<String, Error> {
let path = Path::new(path_str);
let full_name = path.file_name().ok_or(anyhow!(FILE_NAME_NOT_AVAILABLE_ERROR))?;
let full_name_str = full_name.to_str().ok_or(anyhow!(FILE_NAME_NOT_UTF8_ERROR))?;
let prefix_end = full_name_str.find(".").ok_or(anyhow!(FILE_NAME_MISSING_SUFFIX_ERROR))?;
let prefix = &full_name_str[..prefix_end];
Ok(prefix.to_string())
}
/// Registers templates with Handlebars.
///
/// Registration fails if a "$TEMPLATE_NAME.hbs.html" is not available as
/// a component resource.
///
/// See BUILD.gn's resource("templates") rule for adding new templates as
/// component resources.
pub fn register_template_resources<'a>(
template_engine: &mut Box<dyn TemplateEngine>,
template_paths: impl Iterator<Item = &'a str>,
) -> Result<(), Error> {
for template_path in template_paths {
let template_name = template_name_from_path(template_path)?;
// Register the template at given path using the file name's prefix.
(*template_engine).register_resource(&template_name, template_path)?;
println!("Registered: {:?} from {:?}", template_name, template_path);
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::handlebars_utils::{
register_template_resources, template_name_from_path, MockTemplateEngine, TemplateEngine,
FILE_NAME_MISSING_SUFFIX_ERROR,
};
use anyhow::{anyhow, Error};
use mockall::predicate::eq;
use mockall::Sequence;
const TEMPLATE_ENGINE_ERROR: &str = "Template Engine Error!";
/// Verifies provided templates result in register_resource() calls.
#[test]
fn register_template_resources_generates_engine_calls() -> Result<(), Error> {
let mut template_engine = MockTemplateEngine::new();
// Mock a Handlebars expecting 3 template files to be registered.
let mut call_sequence = Sequence::new();
let resources = vec![("a", "a.suffix"), ("b", "/dir/b.suffix"), ("c", "/dir.dir/c.suffix")];
for (name, path) in &resources {
template_engine
.expect_register_resource()
.with(eq(*name), eq(*path))
.times(1)
.returning(|_, _| Ok(()))
.in_sequence(&mut call_sequence);
}
let mut boxed_template_engine: Box<dyn TemplateEngine> = Box::new(template_engine);
register_template_resources(
&mut boxed_template_engine,
vec!["a.suffix", "/dir/b.suffix", "/dir.dir/c.suffix"].into_iter(),
)
}
/// Verifies register_template_resources() fails when given a file without a suffix.
/// Suffixes are required to disambiguate "filenames" from "logical template names."
#[test]
fn register_template_resources_fails_when_passed_file_without_suffix() {
let template_engine = MockTemplateEngine::new();
let mut boxed_template_engine: Box<dyn TemplateEngine> = Box::new(template_engine);
// Expect error since there's no "." in the path's filename.
let result =
register_template_resources(&mut boxed_template_engine, vec!["no_suffix"].into_iter());
assert!(result.is_err(), "register_template_resources should return an error.");
assert_eq!(
result.err().unwrap().to_string(),
FILE_NAME_MISSING_SUFFIX_ERROR,
"register_template_resources should return a FILE_NAME_MISSING_SUFFIX_ERROR",
);
}
/// Verifies register_template_resources() percolates register_resource() errors.
#[test]
fn register_template_resources_percolates_template_error() {
let mut template_engine = MockTemplateEngine::new();
// Only 1 call expected because registering stops after first failure.
template_engine
.expect_register_resource()
.with(eq("a"), eq("a.suffix"))
.times(1)
.returning(|_, _| Err(anyhow!(TEMPLATE_ENGINE_ERROR)));
let mut boxed_template_engine: Box<dyn TemplateEngine> = Box::new(template_engine);
// Request to register 2 resources -- but should fail at first failure.
let result = register_template_resources(
&mut boxed_template_engine,
vec!["a.suffix", "b.suffix"].into_iter(),
);
assert!(result.is_err(), "register_template_resources should return an error");
assert_eq!(
result.err().unwrap().to_string(),
TEMPLATE_ENGINE_ERROR,
"register_template_resources should return a TEMPLATE_ENGINE_ERROR",
);
}
#[test]
/// Verifies template_name_from_path() handles valid paths correctly.
fn names_successfully_resolved_from_valid_template_paths() {
for (name, path) in vec![
("a", "a.foo"),
("b", "b.foo.bar"),
("c", "c.foo.bar.baz"),
("d", "/z/d.foo.bar"),
("e", "/z/y/e.foo.bar"),
] {
assert_eq!(template_name_from_path(path).expect("Valid name from path"), name);
}
}
#[test]
/// Verifies template_name_from_path() handles invalid paths correctly.
fn names_not_resolved_from_invalid_template_paths() {
for bad_name in vec!["", "a", "/z/b", "/z/y/c"] {
assert!(template_name_from_path(&bad_name).is_err());
}
}
}