diff options
Diffstat (limited to 'src/bin/subplot.rs')
-rw-r--r-- | src/bin/subplot.rs | 304 |
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); } } |