diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bin/cli/mod.rs | 133 | ||||
-rw-r--r-- | src/bin/sp-codegen.rs | 10 | ||||
-rw-r--r-- | src/bin/sp-docgen.rs | 7 | ||||
-rw-r--r-- | src/bin/sp-extract.rs | 19 | ||||
-rw-r--r-- | src/bin/sp-meta.rs | 115 | ||||
-rw-r--r-- | src/bin/subplot.rs | 349 | ||||
-rw-r--r-- | src/bindings.rs | 4 | ||||
-rw-r--r-- | src/codegen.rs | 4 | ||||
-rw-r--r-- | src/doc.rs | 5 | ||||
-rw-r--r-- | src/resource.rs | 9 | ||||
-rw-r--r-- | src/typeset.rs | 5 |
11 files changed, 512 insertions, 148 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); + } + } +} diff --git a/src/bindings.rs b/src/bindings.rs index fe26690..99fd6cf 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -557,14 +557,14 @@ fn from_hashmap(parsed: &ParsedBinding) -> Result<Binding> { regex_from_simple_pattern(pattern, parsed.regex.is_some(), &mut types)? }; - Ok(Binding::new( + Binding::new( kind, &pattern, &parsed.function, parsed.cleanup.as_deref(), parsed.case_sensitive, types, - )?) + ) } #[cfg(test)] diff --git a/src/codegen.rs b/src/codegen.rs index 91f3358..588fe1b 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -21,8 +21,8 @@ pub fn template_spec(doc: &Document) -> Result<TemplateSpec> { let mut filename = PathBuf::from(template); filename.push("template"); filename.push("template.yaml"); - Ok(TemplateSpec::from_file(&filename) - .with_context(|| format!("Failed to read template file: {}", filename.display()))?) + TemplateSpec::from_file(&filename) + .with_context(|| format!("Failed to read template file: {}", filename.display())) } /// Generate a test program from a document, using a template spec. @@ -317,11 +317,10 @@ impl<'a> Document { pub fn matched_scenarios(&mut self) -> Result<Vec<MatchedScenario>> { let scenarios = self.scenarios()?; let bindings = self.meta().bindings(); - let vec: Result<Vec<MatchedScenario>> = scenarios + scenarios .iter() .map(|scen| MatchedScenario::new(scen, bindings)) - .collect(); - Ok(vec?) + .collect() } } diff --git a/src/resource.rs b/src/resource.rs index 6f2b33b..a03086c 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -10,11 +10,12 @@ use std::path::{Path, PathBuf}; use std::sync::Mutex; use structopt::StructOpt; +#[allow(missing_docs)] #[derive(Debug, StructOpt)] -/// Options which relate to resource management -/// -/// To use this, include them *flat* in your options struct, and then after -/// parsing, call the [ResourceOpts::handle()] function. +// Options which relate to resource management +// +// To use this, include them *flat* in your options struct, and then after +// parsing, call the [ResourceOpts::handle()] function. pub struct ResourceOpts { #[structopt( long, diff --git a/src/typeset.rs b/src/typeset.rs index a30df6f..ca257a9 100644 --- a/src/typeset.rs +++ b/src/typeset.rs @@ -92,10 +92,7 @@ fn step( } }; - let mut inlines = Vec::new(); - - inlines.push(keyword(&step)); - inlines.push(space()); + let mut inlines = vec![keyword(&step), space()]; for part in m.parts() { match part { |