summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDaniel Silverstone <dsilvers@digital-scurf.org>2021-10-19 19:30:37 +0100
committerDaniel Silverstone <dsilvers@digital-scurf.org>2021-11-19 20:18:50 +0000
commit5a92266170ae9879b0651e6074e538a4de0a214c (patch)
treed5ecb647701534d6374f441f2c790bfdd2dca848 /src
parent572d3097770cd8e5cd22d7767c1d18e0d50b9a90 (diff)
downloadsubplot-5a92266170ae9879b0651e6074e538a4de0a214c.tar.gz
various: Rework document to support multiple implementations
In order to eventually shift the document metadata to support more than one template defined for the document this reworks all the internal APIs to expect templates, and also the external CLI to be able to provide it. Signed-off-by: Daniel Silverstone <dsilvers@digital-scurf.org>
Diffstat (limited to 'src')
-rw-r--r--src/bin/cli/mod.rs30
-rw-r--r--src/bin/subplot.rs38
-rw-r--r--src/codegen.rs32
-rw-r--r--src/doc.rs52
-rw-r--r--src/error.rs11
-rw-r--r--src/lib.rs2
-rw-r--r--src/metadata.rs61
7 files changed, 157 insertions, 69 deletions
diff --git a/src/bin/cli/mod.rs b/src/bin/cli/mod.rs
index 84bda10..4f172e2 100644
--- a/src/bin/cli/mod.rs
+++ b/src/bin/cli/mod.rs
@@ -4,10 +4,10 @@
use anyhow::Result;
use serde::Serialize;
-use std::convert::TryFrom;
use std::fmt::Debug;
use std::path::Path;
use std::str::FromStr;
+use std::{collections::HashMap, convert::TryFrom};
use subplot::{DataFile, Document, Style, SubplotError};
use tracing::{event, instrument, Level};
@@ -25,7 +25,7 @@ pub struct Metadata {
sources: Vec<String>,
title: String,
binding_files: Vec<String>,
- function_files: Vec<String>,
+ impls: HashMap<String, Vec<String>>,
bibliographies: Vec<String>,
scenarios: Vec<String>,
files: Vec<String>,
@@ -35,7 +35,7 @@ impl TryFrom<&mut Document> for Metadata {
type Error = subplot::SubplotError;
fn try_from(doc: &mut Document) -> std::result::Result<Self, Self::Error> {
let mut sources: Vec<_> = doc
- .sources()
+ .sources(None)
.into_iter()
.map(|p| filename(Some(&p)))
.collect();
@@ -48,13 +48,21 @@ impl TryFrom<&mut Document> for Metadata {
.map(|p| filename(Some(p)))
.collect();
binding_files.sort_unstable();
- let mut function_files: Vec<_> = doc
+ let impls: HashMap<_, _> = doc
.meta()
- .functions_filenames()
- .into_iter()
- .map(|p| filename(Some(p)))
+ .templates()
+ .map(|template| {
+ let mut filenames: Vec<_> = doc
+ .meta()
+ .document_impl(template)
+ .unwrap()
+ .functions_filenames()
+ .map(|p| filename(Some(p)))
+ .collect();
+ filenames.sort_unstable();
+ (template.to_string(), filenames)
+ })
.collect();
- function_files.sort_unstable();
let mut bibliographies: Vec<_> = doc
.meta()
.bibliographies()
@@ -78,7 +86,7 @@ impl TryFrom<&mut Document> for Metadata {
sources,
title,
binding_files,
- function_files,
+ impls,
bibliographies,
scenarios,
files,
@@ -95,7 +103,9 @@ impl Metadata {
Self::write_list(&self.sources, "source");
println!("title: {}", self.title);
Self::write_list(&self.binding_files, "bindings");
- Self::write_list(&self.function_files, "functions");
+ for (template, filenames) in self.impls.iter() {
+ Self::write_list(filenames, &format!("functions[{}]", template));
+ }
Self::write_list(&self.bibliographies, "bibliography");
Self::write_list(&self.files, "file");
Self::write_list(&self.scenarios, "scenario");
diff --git a/src/bin/subplot.rs b/src/bin/subplot.rs
index 8d4a660..08c4713 100644
--- a/src/bin/subplot.rs
+++ b/src/bin/subplot.rs
@@ -281,6 +281,12 @@ impl Metadata {
///
/// Process a subplot document and typeset it using Pandoc.
struct Docgen {
+ /// The template to use from the document.
+ ///
+ /// If not specified, subplot will try and find a unique template name from the document
+ #[structopt(name = "TEMPLATE", long = "--template", short = "-t")]
+ template: Option<String>,
+
// Input Subplot document
#[structopt(parse(from_os_str))]
input: PathBuf,
@@ -314,7 +320,13 @@ impl Docgen {
event!(Level::TRACE, "Doc linted ok");
let meta = doc.meta();
event!(Level::TRACE, ?meta, "Looking for template");
- let template = meta.template_name().unwrap_or("").to_string();
+ let template = self
+ .template
+ .as_deref()
+ .map(Ok)
+ .unwrap_or_else(|| doc.template())
+ .unwrap_or("");
+ let template = template.to_string();
event!(Level::TRACE, ?template);
if !doc.check_named_files_exist(&template)? || !doc.check_matched_steps_have_impl(&template)
{
@@ -339,7 +351,7 @@ impl Docgen {
pandoc.add_option(pandoc::PandocOption::Standalone);
pandoc.add_option(pandoc::PandocOption::NumberSections);
- if Self::need_output(&mut doc, &self.output) {
+ if Self::need_output(&mut doc, &template, &self.output) {
doc.typeset();
pandoc.set_input_format(pandoc::InputFormat::Json, vec![]);
pandoc.set_input(pandoc::InputKind::Pipe(doc.ast()?));
@@ -362,13 +374,13 @@ impl Docgen {
dt.format("%Y-%m-%d %H:%M").to_string()
}
- fn need_output(doc: &mut subplot::Document, output: &Path) -> bool {
+ fn need_output(doc: &mut subplot::Document, template: &str, output: &Path) -> bool {
let output = match Self::mtime(output) {
Err(_) => return true,
Ok(ts) => ts,
};
- for filename in doc.sources() {
+ for filename in doc.sources(Some(template)) {
let source = match Self::mtime(&filename) {
Err(_) => return true,
Ok(ts) => ts,
@@ -387,6 +399,12 @@ impl Docgen {
/// This reads a subplot document, extracts the scenarios, and writes out a test
/// program capable of running the scenarios in the subplot document.
struct Codegen {
+ /// The template to use from the document.
+ ///
+ /// If not specified, subplot will try and find a unique template name from the document
+ #[structopt(name = "TEMPLATE", long = "--template", short = "-t")]
+ template: Option<String>,
+
/// Input filename.
#[structopt(parse(from_os_str))]
filename: PathBuf,
@@ -414,13 +432,19 @@ impl Codegen {
let span = span!(Level::TRACE, "codegen");
let _enter = span.enter();
- let output = codegen(&self.filename, &self.output)?;
+ let output = codegen(&self.filename, &self.output, self.template.as_deref())?;
if self.run {
- let run = match output.spec.run() {
+ let spec = output
+ .doc
+ .meta()
+ .document_impl(&output.template)
+ .unwrap()
+ .spec();
+ let run = match spec.run() {
None => {
eprintln!(
"Template {} does not specify how to run suites",
- output.spec.template_filename().display()
+ spec.template_filename().display()
);
std::process::exit(1);
}
diff --git a/src/codegen.rs b/src/codegen.rs
index b67d94a..b940b4d 100644
--- a/src/codegen.rs
+++ b/src/codegen.rs
@@ -9,28 +9,19 @@ use base64::encode;
use serde::Serialize;
use tera::{Context, Tera, Value};
-/// Return the requested template specification.
-pub fn template_spec(doc: &Document) -> Result<TemplateSpec, SubplotError> {
- let template = doc
- .meta()
- .template_name()
- .ok_or(SubplotError::MissingTemplate)?;
-
- let mut filename = PathBuf::from(template);
- filename.push("template");
- filename.push("template.yaml");
- TemplateSpec::from_file(&filename)
-}
-
/// Generate a test program from a document, using a template spec.
pub fn generate_test_program(
doc: &mut Document,
- spec: &TemplateSpec,
filename: &Path,
template: &str,
) -> Result<(), SubplotError> {
let context = context(doc, template)?;
- let code = tera(spec, template)?
+ let docimpl = doc
+ .meta()
+ .document_impl(template)
+ .ok_or(SubplotError::MissingTemplate)?;
+
+ let code = tera(docimpl.spec(), template)?
.render("template", &context)
.expect("render");
write(filename, &code)?;
@@ -43,12 +34,13 @@ fn context(doc: &mut Document, template: &str) -> Result<Context, SubplotError>
context.insert("scenarios", &scenarios);
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, Some(template))
- .map_err(|err| SubplotError::FunctionsFileNotFound(filename.into(), err))?;
- funcs.push(Func::new(filename, content));
+ 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);
diff --git a/src/doc.rs b/src/doc.rs
index ffdd4ac..25dbdb3 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -12,7 +12,6 @@ use crate::Scenario;
use crate::ScenarioStep;
use crate::Style;
use crate::{bindings::CaptureType, parser::parse_scenario_snippet};
-use crate::{template_spec, TemplateSpec};
use crate::{Result, SubplotError};
use std::collections::HashSet;
@@ -216,15 +215,27 @@ impl<'a> Document {
///
/// The sources are any files that affect the output so that if
/// the source file changes, the output needs to be re-generated.
- pub fn sources(&mut self) -> Vec<PathBuf> {
+ pub fn sources(&mut self, template: Option<&str>) -> Vec<PathBuf> {
let mut names = vec![];
for x in self.meta().bindings_filenames() {
names.push(PathBuf::from(x))
}
- for x in self.meta().functions_filenames() {
- names.push(PathBuf::from(x))
+ if let Some(template) = template {
+ if let Some(spec) = self.meta().document_impl(template) {
+ for x in spec.functions_filenames() {
+ names.push(PathBuf::from(x));
+ }
+ }
+ } else {
+ for template in self.meta().templates() {
+ if let Some(spec) = self.meta().document_impl(template) {
+ for x in spec.functions_filenames() {
+ names.push(PathBuf::from(x));
+ }
+ }
+ }
}
for x in self.meta().bibliographies().iter() {
@@ -422,6 +433,18 @@ impl<'a> Document {
.map(|scen| MatchedScenario::new(template, scen, bindings))
.collect()
}
+
+ /// Extract a template name from this document
+ pub fn template(&self) -> Result<&str> {
+ let templates: Vec<_> = self.meta().templates().collect();
+ if templates.len() == 1 {
+ Ok(templates[0])
+ } else if templates.is_empty() {
+ Err(SubplotError::MissingTemplate)
+ } else {
+ Err(SubplotError::AmbiguousTemplate)
+ }
+ }
}
/// Load a `Document` from a file.
@@ -475,16 +498,15 @@ where
}
/// Generate code for one document.
-pub fn codegen(filename: &Path, output: &Path) -> Result<CodegenOutput> {
+pub fn codegen(filename: &Path, output: &Path, template: Option<&str>) -> Result<CodegenOutput> {
let span = span!(Level::TRACE, "codegen");
let _enter = span.enter();
let mut doc = load_document_with_pullmark(filename, Style::default())?;
doc.lint()?;
- let template = doc
- .meta()
- .template_name()
- .ok_or(SubplotError::MissingTemplate)?
+ let template = template
+ .map(Ok)
+ .unwrap_or_else(|| doc.template())?
.to_string();
event!(Level::TRACE, ?template);
if !doc.check_named_files_exist(&template)? || !doc.check_matched_steps_have_impl(&template) {
@@ -494,21 +516,21 @@ pub fn codegen(filename: &Path, output: &Path) -> Result<CodegenOutput> {
}
event!(Level::TRACE, "Generating code");
- let spec = template_spec(&doc)?;
- generate_test_program(&mut doc, &spec, output, &template)?;
+
+ generate_test_program(&mut doc, output, &template)?;
event!(Level::TRACE, "Finished generating code");
- Ok(CodegenOutput::new(spec, doc))
+ Ok(CodegenOutput::new(template, doc))
}
pub struct CodegenOutput {
- pub spec: TemplateSpec,
+ pub template: String,
pub doc: Document,
}
impl CodegenOutput {
- fn new(spec: TemplateSpec, doc: Document) -> Self {
- Self { spec, doc }
+ fn new(template: String, doc: Document) -> Self {
+ Self { template, doc }
}
}
diff --git a/src/error.rs b/src/error.rs
index c041eaf..ec88733 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -112,10 +112,19 @@ pub enum SubplotError {
/// The document YAML metadata does not define the template to use
/// during code generation.
///
- /// To fix, add a `template` field.
+ /// To fix, ensure an appropriate `impl` entry is present.
#[error("document has no template")]
MissingTemplate,
+ /// Document has more than one template
+ ///
+ /// The document YAML metadata specifies more than one possible
+ /// template implementation to be used during code generation.
+ ///
+ /// To fix, specify `--template` on the codegen CLI.
+ #[error("document has more than one template possibility")]
+ AmbiguousTemplate,
+
/// Pandoc AST is not JSON
///
/// Subplot acts as a Pandoc filter, and as part of that Pandoc
diff --git a/src/lib.rs b/src/lib.rs
index 59d9fb2..314ad2a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -69,7 +69,7 @@ mod templatespec;
pub use templatespec::TemplateSpec;
mod codegen;
-pub use codegen::{generate_test_program, template_spec};
+pub use codegen::generate_test_program;
mod ast;
pub use ast::AbstractSyntaxTree;
diff --git a/src/metadata.rs b/src/metadata.rs
index 0ee0b1a..03d4eff 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -1,6 +1,7 @@
use crate::Result;
use crate::{Bindings, TemplateSpec};
+use std::collections::HashMap;
use std::fmt::Debug;
use std::ops::Deref;
use std::path::{Path, PathBuf};
@@ -16,14 +17,18 @@ pub struct Metadata {
date: Option<String>,
bindings_filenames: Vec<PathBuf>,
bindings: Bindings,
- functions_filenames: Vec<PathBuf>,
- template: Option<String>,
- spec: Option<TemplateSpec>,
+ impls: HashMap<String, DocumentImpl>,
bibliographies: Vec<PathBuf>,
/// Extra class names which should be considered 'correct' for this document
classes: Vec<String>,
}
+#[derive(Debug)]
+pub struct DocumentImpl {
+ spec: TemplateSpec,
+ functions: Vec<PathBuf>,
+}
+
impl Metadata {
/// Construct a Metadata from a Document, if possible.
#[instrument(level = "trace", skip(doc))]
@@ -57,14 +62,22 @@ impl Metadata {
get_bindings(&bindings_filenames, &mut bindings, template.as_deref())?;
event!(Level::TRACE, "Loaded all metadata successfully");
+ let mut impls = HashMap::new();
+
+ if let (Some(template), Some(spec)) = (template, spec) {
+ let mut docimpl = DocumentImpl::new(spec);
+ for fname in functions_filenames {
+ docimpl.add_function_file(fname);
+ }
+ impls.insert(template, docimpl);
+ }
+
Ok(Metadata {
title,
date,
bindings_filenames,
bindings,
- functions_filenames,
- template,
- spec,
+ impls,
bibliographies,
classes,
})
@@ -85,17 +98,14 @@ impl Metadata {
self.bindings_filenames.iter().map(|f| f.as_ref()).collect()
}
- /// Return filename where functions are specified.
- pub fn functions_filenames(&self) -> Vec<&Path> {
- self.functions_filenames
- .iter()
- .map(|f| f.as_ref())
- .collect()
+ /// Return the document implementation (filenames, spec, etc) for the given template name
+ pub fn document_impl(&self, template: &str) -> Option<&DocumentImpl> {
+ self.impls.get(template)
}
- /// Return the name of the code template, if specified.
- pub fn template_name(&self) -> Option<&str> {
- self.template.as_deref()
+ /// Return the templates the document expects to implement
+ pub fn templates(&self) -> impl Iterator<Item = &str> {
+ self.impls.keys().map(String::as_str)
}
/// Return the bindings.
@@ -114,6 +124,27 @@ impl Metadata {
}
}
+impl DocumentImpl {
+ fn new(spec: TemplateSpec) -> Self {
+ Self {
+ spec,
+ functions: Vec::new(),
+ }
+ }
+
+ fn add_function_file(&mut self, function: PathBuf) {
+ self.functions.push(function);
+ }
+
+ pub fn functions_filenames(&self) -> impl Iterator<Item = &Path> {
+ self.functions.iter().map(PathBuf::as_path)
+ }
+
+ pub fn spec(&self) -> &TemplateSpec {
+ &self.spec
+ }
+}
+
type Mapp = Map<String, MetaValue>;
fn get_title(map: &Mapp) -> String {