use crate::html::Location;
use crate::scenarios::ScenarioFilter;
use crate::{resource, Document, SubplotError, TemplateSpec};
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use base64::prelude::{Engine as _, BASE64_STANDARD};
use serde::Serialize;
use tera::{Context, Filter, Tera, Value};
pub(crate) fn generate_test_program_string(
doc: &mut Document,
template: &str,
filter: &ScenarioFilter,
) -> Result<String, SubplotError> {
let context = context(doc, template, filter)?;
let docimpl = doc
.meta()
.document_impl(template)
.ok_or(SubplotError::MissingTemplate)?;
let code = tera(docimpl.spec(), template)?
.render("template", &context)
.expect("render");
Ok(code)
}
pub fn generate_test_program(
doc: &mut Document,
filename: &Path,
template: &str,
filter: &ScenarioFilter,
) -> Result<(), SubplotError> {
let code = generate_test_program_string(doc, template, filter)?;
write(filename, &code)?;
Ok(())
}
fn context(
doc: &mut Document,
template: &str,
filter: &ScenarioFilter,
) -> Result<Context, SubplotError> {
let mut context = Context::new();
let scenarios = doc.matched_scenarios(template, filter)?;
context.insert("scenarios", &scenarios);
context.insert("files", doc.embedded_files());
let mut funcs = vec![];
if let Some(docimpl) = doc.meta().document_impl(template) {
for filename in docimpl.functions_filenames() {
let content = resource::read_as_string(filename, Some(template))
.map_err(|err| SubplotError::FunctionsFileNotFound(filename.into(), err))?;
funcs.push(Func::new(filename, content));
}
}
context.insert("functions", &funcs);
if scenarios.is_empty() {
return Err(SubplotError::NoScenariosMatched(template.to_string()));
}
Ok(context)
}
fn tera(tmplspec: &TemplateSpec, templatename: &str) -> Result<Tera, SubplotError> {
let mut tera = Tera::default();
tera.register_filter("base64", base64);
tera.register_filter("nameslug", UniqueNameSlug::default());
tera.register_filter("commentsafe", commentsafe);
tera.register_filter("location", locationfilter);
let dirname = tmplspec.template_filename().parent().unwrap();
for helper in tmplspec.helpers() {
let helper_path = dirname.join(helper);
let helper_content = resource::read_as_string(&helper_path, Some(templatename))
.map_err(|err| SubplotError::ReadFile(helper_path.clone(), err))?;
let helper_name = helper.display().to_string();
tera.add_raw_template(&helper_name, &helper_content)
.map_err(|err| SubplotError::TemplateError(helper_name.to_string(), err))?;
}
let path = tmplspec.template_filename();
let template = resource::read_as_string(path, Some(templatename))
.map_err(|err| SubplotError::ReadFile(path.to_path_buf(), err))?;
tera.add_raw_template("template", &template)
.map_err(|err| {
SubplotError::TemplateError(tmplspec.template_filename().display().to_string(), err)
})?;
Ok(tera)
}
pub(crate) fn write(filename: &Path, content: &str) -> Result<(), SubplotError> {
let mut f: File = File::create(filename)
.map_err(|err| SubplotError::CreateFile(filename.to_path_buf(), err))?;
f.write_all(content.as_bytes())
.map_err(|err| SubplotError::WriteFile(filename.to_path_buf(), err))?;
Ok(())
}
fn base64(v: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
match v {
Value::String(s) => Ok(Value::String(BASE64_STANDARD.encode(s))),
_ => Err(tera::Error::msg(
"can only base64 encode strings".to_string(),
)),
}
}
fn locationfilter(v: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
let location: Location = serde_json::from_value(v.clone())?;
Ok(Value::String(format!(
"{:?}",
match location {
Location::Known {
filename,
line,
col,
} => format!("{}:{}:{}", filename.display(), line, col),
Location::Unknown => "unknown".to_string(),
}
)))
}
#[derive(Default)]
struct UniqueNameSlug {
names: Arc<Mutex<HashSet<String>>>,
}
impl Filter for UniqueNameSlug {
fn filter(&self, name: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
match name {
Value::String(s) => {
let mut newname = s
.chars()
.map(|c| match c {
'a'..='z' | '0'..='9' => c,
'A'..='Z' => c.to_ascii_lowercase(),
_ => '_',
})
.collect::<String>();
if newname.is_empty() || newname.chars().next().unwrap().is_ascii_digit() {
newname.insert(0, 'n');
newname.insert(1, '_');
}
let mut idx = 0;
let mut set = self.names.lock().unwrap();
let pfx = newname.clone();
while set.contains(&newname) {
newname = format!("{pfx}_{idx}");
idx += 1;
}
set.insert(newname.clone());
Ok(Value::String(newname))
}
_ => Err(tera::Error::msg(
"can only create nameslugs from strings".to_string(),
)),
}
}
}
fn commentsafe(s: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
match s {
Value::String(s) => {
let mut cleaned = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\n' => cleaned.push_str("\\n"),
'\r' => cleaned.push_str("\\r"),
'\\' => cleaned.push_str("\\\\"),
_ => cleaned.push(c),
}
}
Ok(Value::String(cleaned))
}
_ => Err(tera::Error::msg(
"can only make clean comments from strings".to_string(),
)),
}
}
#[derive(Debug, Serialize)]
pub struct Func {
pub source: PathBuf,
pub code: String,
}
impl Func {
pub fn new(source: &Path, code: String) -> Func {
Func {
source: source.to_path_buf(),
code,
}
}
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use tera::{Filter, Value};
#[test]
fn verify_commentsafe_filter() {
static GOOD_CASES: &[(&str, &str)] = &[
("", ""), ("hello world", "hello world"), ("Capitalised Words", "Capitalised Words"), ("multiple\nlines\rblah", "multiple\\nlines\\rblah"), ];
for (input, output) in GOOD_CASES.iter().copied() {
let input = Value::from(input);
let output = Value::from(output);
let empty = HashMap::new();
assert_eq!(super::commentsafe(&input, &empty).ok(), Some(output));
}
}
#[test]
fn verify_name_slugification() {
static GOOD_CASES: &[(&str, &str)] = &[
("foobar", "foobar"),
("FooBar", "foobar_0"),
("Motörhead", "mot_rhead"),
("foo bar", "foo_bar"),
("check ipv4 stuff", "check_ipv4_stuff"),
("check ipv6 stuff", "check_ipv6_stuff"),
("1 is the loneliest number", "n_1_is_the_loneliest_number"),
("check ipv6 stuff", "check_ipv6_stuff_0"),
];
let filt = super::UniqueNameSlug::default();
for (input, output) in GOOD_CASES.iter().copied() {
let input = Value::from(input);
let output = Value::from(output);
let empty = HashMap::new();
assert_eq!(filt.filter(&input, &empty).ok(), Some(output));
}
}
}