diff options
author | Lars Wirzenius <liw@liw.fi> | 2021-09-13 09:44:39 +0000 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2021-09-13 09:44:39 +0000 |
commit | a06ef1f46c8603b8b5e5a44e6521b10238331891 (patch) | |
tree | 995392da4d56597be9ac71bba4669df299506ea7 /src | |
parent | 217d920d72d9bf56174e0694329ef9c558160b69 (diff) | |
parent | 66bedaf70e7b652f791dd0e2fcbd39db3cbec6f9 (diff) | |
download | subplot-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.rs | 14 | ||||
-rw-r--r-- | src/bindings.rs | 334 | ||||
-rw-r--r-- | src/codegen.rs | 19 | ||||
-rw-r--r-- | src/doc.rs | 32 | ||||
-rw-r--r-- | src/matches.rs | 33 | ||||
-rw-r--r-- | src/metadata.rs | 27 | ||||
-rw-r--r-- | src/resource.rs | 63 | ||||
-rw-r--r-- | src/templatespec.rs | 2 | ||||
-rw-r--r-- | src/typeset.rs | 2 |
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) @@ -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); |