diff options
Diffstat (limited to 'src/bin/subplot.rs')
-rw-r--r-- | src/bin/subplot.rs | 349 |
1 files changed, 349 insertions, 0 deletions
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); + } + } +} |