summaryrefslogtreecommitdiff
path: root/src/bin
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/cli/mod.rs133
-rw-r--r--src/bin/sp-codegen.rs10
-rw-r--r--src/bin/sp-docgen.rs7
-rw-r--r--src/bin/sp-extract.rs19
-rw-r--r--src/bin/sp-meta.rs115
-rw-r--r--src/bin/subplot.rs349
6 files changed, 500 insertions, 133 deletions
diff --git a/src/bin/cli/mod.rs b/src/bin/cli/mod.rs
new file mode 100644
index 0000000..f3225db
--- /dev/null
+++ b/src/bin/cli/mod.rs
@@ -0,0 +1,133 @@
+//! CLI Functionality abstractions
+
+#![allow(unused)]
+
+use anyhow::Result;
+use serde::Serialize;
+use subplot::{DataFile, Document, Style, SubplotError};
+
+use std::convert::TryFrom;
+use std::path::Path;
+use std::str::FromStr;
+
+pub fn load_document<P: AsRef<Path>>(filename: P, style: Style) -> Result<Document> {
+ let filename = filename.as_ref();
+ let base_path = subplot::get_basedir_from(filename)?;
+ let doc = Document::from_file(&base_path, filename, style)?;
+
+ Ok(doc)
+}
+
+pub fn extract_file<'a>(doc: &'a Document, filename: &str) -> Result<&'a DataFile> {
+ for file in doc.files() {
+ if file.filename() == filename {
+ return Ok(file);
+ }
+ }
+ Err(SubplotError::EmbeddedFileNotFound(filename.to_owned()).into())
+}
+
+#[derive(Serialize)]
+pub struct Metadata {
+ sources: Vec<String>,
+ title: String,
+ binding_files: Vec<String>,
+ function_files: Vec<String>,
+ bibliographies: Vec<String>,
+ scenarios: Vec<String>,
+ files: Vec<String>,
+}
+
+impl TryFrom<&mut Document> for Metadata {
+ type Error = subplot::SubplotError;
+ fn try_from(doc: &mut Document) -> std::result::Result<Self, Self::Error> {
+ let sources: Vec<_> = doc
+ .sources()
+ .into_iter()
+ .map(|p| filename(Some(&p)))
+ .collect();
+ let title = doc.meta().title().to_owned();
+ let binding_files = doc
+ .meta()
+ .bindings_filenames()
+ .into_iter()
+ .map(|p| filename(Some(&p)))
+ .collect();
+ let function_files = doc
+ .meta()
+ .functions_filenames()
+ .into_iter()
+ .map(|p| filename(Some(&p)))
+ .collect();
+ let bibliographies = doc
+ .meta()
+ .bibliographies()
+ .into_iter()
+ .map(|p| filename(Some(&p)))
+ .collect();
+ let scenarios = doc
+ .scenarios()?
+ .into_iter()
+ .map(|s| s.title().to_owned())
+ .collect();
+ let files = doc
+ .files()
+ .iter()
+ .map(|f| f.filename().to_owned())
+ .collect();
+ Ok(Self {
+ sources,
+ title,
+ binding_files,
+ function_files,
+ bibliographies,
+ scenarios,
+ files,
+ })
+ }
+}
+
+impl Metadata {
+ fn write_list(v: &[String], prefix: &str) {
+ v.iter().for_each(|entry| println!("{}: {}", prefix, entry))
+ }
+
+ pub fn write_out(&self) {
+ Self::write_list(&self.sources, "source");
+ println!("title: {}", self.title);
+ Self::write_list(&self.binding_files, "bindings");
+ Self::write_list(&self.function_files, "functions");
+ Self::write_list(&self.bibliographies, "bibliography");
+ Self::write_list(&self.files, "file");
+ Self::write_list(&self.scenarios, "scenario");
+ }
+}
+
+fn filename(name: Option<&Path>) -> String {
+ let path = match name {
+ None => return "".to_string(),
+ Some(x) => x,
+ };
+ match path.to_str() {
+ None => "non-UTF8 filename".to_string(),
+ Some(x) => x.to_string(),
+ }
+}
+
+#[derive(Debug)]
+pub enum OutputFormat {
+ Plain,
+ Json,
+}
+
+impl FromStr for OutputFormat {
+ type Err = String;
+
+ fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
+ match s.to_ascii_lowercase().as_ref() {
+ "plain" => Ok(OutputFormat::Plain),
+ "json" => Ok(OutputFormat::Json),
+ _ => Err(format!("Unknown output format: `{}`", s)),
+ }
+ }
+}
diff --git a/src/bin/sp-codegen.rs b/src/bin/sp-codegen.rs
index f83cf59..c316bea 100644
--- a/src/bin/sp-codegen.rs
+++ b/src/bin/sp-codegen.rs
@@ -4,16 +4,14 @@ use std::process::Command;
use anyhow::Result;
use structopt::StructOpt;
-use subplot::{
- generate_test_program, resource, template_spec, Document, Style, SubplotError, TemplateSpec,
-};
+use subplot::{generate_test_program, resource, template_spec, Style, SubplotError, TemplateSpec};
+
+mod cli;
fn main() -> Result<()> {
let opt = Opt::from_args();
opt.resources.handle();
- let basedir = subplot::get_basedir_from(&opt.filename)?;
- let style = Style::default();
- let mut doc = Document::from_file(&basedir, &opt.filename, style)?;
+ let mut doc = cli::load_document(&opt.filename, Style::default())?;
doc.lint()?;
if !doc.check_named_files_exist()? {
eprintln!("Unable to continue");
diff --git a/src/bin/sp-docgen.rs b/src/bin/sp-docgen.rs
index 61522ce..d633791 100644
--- a/src/bin/sp-docgen.rs
+++ b/src/bin/sp-docgen.rs
@@ -8,7 +8,9 @@ use structopt::StructOpt;
use anyhow::Result;
-use subplot::{get_basedir_from, resource, Document, Style};
+use subplot::{resource, Style};
+
+mod cli;
// Define the command line arguments.
#[derive(Debug, StructOpt)]
@@ -42,8 +44,7 @@ fn main() -> Result<()> {
style.typeset_links_as_notes();
}
- let basedir = get_basedir_from(first_file)?;
- let mut doc = Document::from_file(&basedir, &first_file, style)?;
+ let mut doc = cli::load_document(&first_file, style)?;
doc.lint()?;
if !doc.check_named_files_exist()? {
eprintln!("Continuing despite warnings");
diff --git a/src/bin/sp-extract.rs b/src/bin/sp-extract.rs
index cdd9fba..86bc15a 100644
--- a/src/bin/sp-extract.rs
+++ b/src/bin/sp-extract.rs
@@ -4,7 +4,9 @@ use std::path::PathBuf;
use structopt::StructOpt;
-use subplot::{DataFile, Document, Style, SubplotError};
+use subplot::Style;
+
+mod cli;
#[derive(Debug, StructOpt)]
#[structopt(name = "sp-meta", about = "Show Subplot document metadata.")]
@@ -24,24 +26,13 @@ struct Opt {
fn main() -> Result<()> {
let opt = Opt::from_args();
- let basedir = subplot::get_basedir_from(&opt.filename)?;
- let style = Style::default();
- let doc = Document::from_file(&basedir, &opt.filename, style)?;
+ let doc = cli::load_document(&opt.filename, Style::default())?;
for filename in opt.embedded {
- let file = get_embedded(&doc, &filename)?;
+ let file = cli::extract_file(&doc, &filename)?;
let output = opt.directory.join(filename);
write(output, file.contents())?;
}
Ok(())
}
-
-fn get_embedded<'a>(doc: &'a Document, filename: &str) -> Result<&'a DataFile> {
- for file in doc.files() {
- if file.filename() == filename {
- return Ok(file);
- }
- }
- Err(SubplotError::EmbeddedFileNotFound(filename.to_owned()).into())
-}
diff --git a/src/bin/sp-meta.rs b/src/bin/sp-meta.rs
index c633c59..b907999 100644
--- a/src/bin/sp-meta.rs
+++ b/src/bin/sp-meta.rs
@@ -1,30 +1,14 @@
use anyhow::Result;
use std::convert::TryFrom;
-use std::path::{Path, PathBuf};
-use std::str::FromStr;
+use std::path::PathBuf;
-use serde::Serialize;
use structopt::StructOpt;
-use subplot::{Document, Style};
+use subplot::Style;
-#[derive(Debug)]
-enum OutputFormat {
- Plain,
- Json,
-}
+mod cli;
-impl FromStr for OutputFormat {
- type Err = String;
-
- fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
- match s.to_ascii_lowercase().as_ref() {
- "plain" => Ok(OutputFormat::Plain),
- "json" => Ok(OutputFormat::Json),
- _ => Err(format!("Unknown output format: `{}`", s)),
- }
- }
-}
+use cli::{Metadata, OutputFormat};
#[derive(Debug, StructOpt)]
#[structopt(name = "sp-meta", about = "Show Subplot document metadata.")]
@@ -37,87 +21,9 @@ struct Opt {
filename: PathBuf,
}
-#[derive(Serialize)]
-struct Metadata {
- sources: Vec<String>,
- title: String,
- binding_files: Vec<String>,
- function_files: Vec<String>,
- bibliographies: Vec<String>,
- scenarios: Vec<String>,
- files: Vec<String>,
-}
-
-impl TryFrom<&mut Document> for Metadata {
- type Error = subplot::SubplotError;
- fn try_from(doc: &mut Document) -> std::result::Result<Self, Self::Error> {
- let sources: Vec<_> = doc
- .sources()
- .into_iter()
- .map(|p| filename(Some(&p)))
- .collect();
- let title = doc.meta().title().to_owned();
- let binding_files = doc
- .meta()
- .bindings_filenames()
- .into_iter()
- .map(|p| filename(Some(&p)))
- .collect();
- let function_files = doc
- .meta()
- .functions_filenames()
- .into_iter()
- .map(|p| filename(Some(&p)))
- .collect();
- let bibliographies = doc
- .meta()
- .bibliographies()
- .into_iter()
- .map(|p| filename(Some(&p)))
- .collect();
- let scenarios = doc
- .scenarios()?
- .into_iter()
- .map(|s| s.title().to_owned())
- .collect();
- let files = doc
- .files()
- .iter()
- .map(|f| f.filename().to_owned())
- .collect();
- Ok(Self {
- sources,
- title,
- binding_files,
- function_files,
- bibliographies,
- scenarios,
- files,
- })
- }
-}
-
-impl Metadata {
- fn write_list(v: &[String], prefix: &str) {
- v.iter().for_each(|entry| println!("{}: {}", prefix, entry))
- }
-
- fn write_out(&self) {
- Self::write_list(&self.sources, "source");
- println!("title: {}", self.title);
- Self::write_list(&self.binding_files, "bindings");
- Self::write_list(&self.function_files, "functions");
- Self::write_list(&self.bibliographies, "bibliography");
- Self::write_list(&self.files, "file");
- Self::write_list(&self.scenarios, "scenario");
- }
-}
-
fn main() -> Result<()> {
let opt = Opt::from_args();
- let basedir = subplot::get_basedir_from(&opt.filename)?;
- let style = Style::default();
- let mut doc = Document::from_file(&basedir, &opt.filename, style)?;
+ let mut doc = cli::load_document(&opt.filename, Style::default())?;
let meta = Metadata::try_from(&mut doc)?;
match opt.output_format {
@@ -127,14 +33,3 @@ fn main() -> Result<()> {
Ok(())
}
-
-fn filename(name: Option<&Path>) -> String {
- let path = match name {
- None => return "".to_string(),
- Some(x) => x,
- };
- match path.to_str() {
- None => "non-UTF8 filename".to_string(),
- Some(x) => x.to_string(),
- }
-}
diff --git a/src/bin/subplot.rs b/src/bin/subplot.rs
new file mode 100644
index 0000000..ebbdec8
--- /dev/null
+++ b/src/bin/subplot.rs
@@ -0,0 +1,349 @@
+//! Subplot top level binary
+//!
+
+use anyhow::Result;
+
+use chrono::{Local, TimeZone};
+use structopt::StructOpt;
+use subplot::{generate_test_program, resource, template_spec, DataFile, Document, Style};
+
+use std::convert::TryFrom;
+use std::ffi::OsString;
+use std::fs::{self, write, File};
+use std::io::{Read, Write};
+use std::path::{Path, PathBuf};
+use std::process::{self, Command};
+use std::time::UNIX_EPOCH;
+
+mod cli;
+
+#[derive(Debug, StructOpt)]
+struct Toplevel {
+ #[structopt(flatten)]
+ resources: resource::ResourceOpts,
+
+ #[structopt(flatten)]
+ command: Cmd,
+}
+
+impl Toplevel {
+ fn run(&self) -> Result<()> {
+ self.command.run()
+ }
+}
+
+#[derive(Debug, StructOpt)]
+enum Cmd {
+ Extract(Extract),
+ Filter(Filter),
+ Metadata(Metadata),
+ Docgen(Docgen),
+ Codegen(Codegen),
+}
+
+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(),
+ }
+ }
+}
+
+#[derive(Debug, StructOpt)]
+/// Extract embedded files from a subplot document
+///
+/// If no embedded filenames are provided, this will
+/// extract all embedded files. if the output directory
+/// is not specified then this will extract to the current directory.
+struct Extract {
+ /// Directory to write extracted files to
+ #[structopt(
+ name = "DIR",
+ long = "directory",
+ short = "d",
+ parse(from_os_str),
+ default_value = "."
+ )]
+ directory: PathBuf,
+
+ /// Don't actually write the files out
+ #[structopt(long)]
+ dry_run: bool,
+
+ /// Input subplot document filename
+ #[structopt(parse(from_os_str))]
+ filename: PathBuf,
+
+ /// Names of embedded files to be extracted.
+ embedded: Vec<String>,
+}
+
+impl Extract {
+ fn run(&self) -> Result<()> {
+ let doc = cli::load_document(&self.filename, Style::default())?;
+
+ let files: Vec<&DataFile> = if self.embedded.is_empty() {
+ doc.files()
+ .iter()
+ .map(Result::Ok)
+ .collect::<Result<Vec<_>>>()
+ } else {
+ self.embedded
+ .iter()
+ .map(|filename| cli::extract_file(&doc, filename))
+ .collect::<Result<Vec<_>>>()
+ }?;
+
+ for f in files {
+ let outfile = self.directory.join(f.filename());
+ if self.dry_run {
+ println!("Would write out {}", outfile.display());
+ } else {
+ write(outfile, f.contents())?
+ }
+ }
+
+ Ok(())
+ }
+}
+
+#[derive(Debug, StructOpt)]
+/// 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 {
+ #[structopt(name = "INPUT", long = "input", short = "i", parse(from_os_str))]
+ /// Input file (uses STDIN if omitted)
+ input: Option<PathBuf>,
+
+ #[structopt(name = "OUTPUT", long = "output", short = "o", parse(from_os_str))]
+ /// Output file (uses STDOUT if omitted)
+ output: Option<PathBuf>,
+
+ #[structopt(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 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)?;
+ 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, StructOpt)]
+/// Extract metadata about a document
+///
+/// Load and process a subplot document, extracting various metadata about the
+/// document. This can then render that either as a plain text report for humans,
+/// or as a JSON object for further scripted processing.
+struct Metadata {
+ /// Form that you want the output to take
+ #[structopt(short = "o", default_value = "plain", possible_values=&["plain", "json"])]
+ output_format: cli::OutputFormat,
+
+ /// Input subplot document filename
+ #[structopt(parse(from_os_str))]
+ filename: PathBuf,
+}
+
+impl Metadata {
+ fn run(&self) -> Result<()> {
+ let mut doc = cli::load_document(&self.filename, Style::default())?;
+ let meta = cli::Metadata::try_from(&mut doc)?;
+ match self.output_format {
+ cli::OutputFormat::Plain => meta.write_out(),
+ cli::OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&meta)?),
+ }
+ Ok(())
+ }
+}
+
+#[derive(Debug, StructOpt)]
+/// Typeset subplot document
+///
+/// Process a subplot document and typeset it using Pandoc.
+struct Docgen {
+ // Input Subplot document
+ #[structopt(parse(from_os_str))]
+ input: PathBuf,
+
+ // Output document filename
+ #[structopt(name = "FILE", long = "--output", short = "-o", parse(from_os_str))]
+ output: PathBuf,
+
+ // Set date.
+ #[structopt(name = "DATE", long = "--date")]
+ date: Option<String>,
+}
+
+impl Docgen {
+ fn run(&self) -> Result<()> {
+ let mut style = Style::default();
+ if self.output.extension() == Some(&OsString::from("pdf")) {
+ style.typeset_links_as_notes();
+ }
+ let mut doc = cli::load_document(&self.input, style)?;
+ doc.lint()?;
+ if !doc.check_named_files_exist()? {
+ eprintln!("Continuing despite warnings");
+ }
+
+ 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
+ // user-provided-date. Our parsing code will use that if date is
+ // not document metadata.
+ let date = if let Some(date) = self.date.clone() {
+ date
+ } 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.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()));
+ pandoc.execute()?;
+ }
+
+ Ok(())
+ }
+
+ fn mtime(filename: &Path) -> Result<(u64, u32)> {
+ let mtime = fs::metadata(filename)?.modified()?;
+ let mtime = mtime.duration_since(UNIX_EPOCH)?;
+ Ok((mtime.as_secs(), mtime.subsec_nanos()))
+ }
+
+ fn mtime_formatted(mtime: (u64, u32)) -> String {
+ let secs: i64 = format!("{}", mtime.0).parse().unwrap_or(0);
+ let dt = Local.timestamp(secs, mtime.1);
+ dt.format("%Y-%m-%d %H:%M").to_string()
+ }
+
+ fn need_output(doc: &mut subplot::Document, output: &Path) -> bool {
+ let output = match Self::mtime(output) {
+ Err(_) => return true,
+ Ok(ts) => ts,
+ };
+
+ for filename in doc.sources() {
+ let source = match Self::mtime(&filename) {
+ Err(_) => return true,
+ Ok(ts) => ts,
+ };
+ if source >= output {
+ return true;
+ }
+ }
+ false
+ }
+}
+
+#[derive(Debug, StructOpt)]
+/// 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 Codegen {
+ /// Input filename.
+ #[structopt(parse(from_os_str))]
+ filename: PathBuf,
+
+ /// Write generated test program to this file.
+ #[structopt(
+ long,
+ short,
+ parse(from_os_str),
+ help = "Writes generated test program to FILE"
+ )]
+ output: PathBuf,
+
+ /// Run the generated test program after writing it?
+ #[structopt(long, short, help = "Runs generated test program")]
+ run: bool,
+}
+
+impl Codegen {
+ fn run(&self) -> Result<()> {
+ let mut doc = cli::load_document(&self.filename, Style::default())?;
+ doc.lint()?;
+ if !doc.check_named_files_exist()? {
+ eprintln!("Unable to continue");
+ std::process::exit(1);
+ }
+
+ let spec = template_spec(&doc)?;
+ generate_test_program(&mut doc, &spec, &self.output)?;
+
+ if self.run {
+ let run = match spec.run() {
+ None => {
+ eprintln!(
+ "Template {} does not specify how to run suites",
+ spec.template_filename().display()
+ );
+ std::process::exit(1);
+ }
+ Some(x) => x,
+ };
+
+ let status = Command::new(run).arg(&self.output).status()?;
+ if !status.success() {
+ eprintln!("Test suite failed!");
+ std::process::exit(2);
+ }
+ }
+ Ok(())
+ }
+}
+
+fn main() {
+ let args = Toplevel::from_args();
+ args.resources.handle();
+ match args.run() {
+ Ok(_) => {}
+ Err(e) => {
+ eprintln!("Failure: {:?}", e);
+ process::exit(1);
+ }
+ }
+}