summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-09-13 09:44:39 +0000
committerLars Wirzenius <liw@liw.fi>2021-09-13 09:44:39 +0000
commita06ef1f46c8603b8b5e5a44e6521b10238331891 (patch)
tree995392da4d56597be9ac71bba4669df299506ea7 /src
parent217d920d72d9bf56174e0694329ef9c558160b69 (diff)
parent66bedaf70e7b652f791dd0e2fcbd39db3cbec6f9 (diff)
downloadsubplot-a06ef1f46c8603b8b5e5a44e6521b10238331891.tar.gz
Merge branch 'multi-lang-docs' into 'main'
Implement polyglot bindings See merge request subplot/subplot!210
Diffstat (limited to 'src')
-rw-r--r--src/bin/subplot.rs14
-rw-r--r--src/bindings.rs334
-rw-r--r--src/codegen.rs19
-rw-r--r--src/doc.rs32
-rw-r--r--src/matches.rs33
-rw-r--r--src/metadata.rs27
-rw-r--r--src/resource.rs63
-rw-r--r--src/templatespec.rs2
-rw-r--r--src/typeset.rs2
9 files changed, 273 insertions, 253 deletions
diff --git a/src/bin/subplot.rs b/src/bin/subplot.rs
index b8524e1..034cbb2 100644
--- a/src/bin/subplot.rs
+++ b/src/bin/subplot.rs
@@ -310,7 +310,9 @@ impl Docgen {
}
let mut doc = cli::load_document(&self.input, style)?;
doc.lint()?;
- if !doc.check_named_files_exist()? {
+ let template = doc.meta().template_name().unwrap_or("").to_string();
+ if !doc.check_named_files_exist(&template)? || !doc.check_matched_steps_have_impl(&template)
+ {
eprintln!("Continuing despite warnings");
}
@@ -408,13 +410,19 @@ impl Codegen {
let _enter = span.enter();
let mut doc = cli::load_document(&self.filename, Style::default())?;
doc.lint()?;
- if !doc.check_named_files_exist()? {
+ let template = doc
+ .meta()
+ .template_name()
+ .ok_or_else(|| anyhow::anyhow!("No template name given"))?
+ .to_string();
+ if !doc.check_named_files_exist(&template)? || !doc.check_matched_steps_have_impl(&template)
+ {
eprintln!("Unable to continue");
std::process::exit(1);
}
let spec = template_spec(&doc)?;
- generate_test_program(&mut doc, &spec, &self.output)?;
+ generate_test_program(&mut doc, &spec, &self.output, &template)?;
if self.run {
let run = match spec.run() {
diff --git a/src/bindings.rs b/src/bindings.rs
index 4447c66..91a2c8d 100644
--- a/src/bindings.rs
+++ b/src/bindings.rs
@@ -10,8 +10,10 @@ use serde_aux::prelude::*;
use std::collections::HashMap;
use std::fmt::Debug;
+use std::ops::Deref;
use std::path::Path;
use std::str::FromStr;
+use std::sync::Arc;
use lazy_static::lazy_static;
use regex::{escape, Regex, RegexBuilder};
@@ -90,6 +92,61 @@ impl CaptureType {
}
}
}
+
+/// A link from a binding to its implementation in a given language.
+///
+/// Such a link comprises a function name to call for the step and
+/// an optional function name to call to clean the step up at the end.
+#[derive(Debug, Clone)]
+pub struct BindingImpl {
+ function: String,
+ cleanup: Option<String>,
+}
+
+impl BindingImpl {
+ /// Create a new binding implementation
+ ///
+ /// ```ignore
+ /// # use subplot::bindings::BindingImpl;
+ ///
+ /// let bimpl = BindingImpl::new("foo::bar::func", Some("foo::bar::func_cleanup"));
+ /// ```
+ pub fn new(function: &str, cleanup: Option<&str>) -> Self {
+ Self {
+ function: function.to_string(),
+ cleanup: cleanup.map(str::to_string),
+ }
+ }
+
+ /// Retrieve the function name in this binding
+ ///
+ /// ```ignore
+ /// # use subplot::bindings::BindingImpl;
+ /// let bimpl = BindingImpl::new("foo::bar::func", None);
+ ///
+ /// assert_eq!(bimpl.function(), "foo::bar::func");
+ /// ```
+ pub fn function(&self) -> &str {
+ &self.function
+ }
+
+ /// Retrieve the cleanup function name in this binding
+ ///
+ /// ```ignore
+ /// # use subplot::bindings::BindingImpl;
+ /// let bimpl = BindingImpl::new("foo::bar::func", None);
+ ///
+ /// assert_eq!(bimpl.cleanup(), None);
+ ///
+ /// let bimpl = BindingImpl::new("foo::bar::func", Some("foo::bar::func_cleanup"));
+ ///
+ /// assert_eq!(bimpl.cleanup(), Some("foo::bar::func_cleanup"));
+ /// ```
+ pub fn cleanup(&self) -> Option<&str> {
+ self.cleanup.as_deref()
+ }
+}
+
/// A binding of a scenario step to its implementation.
///
/// Contains the pattern used to match against scenario steps,
@@ -100,8 +157,7 @@ pub struct Binding {
kind: StepKind,
pattern: String,
regex: Regex,
- function: String,
- cleanup: Option<String>,
+ impls: HashMap<String, Arc<BindingImpl>>,
types: HashMap<String, CaptureType>,
}
@@ -110,8 +166,6 @@ impl Binding {
pub fn new(
kind: StepKind,
pattern: &str,
- function: &str,
- cleanup: Option<&str>,
case_sensitive: bool,
mut types: HashMap<String, CaptureType>,
) -> Result<Binding> {
@@ -129,12 +183,19 @@ impl Binding {
kind,
pattern: pattern.to_owned(),
regex,
- function: function.to_string(),
- cleanup: cleanup.map(String::from),
+ impls: HashMap::new(),
types,
})
}
+ /// Insert an impl into this binding
+ pub fn add_impl(&mut self, template: &str, function: &str, cleanup: Option<&str>) {
+ self.impls.insert(
+ template.to_string(),
+ Arc::new(BindingImpl::new(function, cleanup)),
+ );
+ }
+
/// Return the kind of step the binding is for.
pub fn kind(&self) -> StepKind {
self.kind
@@ -145,14 +206,9 @@ impl Binding {
&self.pattern
}
- /// Return name of function that implements step.
- pub fn function(&self) -> &str {
- &self.function
- }
-
- /// Return name of function that implements cleanup.
- pub fn cleanup(&self) -> Option<&str> {
- self.cleanup.as_deref()
+ /// Retrieve a particular implementation by name
+ pub fn step_impl(&self, template: &str) -> Option<Arc<BindingImpl>> {
+ self.impls.get(template).cloned()
}
/// Return the compiled regular expression for the pattern of the
@@ -163,8 +219,13 @@ impl Binding {
&self.regex
}
+ /// Return the type bindings for this binding.
+ pub fn types(&self) -> impl Iterator<Item = (&str, CaptureType)> {
+ self.types.iter().map(|(s, c)| (s.as_str(), *c))
+ }
+
/// Try to match defined binding against a parsed scenario step.
- pub fn match_with_step(&self, step: &ScenarioStep) -> Option<MatchedStep> {
+ pub fn match_with_step(&self, template: &str, step: &ScenarioStep) -> Option<MatchedStep> {
if self.kind() != step.kind() {
return None;
}
@@ -173,7 +234,7 @@ impl Binding {
let caps = self.regex.captures(step_text)?;
// If there is only one capture, it's the whole string.
- let mut m = MatchedStep::new(self, &self.types);
+ let mut m = MatchedStep::new(self, template);
if caps.len() == 1 {
m.append_part(PartialStep::uncaptured(step_text));
return Some(m);
@@ -248,82 +309,27 @@ mod test_binding {
use std::collections::HashMap;
#[test]
- fn creates_new_without_cleanup() {
- let b = Binding::new(
- StepKind::Given,
- "I am Tomjon",
- "set_name",
- None,
- false,
- HashMap::new(),
- )
- .unwrap();
+ fn creates_new() {
+ let b = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap();
assert_eq!(b.kind(), StepKind::Given);
assert!(b.regex().is_match("I am Tomjon"));
assert!(!b.regex().is_match("I am Tomjon of Lancre"));
assert!(!b.regex().is_match("Hello, I am Tomjon"));
- assert_eq!(b.function(), "set_name");
- assert_eq!(b.cleanup(), None);
- }
-
- #[test]
- fn creates_new_with_cleanup() {
- let b = Binding::new(
- StepKind::Given,
- "I am Tomjon",
- "set_name",
- Some("unset_name"),
- false,
- HashMap::new(),
- )
- .unwrap();
- assert_eq!(b.kind(), StepKind::Given);
- assert!(b.regex().is_match("I am Tomjon"));
- assert!(!b.regex().is_match("I am Tomjon of Lancre"));
- assert!(!b.regex().is_match("Hello, I am Tomjon"));
- assert_eq!(b.function(), "set_name");
- assert_eq!(b.cleanup(), Some("unset_name"));
}
#[test]
fn equal() {
- let a = Binding::new(
- StepKind::Given,
- "I am Tomjon",
- "set_name",
- Some("unset"),
- false,
- HashMap::new(),
- )
- .unwrap();
- let b = Binding::new(
- StepKind::Given,
- "I am Tomjon",
- "set_name",
- Some("unset"),
- false,
- HashMap::new(),
- )
- .unwrap();
+ let a = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap();
+ let b = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap();
assert_eq!(a, b);
}
#[test]
fn not_equal() {
- let a = Binding::new(
- StepKind::Given,
- "I am Tomjon",
- "set_name",
- None,
- false,
- HashMap::new(),
- )
- .unwrap();
+ let a = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap();
let b = Binding::new(
StepKind::Given,
"I am Tomjon of Lancre",
- "set_name",
- None,
false,
HashMap::new(),
)
@@ -334,30 +340,22 @@ mod test_binding {
#[test]
fn does_not_match_with_wrong_kind() {
let step = ScenarioStep::new(StepKind::Given, "given", "yo");
- let b = Binding::new(StepKind::When, "yo", "do_yo", None, false, HashMap::new()).unwrap();
- assert!(b.match_with_step(&step).is_none());
+ let b = Binding::new(StepKind::When, "yo", false, HashMap::new()).unwrap();
+ assert!(b.match_with_step("", &step).is_none());
}
#[test]
fn does_not_match_with_wrong_text() {
let step = ScenarioStep::new(StepKind::Given, "given", "foo");
- let b = Binding::new(StepKind::Given, "bar", "yo", None, false, HashMap::new()).unwrap();
- assert!(b.match_with_step(&step).is_none());
+ let b = Binding::new(StepKind::Given, "bar", false, HashMap::new()).unwrap();
+ assert!(b.match_with_step("", &step).is_none());
}
#[test]
fn match_with_fixed_pattern() {
let step = ScenarioStep::new(StepKind::Given, "given", "foo");
- let b = Binding::new(
- StepKind::Given,
- "foo",
- "do_foo",
- None,
- false,
- HashMap::new(),
- )
- .unwrap();
- let m = b.match_with_step(&step).unwrap();
+ let b = Binding::new(StepKind::Given, "foo", false, HashMap::new()).unwrap();
+ let m = b.match_with_step("", &step).unwrap();
assert_eq!(m.kind(), StepKind::Given);
let mut parts = m.parts();
let p = parts.next().unwrap();
@@ -371,13 +369,11 @@ mod test_binding {
let b = Binding::new(
StepKind::Given,
r"I am (?P<who>\S+), I am",
- "set_name",
- None,
false,
HashMap::new(),
)
.unwrap();
- let m = b.match_with_step(&step).unwrap();
+ let m = b.match_with_step("", &step).unwrap();
assert_eq!(m.kind(), StepKind::Given);
let mut parts = m.parts();
assert_eq!(parts.next().unwrap(), &PartialStep::uncaptured("I am "));
@@ -389,26 +385,10 @@ mod test_binding {
#[test]
fn case_sensitive_mismatch() {
let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon");
- let b = Binding::new(
- StepKind::Given,
- r"i am tomjon",
- "set_name",
- None,
- false,
- HashMap::new(),
- )
- .unwrap();
- assert!(b.match_with_step(&step).is_some());
- let b = Binding::new(
- StepKind::Given,
- r"i am tomjon",
- "set_name",
- None,
- true,
- HashMap::new(),
- )
- .unwrap();
- assert!(b.match_with_step(&step).is_none());
+ let b = Binding::new(StepKind::Given, r"i am tomjon", false, HashMap::new()).unwrap();
+ assert!(b.match_with_step("", &step).is_some());
+ let b = Binding::new(StepKind::Given, r"i am tomjon", true, HashMap::new()).unwrap();
+ assert!(b.match_with_step("", &step).is_none());
}
}
@@ -425,12 +405,33 @@ impl Default for Bindings {
}
#[derive(Debug, Deserialize)]
+struct ParsedImpl {
+ function: String,
+ cleanup: Option<String>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(transparent)]
+struct ParsedImplWrapper {
+ #[serde(deserialize_with = "deserialize_struct_case_insensitive")]
+ pimpl: ParsedImpl,
+}
+
+impl Deref for ParsedImplWrapper {
+ type Target = ParsedImpl;
+
+ fn deref(&self) -> &Self::Target {
+ &self.pimpl
+ }
+}
+
+#[derive(Debug, Deserialize)]
struct ParsedBinding {
given: Option<String>,
when: Option<String>,
then: Option<String>,
- function: String,
- cleanup: Option<String>,
+ #[serde(default, rename = "impl")]
+ impls: HashMap<String, ParsedImplWrapper>,
regex: Option<bool>,
#[serde(default)]
case_sensitive: bool,
@@ -483,11 +484,11 @@ impl Bindings {
/// Find the binding matching a given scenario step, if there is
/// exactly one.
- pub fn find(&self, step: &ScenarioStep) -> Result<MatchedStep> {
+ pub fn find(&self, template: &str, step: &ScenarioStep) -> Result<MatchedStep> {
let mut matches: Vec<MatchedStep> = self
.bindings()
.iter()
- .filter_map(|b| b.match_with_step(step))
+ .filter_map(|b| b.match_with_step(template, step))
.collect();
if matches.len() > 1 {
// Too many matching bindings.
@@ -506,11 +507,11 @@ impl Bindings {
/// Add bindings from a file.
#[instrument(level = "trace", skip(self))]
- pub fn add_from_file<P>(&mut self, filename: P) -> Result<()>
+ pub fn add_from_file<P>(&mut self, filename: P, template: Option<&str>) -> Result<()>
where
P: AsRef<Path> + Debug,
{
- let yaml = resource::read_as_string(filename.as_ref())
+ let yaml = resource::read_as_string(filename.as_ref(), template)
.map_err(|e| SubplotError::BindingsFileNotFound(filename.as_ref().into(), e))?;
event!(Level::TRACE, "Loaded file content");
self.add_from_yaml(&yaml).map_err(|e| {
@@ -570,17 +571,16 @@ fn from_hashmap(parsed: &ParsedBinding) -> Result<Binding> {
?kind,
?pattern,
?types,
- "Successfully prepared binding"
+ "Successfully acquired binding"
);
- Binding::new(
- kind,
- &pattern,
- &parsed.function,
- parsed.cleanup.as_deref(),
- parsed.case_sensitive,
- types,
- )
+ let mut ret = Binding::new(kind, &pattern, parsed.case_sensitive, types)?;
+ event!(Level::TRACE, "Binding parsed OK");
+ for (template, pimpl) in &parsed.impls {
+ ret.add_impl(template, &pimpl.function, pimpl.cleanup.as_deref());
+ }
+
+ Ok(ret)
}
#[cfg(test)]
@@ -605,8 +605,6 @@ mod test_bindings {
let binding = Binding::new(
StepKind::Given,
r"I am (?P<name>\S+)",
- "set_name",
- None,
false,
HashMap::new(),
)
@@ -620,16 +618,26 @@ mod test_bindings {
fn adds_from_yaml() {
let yaml = "
- GIVEN: I am Tomjon
- function: set_name
+ impl:
+ python:
+ function: set_name
- when: I declare myself king
- Function: declare_king
+ impl:
+ python:
+ Function: declare_king
- tHEn: there is applause
- function: check_for_applause
+ impl:
+ python:
+ function: check_for_applause
- given: you are alice
- function: other_name
+ impl:
+ python:
+ function: other_name
case_sensitive: true
- then: the total is {total}
- function: check_total
+ impl:
+ python:
+ function: check_total
types:
total: word
";
@@ -650,7 +658,9 @@ mod test_bindings {
let yaml = "
- Given: I am Tomjon
wheN: I am indeed Tomjon
- FUNCTION: set_name
+ impl:
+ python:
+ FUNCTION: set_name
";
match Bindings::new().add_from_yaml(yaml) {
Ok(_) => unreachable!(),
@@ -663,7 +673,9 @@ mod test_bindings {
fn typemap_must_match_pattern() {
let yaml = "
- then: you are {age:word} years old
- function: check_age
+ impl:
+ python:
+ function: check_age
types:
age: number
";
@@ -677,19 +689,11 @@ mod test_bindings {
#[test]
fn does_not_find_match_for_unmatching_kind() {
let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon");
- let binding = Binding::new(
- StepKind::When,
- r"I am Tomjon",
- "set_foo",
- None,
- false,
- HashMap::new(),
- )
- .unwrap();
+ let binding = Binding::new(StepKind::When, r"I am Tomjon", false, HashMap::new()).unwrap();
let mut bindings = Bindings::new();
bindings.add(binding);
assert!(matches!(
- bindings.find(&step),
+ bindings.find("", &step),
Err(SubplotError::BindingUnknown(_))
));
}
@@ -700,8 +704,6 @@ mod test_bindings {
let binding = Binding::new(
StepKind::Given,
r"I am Tomjon of Lancre",
- "set_foo",
- None,
false,
HashMap::new(),
)
@@ -709,7 +711,7 @@ mod test_bindings {
let mut bindings = Bindings::new();
bindings.add(binding);
assert!(matches!(
- bindings.find(&step),
+ bindings.find("", &step),
Err(SubplotError::BindingUnknown(_))
));
}
@@ -718,31 +720,19 @@ mod test_bindings {
fn two_matching_bindings() {
let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon");
let mut bindings = Bindings::default();
- bindings.add(
- Binding::new(
- StepKind::Given,
- r"I am Tomjon",
- "set_foo",
- None,
- false,
- HashMap::new(),
- )
- .unwrap(),
- );
+ bindings.add(Binding::new(StepKind::Given, r"I am Tomjon", false, HashMap::new()).unwrap());
bindings.add(
Binding::new(
StepKind::Given,
&super::regex_from_simple_pattern(r"I am {name}", false, &mut HashMap::new())
.unwrap(),
- "set_foo",
- None,
false,
HashMap::new(),
)
.unwrap(),
);
assert!(matches!(
- bindings.find(&step),
+ bindings.find("", &step),
Err(SubplotError::BindingNotUnique(_, _))
));
}
@@ -750,18 +740,10 @@ mod test_bindings {
#[test]
fn finds_match_for_fixed_string_pattern() {
let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon");
- let binding = Binding::new(
- StepKind::Given,
- r"I am Tomjon",
- "set_name",
- None,
- false,
- HashMap::new(),
- )
- .unwrap();
+ let binding = Binding::new(StepKind::Given, r"I am Tomjon", false, HashMap::new()).unwrap();
let mut bindings = Bindings::new();
bindings.add(binding);
- let m = bindings.find(&step).unwrap();
+ let m = bindings.find("", &step).unwrap();
assert_eq!(m.kind(), StepKind::Given);
let mut parts = m.parts();
let p = parts.next().unwrap();
@@ -778,15 +760,13 @@ mod test_bindings {
let binding = Binding::new(
StepKind::Given,
r"I am (?P<name>\S+)",
- "set_name",
- None,
false,
HashMap::new(),
)
.unwrap();
let mut bindings = Bindings::new();
bindings.add(binding);
- let m = bindings.find(&step).unwrap();
+ let m = bindings.find("", &step).unwrap();
assert_eq!(m.kind(), StepKind::Given);
let mut parts = m.parts();
let p = parts.next().unwrap();
diff --git a/src/codegen.rs b/src/codegen.rs
index d27a4d8..89ea591 100644
--- a/src/codegen.rs
+++ b/src/codegen.rs
@@ -27,22 +27,25 @@ pub fn generate_test_program(
doc: &mut Document,
spec: &TemplateSpec,
filename: &Path,
+ template: &str,
) -> Result<(), SubplotError> {
- let context = context(doc)?;
- let code = tera(spec)?.render("template", &context).expect("render");
+ let context = context(doc, template)?;
+ let code = tera(spec, template)?
+ .render("template", &context)
+ .expect("render");
write(filename, &code)?;
Ok(())
}
-fn context(doc: &mut Document) -> Result<Context, SubplotError> {
+fn context(doc: &mut Document, template: &str) -> Result<Context, SubplotError> {
let mut context = Context::new();
- context.insert("scenarios", &doc.matched_scenarios()?);
+ context.insert("scenarios", &doc.matched_scenarios(template)?);
context.insert("files", doc.files());
let funcs_filenames = doc.meta().functions_filenames();
let mut funcs = vec![];
for filename in funcs_filenames {
- let content = resource::read_as_string(filename)
+ let content = resource::read_as_string(filename, Some(template))
.map_err(|err| SubplotError::FunctionsFileNotFound(filename.into(), err))?;
funcs.push(Func::new(filename, content));
}
@@ -51,7 +54,7 @@ fn context(doc: &mut Document) -> Result<Context, SubplotError> {
Ok(context)
}
-fn tera(tmplspec: &TemplateSpec) -> Result<Tera, SubplotError> {
+fn tera(tmplspec: &TemplateSpec, templatename: &str) -> Result<Tera, SubplotError> {
// Tera insists on a glob, but we want to load a specific template
// only, so we use a glob that doesn't match anything.
let mut tera = Tera::new("/..IGNORE-THIS../..SUBPLOT-TERA-NOT-EXIST../*").expect("new");
@@ -61,12 +64,12 @@ fn tera(tmplspec: &TemplateSpec) -> Result<Tera, SubplotError> {
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)?;
+ let helper_content = resource::read_as_string(helper_path, Some(templatename))?;
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 template = resource::read_as_string(tmplspec.template_filename())?;
+ let template = resource::read_as_string(tmplspec.template_filename(), Some(templatename))?;
tera.add_raw_template("template", &template)
.map_err(|err| {
SubplotError::TemplateError(tmplspec.template_filename().display().to_string(), err)
diff --git a/src/doc.rs b/src/doc.rs
index 1fa9b3b..5d9d3b1 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -268,14 +268,14 @@ impl<'a> Document {
/// Check that all named files (in matched steps) are actually present in the
/// document.
- pub fn check_named_files_exist(&mut self) -> Result<bool> {
+ pub fn check_named_files_exist(&mut self, template: &str) -> Result<bool> {
let filenames: HashSet<_> = self
.files()
.iter()
.map(|f| f.filename().to_lowercase())
.collect();
let mut okay = true;
- let scenarios = match self.matched_scenarios() {
+ let scenarios = match self.matched_scenarios(template) {
Ok(scenarios) => scenarios,
Err(_) => return Ok(true), // We can't do the check, so say it's okay.
};
@@ -296,6 +296,30 @@ impl<'a> Document {
Ok(okay)
}
+ /// Check that all matched steps actually have function implementations
+ pub fn check_matched_steps_have_impl(&mut self, template: &str) -> bool {
+ let mut okay = true;
+ let scenarios = match self.matched_scenarios(template) {
+ Ok(scenarios) => scenarios,
+ Err(_) => return true, // No matches means no missing impls
+ };
+ for scenario in scenarios {
+ let mut said_scenario = false;
+ for step in scenario.steps() {
+ if step.function().is_none() {
+ if !said_scenario {
+ eprintln!("Scenario: '{}'", scenario.title());
+ eprintln!(" Template: '{}'", template);
+ said_scenario = true;
+ }
+ eprintln!(" Step missing implementation: '{}'", step.text());
+ okay = false;
+ }
+ }
+ }
+ okay
+ }
+
/// Typeset a Subplot document.
pub fn typeset(&mut self) {
let mut visitor =
@@ -322,12 +346,12 @@ impl<'a> Document {
}
/// Return matched scenarios in a document.
- pub fn matched_scenarios(&mut self) -> Result<Vec<MatchedScenario>> {
+ pub fn matched_scenarios(&mut self, template: &str) -> Result<Vec<MatchedScenario>> {
let scenarios = self.scenarios()?;
let bindings = self.meta().bindings();
scenarios
.iter()
- .map(|scen| MatchedScenario::new(scen, bindings))
+ .map(|scen| MatchedScenario::new(template, scen, bindings))
.collect()
}
}
diff --git a/src/matches.rs b/src/matches.rs
index f6d10b9..f8e527a 100644
--- a/src/matches.rs
+++ b/src/matches.rs
@@ -17,11 +17,11 @@ pub struct MatchedScenario {
impl MatchedScenario {
/// Construct a new matched scenario
- pub fn new(scen: &Scenario, bindings: &Bindings) -> Result<MatchedScenario> {
+ pub fn new(template: &str, scen: &Scenario, bindings: &Bindings) -> Result<MatchedScenario> {
let steps: Result<Vec<MatchedStep>> = scen
.steps()
.iter()
- .map(|step| bindings.find(step))
+ .map(|step| bindings.find(template, step))
.collect();
Ok(MatchedScenario {
title: scen.title().to_string(),
@@ -33,6 +33,11 @@ impl MatchedScenario {
pub fn steps(&self) -> &[MatchedStep] {
&self.steps
}
+
+ /// Retrieve the scenario title
+ pub fn title(&self) -> &str {
+ &self.title
+ }
}
/// A list of matched steps.
@@ -65,22 +70,23 @@ pub struct MatchedStep {
pattern: String,
text: String,
parts: Vec<PartialStep>,
- function: String,
+ function: Option<String>,
cleanup: Option<String>,
types: HashMap<String, CaptureType>,
}
impl MatchedStep {
/// Return a new empty match. Empty means it has no step parts.
- pub fn new(binding: &Binding, types: &HashMap<String, CaptureType>) -> MatchedStep {
+ pub fn new(binding: &Binding, template: &str) -> MatchedStep {
+ let bimpl = binding.step_impl(template);
MatchedStep {
kind: binding.kind(),
pattern: binding.pattern().to_string(),
text: "".to_string(),
parts: vec![],
- function: binding.function().to_string(),
- cleanup: binding.cleanup().map(String::from),
- types: types.clone(),
+ function: bimpl.clone().map(|b| b.function().to_owned()),
+ cleanup: bimpl.and_then(|b| b.cleanup().map(String::from)),
+ types: binding.types().map(|(s, c)| (s.to_string(), c)).collect(),
}
}
@@ -90,14 +96,14 @@ impl MatchedStep {
}
/// The name of the function to call for the step.
- pub fn function(&self) -> &str {
- &self.function
+ pub fn function(&self) -> Option<&str> {
+ self.function.as_deref()
}
/// Append a partial step to the match.
pub fn append_part(&mut self, part: PartialStep) {
self.parts.push(part);
- self.text = self.text();
+ self.text = self.update_text();
}
/// Iterate over all partial steps in the match.
@@ -109,7 +115,7 @@ impl MatchedStep {
self.pattern.to_string()
}
- fn text(&self) -> String {
+ fn update_text(&self) -> String {
let mut t = String::new();
for part in self.parts() {
t.push_str(part.as_text());
@@ -117,6 +123,11 @@ impl MatchedStep {
t
}
+ /// Return the step's text
+ pub fn text(&self) -> &str {
+ &self.text
+ }
+
/// Return the typemap for the matched step
pub fn types(&self) -> &HashMap<String, CaptureType> {
&self.types
diff --git a/src/metadata.rs b/src/metadata.rs
index 27b07b4..0ee0b1a 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -1,4 +1,4 @@
-use crate::{resource, Result};
+use crate::Result;
use crate::{Bindings, TemplateSpec};
use std::fmt::Debug;
@@ -33,8 +33,8 @@ impl Metadata {
{
let title = get_title(&doc.meta);
let date = get_date(&doc.meta);
- let bindings_filenames = get_bindings_filenames(basedir.as_ref(), &doc.meta);
- let functions_filenames = get_functions_filenames(basedir.as_ref(), &doc.meta);
+ let bindings_filenames = get_bindings_filenames(&doc.meta);
+ let functions_filenames = get_functions_filenames(&doc.meta);
let bibliographies = get_bibliographies(basedir.as_ref(), &doc.meta);
let classes = get_classes(&doc.meta);
event!(
@@ -48,7 +48,6 @@ impl Metadata {
"Loaded basic metadata"
);
let (template, spec) = if let Some((template, spec)) = get_template_spec(&doc.meta)? {
- resource::set_template(&template);
(Some(template), Some(spec))
} else {
(None, None)
@@ -56,7 +55,7 @@ impl Metadata {
event!(Level::TRACE, ?template, ?spec, "Loaded template spec");
let mut bindings = Bindings::new();
- get_bindings(&bindings_filenames, &mut bindings)?;
+ get_bindings(&bindings_filenames, &mut bindings, template.as_deref())?;
event!(Level::TRACE, "Loaded all metadata successfully");
Ok(Metadata {
title,
@@ -129,18 +128,12 @@ fn get_date(map: &Mapp) -> Option<String> {
get_string(map, "date")
}
-fn get_bindings_filenames<P>(basedir: P, map: &Mapp) -> Vec<PathBuf>
-where
- P: AsRef<Path>,
-{
- get_paths(basedir, map, "bindings")
+fn get_bindings_filenames(map: &Mapp) -> Vec<PathBuf> {
+ get_paths("", map, "bindings")
}
-fn get_functions_filenames<P>(basedir: P, map: &Mapp) -> Vec<PathBuf>
-where
- P: AsRef<Path>,
-{
- get_paths(basedir, map, "functions")
+fn get_functions_filenames(map: &Mapp) -> Vec<PathBuf> {
+ get_paths("", map, "functions")
}
fn get_template_spec(map: &Mapp) -> Result<Option<(String, TemplateSpec)>> {
@@ -295,12 +288,12 @@ mod test_join {
}
}
-fn get_bindings<P>(filenames: &[P], bindings: &mut Bindings) -> Result<()>
+fn get_bindings<P>(filenames: &[P], bindings: &mut Bindings, template: Option<&str>) -> Result<()>
where
P: AsRef<Path> + Debug,
{
for filename in filenames {
- bindings.add_from_file(filename)?;
+ bindings.add_from_file(filename, template)?;
}
Ok(())
}
diff --git a/src/resource.rs b/src/resource.rs
index 0149cd7..43967ef 100644
--- a/src/resource.rs
+++ b/src/resource.rs
@@ -45,10 +45,9 @@ lazy_static! {
let ret = Vec::new();
Mutex::new(ret)
};
- static ref TEMPLATE_NAME: Mutex<Option<String>> = Mutex::new(None);
}
-static EMBEDDED_FILES: &[(&str, &[u8])] = include!(concat!(env!("OUT_DIR"), "/embedded_files.inc"));
+static EMBEDDED_FILES: &[(&str, &[u8])] = include!(concat!(env!("OUT_DIR"), "/embedded_files.rs"));
/// Retrieve the embedded file list
pub fn embedded_files() -> &'static [(&'static str, &'static [u8])] {
@@ -63,11 +62,6 @@ fn add_search_path<P: AsRef<Path>>(path: P) {
.push(path.as_ref().into());
}
-/// Set the template name, for use in searching for content...
-pub fn set_template(template: &str) {
- *TEMPLATE_NAME.lock().expect("Unable to lock TEMPLATE_NAME") = Some(template.to_string());
-}
-
/// Open a file for reading, honouring search paths established during
/// startup, and falling back to potentially embedded file content.
///
@@ -77,32 +71,39 @@ pub fn set_template(template: &str) {
/// Second, it's relative to the input document.
/// Finally we check for an embedded file.
///
-/// Then we repeat all the above, inserting the template name in the subpath
+/// Then we repeat all the above, inserting 'common' as the template name, and
+/// finally repeat the above inserting the real template name in the subpath
/// too.
-fn open<P: AsRef<Path>>(subpath: P) -> io::Result<Box<dyn Read>> {
+fn open<P: AsRef<Path>>(subpath: P, template: Option<&str>) -> io::Result<Box<dyn Read>> {
let subpath = subpath.as_ref();
- match internal_open(subpath) {
- Ok(r) => Ok(r),
- Err(e) => {
- let template = TEMPLATE_NAME.lock().expect("Unable to lock TEMPLATE_NAME");
- if let Some(templ) = template.as_deref() {
- let subpath = Path::new(templ).join(subpath);
- match internal_open(&subpath) {
- Ok(r) => Ok(r),
- Err(sub_e) => {
- if sub_e.kind() != io::ErrorKind::NotFound
- && e.kind() == io::ErrorKind::NotFound
- {
- Err(sub_e)
- } else {
- Err(e)
- }
- }
- }
- } else {
- Err(e)
+ let plain = match internal_open(subpath) {
+ Ok(r) => return Ok(r),
+ Err(e) => e,
+ };
+ let commonpath = Path::new("common").join(subpath);
+ let common = match internal_open(&commonpath) {
+ Ok(r) => return Ok(r),
+ Err(e) => e,
+ };
+ let templated = match template {
+ Some(templ) => {
+ let templpath = Path::new(templ).join(subpath);
+ match internal_open(&templpath) {
+ Ok(r) => return Ok(r),
+ Err(e) => Some(e),
}
}
+ None => None,
+ };
+ if plain.kind() != io::ErrorKind::NotFound {
+ return Err(plain);
+ }
+ if common.kind() != io::ErrorKind::NotFound {
+ return Err(common);
+ }
+ match templated {
+ Some(e) => Err(e),
+ None => Err(common),
}
}
@@ -136,8 +137,8 @@ fn internal_open(subpath: &Path) -> io::Result<Box<dyn Read>> {
/// Read a file, honouring search paths established during startup, and
/// falling back to potentially embedded file content
-pub fn read_as_string<P: AsRef<Path>>(subpath: P) -> io::Result<String> {
- let mut f = open(subpath)?;
+pub fn read_as_string<P: AsRef<Path>>(subpath: P, template: Option<&str>) -> io::Result<String> {
+ let mut f = open(subpath, template)?;
let mut ret = String::with_capacity(8192);
f.read_to_string(&mut ret)?;
Ok(ret)
diff --git a/src/templatespec.rs b/src/templatespec.rs
index b64d282..5d7150b 100644
--- a/src/templatespec.rs
+++ b/src/templatespec.rs
@@ -41,7 +41,7 @@ impl TemplateSpec {
/// Read a template.yaml file and create the corresponding TemplateSpec.
pub fn from_file(filename: &Path) -> Result<TemplateSpec> {
- let yaml = resource::read_as_string(filename)?;
+ let yaml = resource::read_as_string(filename, None)?;
let spec = TemplateSpec::from_yaml(&yaml)?;
let dirname = match filename.parent() {
Some(x) => x,
diff --git a/src/typeset.rs b/src/typeset.rs
index 73cc1a2..97780a4 100644
--- a/src/typeset.rs
+++ b/src/typeset.rs
@@ -81,7 +81,7 @@ fn step(
}
let step = step.unwrap();
- let m = match bindings.find(&step) {
+ let m = match bindings.find("", &step) {
Ok(m) => m,
Err(e) => {
eprintln!("Could not select binding: {:?}", e);