summaryrefslogtreecommitdiff
path: root/src/bin/subplot.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/subplot.rs')
-rw-r--r--src/bin/subplot.rs304
1 files changed, 156 insertions, 148 deletions
diff --git a/src/bin/subplot.rs b/src/bin/subplot.rs
index f693a24..8dc8973 100644
--- a/src/bin/subplot.rs
+++ b/src/bin/subplot.rs
@@ -6,15 +6,15 @@ use anyhow::Result;
use env_logger::fmt::Color;
use log::{debug, error, info, trace, warn};
use subplot::{
- codegen, load_document, resource, DataFile, Document, MarkupOpts, Style, SubplotError,
+ codegen, load_document, resource, Binding, Bindings, Document, EmbeddedFile, MarkupOpts, Style,
+ SubplotError, Warnings,
};
use time::{format_description::FormatItem, macros::format_description, OffsetDateTime};
use clap::{CommandFactory, FromArgMatches, Parser};
use std::convert::TryFrom;
-use std::ffi::OsString;
-use std::fs::{self, write, File};
-use std::io::{Read, Write};
+use std::fs::{self, write};
+use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{self, Command};
use std::time::UNIX_EPOCH;
@@ -53,34 +53,35 @@ impl Toplevel {
#[derive(Debug, Parser)]
enum Cmd {
Extract(Extract),
- Filter(Filter),
Metadata(Metadata),
Docgen(Docgen),
Codegen(Codegen),
#[clap(hide = true)]
Resources(Resources),
+ #[clap(hide = true)]
+ Libdocgen(Libdocgen),
}
impl Cmd {
fn run(&self) -> Result<()> {
match self {
Cmd::Extract(e) => e.run(),
- Cmd::Filter(f) => f.run(),
Cmd::Metadata(m) => m.run(),
Cmd::Docgen(d) => d.run(),
Cmd::Codegen(c) => c.run(),
Cmd::Resources(r) => r.run(),
+ Cmd::Libdocgen(r) => r.run(),
}
}
fn doc_path(&self) -> Option<&Path> {
match self {
Cmd::Extract(e) => e.doc_path(),
- Cmd::Filter(f) => f.doc_path(),
Cmd::Metadata(m) => m.doc_path(),
Cmd::Docgen(d) => d.doc_path(),
Cmd::Codegen(c) => c.doc_path(),
Cmd::Resources(r) => r.doc_path(),
+ Cmd::Libdocgen(r) => r.doc_path(),
}
}
}
@@ -91,7 +92,7 @@ fn long_version() -> Result<String> {
writeln!(ret, "{}", render_testament!(VERSION))?;
writeln!(ret, "Crate version: {}", env!("CARGO_PKG_VERSION"))?;
if let Some(branch) = VERSION.branch_name {
- writeln!(ret, "Built from branch: {}", branch)?;
+ writeln!(ret, "Built from branch: {branch}")?;
} else {
writeln!(ret, "Branch information is missing.")?;
}
@@ -139,13 +140,7 @@ struct Extract {
merciful: bool,
/// Directory to write extracted files to
- #[clap(
- name = "DIR",
- long = "directory",
- short = 'd',
- parse(from_os_str),
- default_value = "."
- )]
+ #[clap(name = "DIR", long = "directory", short = 'd', default_value = ".")]
directory: PathBuf,
/// Don't actually write the files out
@@ -153,7 +148,6 @@ struct Extract {
dry_run: bool,
/// Input subplot document filename
- #[clap(parse(from_os_str))]
filename: PathBuf,
/// Names of embedded files to be extracted.
@@ -168,8 +162,8 @@ impl Extract {
fn run(&self) -> Result<()> {
let doc = load_linted_doc(&self.filename, Style::default(), None, self.merciful)?;
- let files: Vec<&DataFile> = if self.embedded.is_empty() {
- doc.files()
+ let files: Vec<&EmbeddedFile> = if self.embedded.is_empty() {
+ doc.embedded_files()
.iter()
.map(Result::Ok)
.collect::<Result<Vec<_>>>()
@@ -194,59 +188,6 @@ impl Extract {
}
#[derive(Debug, Parser)]
-/// Filter a pandoc JSON document.
-///
-/// This filters a pandoc JSON document, applying Subplot's formatting rules and
-/// image conversion support.
-///
-/// If input/output filename is provided, this operates on STDIN/STDOUT.
-struct Filter {
- #[clap(name = "INPUT", long = "input", short = 'i', parse(from_os_str))]
- /// Input file (uses STDIN if omitted)
- input: Option<PathBuf>,
-
- #[clap(name = "OUTPUT", long = "output", short = 'o', parse(from_os_str))]
- /// Output file (uses STDOUT if omitted)
- output: Option<PathBuf>,
-
- #[clap(name = "BASE", long = "base", short = 'b', parse(from_os_str))]
- /// Base directory (defaults to dir of input if given, or '.' if using STDIN)
- base: Option<PathBuf>,
-}
-
-impl Filter {
- fn doc_path(&self) -> Option<&Path> {
- self.input.as_deref().and_then(Path::parent)
- }
-
- fn run(&self) -> Result<()> {
- let mut buffer = String::new();
- if let Some(filename) = &self.input {
- File::open(filename)?.read_to_string(&mut buffer)?;
- } else {
- std::io::stdin().read_to_string(&mut buffer)?;
- }
- let basedir = if let Some(path) = &self.base {
- path.as_path()
- } else if let Some(path) = &self.input {
- path.parent().unwrap_or_else(|| Path::new("."))
- } else {
- Path::new(".")
- };
- let style = Style::default();
- let mut doc = Document::from_json(basedir, vec![], &buffer, style, None)?;
- doc.typeset();
- let bytes = doc.ast()?.into_bytes();
- if let Some(filename) = &self.output {
- File::create(filename)?.write_all(&bytes)?;
- } else {
- std::io::stdout().write_all(&bytes)?;
- }
- Ok(())
- }
-}
-
-#[derive(Debug, Parser)]
/// Extract metadata about a document
///
/// Load and process a subplot document, extracting various metadata about the
@@ -258,11 +199,10 @@ struct Metadata {
merciful: bool,
/// Form that you want the output to take
- #[clap(short = 'o', default_value = "plain", possible_values=&["plain", "json"])]
+ #[clap(short = 'o', default_value = "plain")]
output_format: cli::OutputFormat,
/// Input subplot document filename
- #[clap(parse(from_os_str))]
filename: PathBuf,
}
@@ -285,7 +225,7 @@ impl Metadata {
#[derive(Debug, Parser)]
/// Typeset subplot document
///
-/// Process a subplot document and typeset it using Pandoc.
+/// Process a subplot document and typeset it.
struct Docgen {
/// Allow warnings in document?
#[clap(long)]
@@ -298,11 +238,10 @@ struct Docgen {
template: Option<String>,
// Input Subplot document
- #[clap(parse(from_os_str))]
input: PathBuf,
// Output document filename
- #[clap(name = "FILE", long = "output", short = 'o', parse(from_os_str))]
+ #[clap(name = "FILE", long = "output", short = 'o')]
output: PathBuf,
// Set date.
@@ -316,14 +255,9 @@ impl Docgen {
}
fn run(&self) -> Result<()> {
- let mut style = Style::default();
- if self.output.extension() == Some(&OsString::from("pdf")) {
- trace!("PDF output chosen");
- style.typeset_links_as_notes();
- }
+ let style = Style::default();
let mut doc = load_linted_doc(&self.input, style, self.template.as_deref(), self.merciful)?;
- let mut pandoc = pandoc::new();
// Metadata date from command line or file mtime. However, we
// can't set it directly, since we don't want to override the date
// in the actual document, if given, so we only set
@@ -334,37 +268,38 @@ impl Docgen {
} else if let Some(date) = doc.meta().date() {
date.to_string()
} else {
- Self::mtime_formatted(Self::mtime(&self.input)?)
- };
- pandoc.add_option(pandoc::PandocOption::Meta("date".to_string(), Some(date)));
- pandoc.add_option(pandoc::PandocOption::TableOfContents);
- pandoc.add_option(pandoc::PandocOption::Standalone);
- pandoc.add_option(pandoc::PandocOption::NumberSections);
-
- if Self::need_output(&mut doc, self.template.as_deref(), &self.output) {
- doc.typeset();
- pandoc.set_input_format(pandoc::InputFormat::Json, vec![]);
- pandoc.set_input(pandoc::InputKind::Pipe(doc.ast()?));
- pandoc.set_output(pandoc::OutputKind::File(self.output.clone()));
-
- debug!("Executing pandoc to produce {}", self.output.display());
- let r = pandoc.execute();
- if let Err(pandoc::PandocError::Err(output)) = r {
- let code = output.status.code().or(Some(127)).unwrap();
- let stderr = String::from_utf8_lossy(&output.stderr);
- error!("Failed to execute Pandoc: exit code {}", code);
- error!("{}", stderr.strip_suffix('\n').unwrap());
-
- return Err(anyhow::Error::msg("Pandoc failed"));
+ let mut newest = None;
+ let basedir = if let Some(basedir) = self.input.parent() {
+ basedir.to_path_buf()
+ } else {
+ return Err(SubplotError::BasedirError(self.input.clone()).into());
+ };
+ for filename in doc.meta().markdown_filenames() {
+ let filename = basedir.join(filename);
+ let mtime = Self::mtime(&filename)?;
+ if let Some(so_far) = newest {
+ if mtime > so_far {
+ newest = Some(mtime);
+ }
+ } else {
+ newest = Some(mtime);
+ }
}
- r?;
- }
+ Self::mtime_formatted(newest.unwrap())
+ };
+
+ doc.typeset(&mut Warnings::default(), self.template.as_deref())?;
+ std::fs::write(&self.output, doc.to_html(&date)?)
+ .map_err(|e| SubplotError::WriteFile(self.output.clone(), e))?;
Ok(())
}
fn mtime(filename: &Path) -> Result<(u64, u32)> {
- let mtime = fs::metadata(filename)?.modified()?;
+ let mtime = fs::metadata(filename)
+ .map_err(|e| SubplotError::InputFileUnreadable(filename.into(), e))?
+ .modified()
+ .map_err(|e| SubplotError::InputFileMtime(filename.into(), e))?;
let mtime = mtime.duration_since(UNIX_EPOCH)?;
Ok((mtime.as_secs(), mtime.subsec_nanos()))
}
@@ -377,24 +312,6 @@ impl Docgen {
let time = OffsetDateTime::from_unix_timestamp(secs).unwrap();
time.format(DATE_FORMAT).unwrap()
}
-
- fn need_output(doc: &mut subplot::Document, template: Option<&str>, output: &Path) -> bool {
- let output = match Self::mtime(output) {
- Err(_) => return true,
- Ok(ts) => ts,
- };
-
- for filename in doc.sources(template) {
- let source = match Self::mtime(&filename) {
- Err(_) => return true,
- Ok(ts) => ts,
- };
- if source >= output {
- return true;
- }
- }
- false
- }
}
#[derive(Debug, Parser)]
@@ -410,16 +327,10 @@ struct Codegen {
template: Option<String>,
/// Input filename.
- #[clap(parse(from_os_str))]
filename: PathBuf,
/// Write generated test program to this file.
- #[clap(
- long,
- short,
- parse(from_os_str),
- help = "Writes generated test program to FILE"
- )]
+ #[clap(long, short, help = "Writes generated test program to FILE")]
output: PathBuf,
/// Run the generated test program after writing it?
@@ -464,13 +375,105 @@ impl Codegen {
}
}
+#[derive(Debug, Parser)]
+/// Generate test suites from Subplot documents
+///
+/// This reads a subplot document, extracts the scenarios, and writes out a test
+/// program capable of running the scenarios in the subplot document.
+struct Libdocgen {
+ // Bindings file to read.
+ input: PathBuf,
+
+ // Output document filename
+ #[clap(name = "FILE", long = "output", short = 'o')]
+ output: PathBuf,
+
+ /// The template to use from the document.
+ ///
+ /// If not specified, subplot will try and find a unique template name from the document
+ #[clap(name = "TEMPLATE", long = "template", short = 't')]
+ template: Option<String>,
+
+ /// Be merciful by allowing bindings to not have documentation.
+ #[clap(long)]
+ merciful: bool,
+}
+
+impl Libdocgen {
+ fn doc_path(&self) -> Option<&Path> {
+ None
+ }
+
+ fn run(&self) -> Result<()> {
+ debug!("libdocgen starts");
+
+ let mut bindings = Bindings::new();
+ bindings.add_from_file(&self.input, None)?;
+ // println!("{:#?}", bindings);
+
+ let mut doc = LibDoc::new(&self.input);
+ for b in bindings.bindings() {
+ // println!("{} {}", b.kind(), b.pattern());
+ doc.push_binding(b);
+ }
+
+ std::fs::write(&self.output, doc.to_markdown(self.merciful)?)?;
+
+ debug!("libdogen ends successfully");
+ Ok(())
+ }
+}
+
+struct LibDoc {
+ filename: PathBuf,
+ bindings: Vec<Binding>,
+}
+
+impl LibDoc {
+ fn new(filename: &Path) -> Self {
+ Self {
+ filename: filename.into(),
+ bindings: vec![],
+ }
+ }
+
+ fn push_binding(&mut self, binding: &Binding) {
+ self.bindings.push(binding.clone());
+ }
+
+ fn to_markdown(&self, merciful: bool) -> Result<String> {
+ let mut md = String::new();
+ md.push_str(&format!("# Library `{}`\n\n", self.filename.display()));
+ for b in self.bindings.iter() {
+ md.push_str(&format!("\n## {} `{}`\n", b.kind(), b.pattern()));
+ if let Some(doc) = b.doc() {
+ md.push_str(&format!("\n{}\n", doc));
+ } else if !merciful {
+ return Err(SubplotError::NoBindingDoc(
+ self.filename.clone(),
+ b.kind(),
+ b.pattern().into(),
+ )
+ .into());
+ }
+ if b.types().count() > 0 {
+ md.push_str("\nCaptures:\n\n");
+ for (name, cap_type) in b.types() {
+ md.push_str(&format!("- `{}`: {}\n", name, cap_type.as_str()));
+ }
+ }
+ }
+ Ok(md)
+ }
+}
+
fn load_linted_doc(
filename: &Path,
style: Style,
template: Option<&str>,
merciful: bool,
) -> Result<Document, SubplotError> {
- let mut doc = load_document(filename, style, None)?;
+ let doc = load_document(filename, style, None)?;
trace!("Got doc, now linting it");
doc.lint()?;
trace!("Doc linted ok");
@@ -489,33 +492,34 @@ fn load_linted_doc(
};
let template = template.to_string();
trace!("Template: {:#?}", template);
- doc.check_named_files_exist(&template)?;
- doc.check_matched_steps_have_impl(&template);
- doc.check_embedded_files_are_used(&template)?;
+ let mut warnings = Warnings::default();
+ doc.check_bindings(&mut warnings)?;
+ doc.check_named_code_blocks_have_appropriate_class(&mut warnings)?;
+ doc.check_named_files_exist(&template, &mut warnings)?;
+ if !template.is_empty() {
+ // We have a template, let's check we have implementations
+ doc.check_matched_steps_have_impl(&template, &mut warnings);
+ } else {
+ trace!("No template found, so cannot check impl presence");
+ }
+ doc.check_embedded_files_are_used(&template, &mut warnings)?;
- for w in doc.warnings() {
+ for w in warnings.warnings() {
warn!("{}", w);
}
- if !doc.warnings().is_empty() && !merciful {
- return Err(SubplotError::Warnings(doc.warnings().len()));
+ if !warnings.is_empty() && !merciful {
+ return Err(SubplotError::Warnings(warnings.len()));
}
Ok(doc)
}
-fn print_source_errors(e: Option<&dyn std::error::Error>) {
- if let Some(e) = e {
- error!("{}", e);
- print_source_errors(e.source());
- }
-}
-
fn real_main() {
info!("Starting Subplot");
let argparser = Toplevel::command();
let version = long_version().unwrap();
- let argparser = argparser.long_version(version.as_str());
+ let argparser = argparser.long_version(version);
let args = argparser.get_matches();
let args = Toplevel::from_arg_matches(&args).unwrap();
args.handle_special_args();
@@ -525,7 +529,11 @@ fn real_main() {
}
Err(e) => {
error!("{}", e);
- print_source_errors(e.source());
+ let mut e = e.source();
+ while let Some(source) = e {
+ error!("caused by: {}", source);
+ e = source.source();
+ }
process::exit(1);
}
}