use crate::ast; use crate::bindings::CaptureType; use crate::generate_test_program; use crate::get_basedir_from; use crate::md::Markdown; use crate::EmbeddedFile; use crate::EmbeddedFiles; use crate::MatchedScenario; use crate::Metadata; use crate::PartialStep; use crate::Scenario; use crate::Style; use crate::SubplotError; use crate::YamlMetadata; use crate::{Warning, Warnings}; use std::collections::HashSet; use std::convert::TryFrom; use std::default::Default; use std::fmt::Debug; use std::fs::read; use std::ops::Deref; use std::path::{Path, PathBuf}; use log::{error, trace}; /// The set of known (special) classes which subplot will always recognise /// as being valid. static SPECIAL_CLASSES: &[&str] = &[ "scenario", "file", "example", "dot", "pikchr", "plantuml", "roadmap", ]; /// The set of known (file-type) classes which subplot will always recognise /// as being valid. static KNOWN_FILE_CLASSES: &[&str] = &["rust", "yaml", "python", "sh", "shell", "markdown", "bash"]; /// The set of known (special-to-pandoc) classes which subplot will always recognise /// as being valid. We include the subplot-specific noNumberLines class which we use /// to invert the default numberLines on .file blocks. static KNOWN_PANDOC_CLASSES: &[&str] = &["numberLines", "noNumberLines"]; /// A parsed Subplot document. /// /// # Example /// /// fix this example; /// ~~~~ignored /// let markdown = "\ /// --- /// title: Test Title /// ... /// This is a test document. /// "; /// /// use std::io::Write; /// use tempfile::NamedTempFile; /// let mut f = NamedTempFile::new().unwrap(); /// f.write_all(markdown.as_bytes()).unwrap(); /// let filename = f.path(); /// /// use subplot; /// let basedir = std::path::Path::new("."); /// let style = subplot::Style::default(); /// let doc = subplot::Document::from_file(&basedir, filename, style, None).unwrap(); /// assert_eq!(doc.files(), &[]); /// ~~~~ #[derive(Debug)] pub struct Document { subplot: PathBuf, markdowns: Vec, md: Markdown, meta: Metadata, files: EmbeddedFiles, style: Style, warnings: Warnings, } impl Document { fn new( subplot: PathBuf, markdowns: Vec, md: Markdown, meta: Metadata, files: EmbeddedFiles, style: Style, ) -> Document { let doc = Document { subplot, markdowns, md, meta, files, style, warnings: Warnings::default(), }; trace!("Document::new -> {:#?}", doc); doc } /// Return all warnings about this document. pub fn warnings(&self) -> &[Warning] { self.warnings.warnings() } fn from_ast

( basedir: P, subplot: PathBuf, markdowns: Vec, yamlmeta: &ast::YamlMetadata, mut md: Markdown, style: Style, template: Option<&str>, ) -> Result where P: AsRef + Debug, { let meta = Metadata::from_yaml_metadata(basedir, yamlmeta, template)?; trace!("metadata from YAML: {:#?}", meta); let mut issues = md.lint(); if !issues.is_empty() { // Currently we can't really return more than one error so return one return Err(issues.remove(0)); } let files = md.embedded_files(); let doc = Document::new(subplot, markdowns, md, meta, files, style); trace!("Loaded from JSON OK"); Ok(doc) } /// Construct a Document from a named file. /// /// The file can be in any format Pandoc understands. This runs /// Pandoc to parse the file into an AST, so it can be a little /// slow. pub fn from_file( basedir: &Path, filename: &Path, style: Style, template: Option<&str>, ) -> Result { trace!( "Document::from_file: basedir={} filename={}", basedir.display(), filename.display() ); let meta = load_metadata_from_yaml_file(filename)?; trace!("metadata from YAML file: {:#?}", meta); let mdfile = meta.markdown(); let mdfile = basedir.join(mdfile); let mut md = Markdown::try_from(mdfile.as_path())?; md.set_metadata(&meta); let markdowns = vec![mdfile]; let doc = Self::from_ast( basedir, filename.into(), markdowns, &meta, md, style, template, )?; trace!("Loaded document OK"); Ok(doc) } /// Return the AST of a Document, serialized as JSON. /// /// This is useful in a Pandoc filter, so that the filter can give /// it back to Pandoc for typesetting. pub fn ast(&mut self) -> Result { self.md.to_json() } /// Return the document's metadata. pub fn meta(&self) -> &Metadata { &self.meta } /// Return all source filenames for the document. /// /// The sources are any files that affect the output so that if /// the source file changes, the output needs to be re-generated. pub fn sources(&mut self, template: Option<&str>) -> Vec { let mut names = vec![self.subplot.clone()]; for x in self.meta().bindings_filenames() { names.push(PathBuf::from(x)) } if let Some(template) = template { if let Some(spec) = self.meta().document_impl(template) { for x in spec.functions_filenames() { names.push(PathBuf::from(x)); } } } else { for template in self.meta().templates() { if let Some(spec) = self.meta().document_impl(template) { for x in spec.functions_filenames() { names.push(PathBuf::from(x)); } } } } for x in self.meta().bibliographies().iter() { names.push(PathBuf::from(x)) } for x in self.markdowns.iter() { names.push(x.to_path_buf()); } let mut images = self.md.images(); names.append(&mut images); names } /// Return list of files embeddedin the document. pub fn files(&self) -> &[EmbeddedFile] { self.files.files() } /// Check the document for common problems. pub fn lint(&mut self) -> Result<(), SubplotError> { trace!("Linting document"); self.check_doc_has_title()?; self.check_filenames_are_unique()?; self.check_block_classes()?; trace!("No linting problems found"); Ok(()) } // Check that all filenames for embedded files are unique. fn check_filenames_are_unique(&self) -> Result<(), SubplotError> { let mut known = HashSet::new(); for filename in self.files().iter().map(|f| f.filename().to_lowercase()) { if known.contains(&filename) { return Err(SubplotError::DuplicateEmbeddedFilename(filename)); } known.insert(filename); } Ok(()) } // Check that document has a title in its metadata. fn check_doc_has_title(&self) -> Result<(), SubplotError> { if self.meta().title().is_empty() { Err(SubplotError::NoTitle) } else { Ok(()) } } /// Check that all the block classes in the document are known fn check_block_classes(&mut self) -> Result<(), SubplotError> { let classes_in_doc = self.md.block_classes(); // Build the set of known good classes let mut known_classes: HashSet = HashSet::new(); for class in std::iter::empty() .chain(SPECIAL_CLASSES.iter().map(Deref::deref)) .chain(KNOWN_FILE_CLASSES.iter().map(Deref::deref)) .chain(KNOWN_PANDOC_CLASSES.iter().map(Deref::deref)) .chain(self.meta().classes()) { known_classes.insert(class.to_string()); } // Acquire the set of used names which are not known let unknown_classes: Vec<_> = classes_in_doc.difference(&known_classes).cloned().collect(); // If the unknown classes list is not empty, we had a problem and // we will report it to the user. if !unknown_classes.is_empty() { Err(SubplotError::UnknownClasses(unknown_classes.join(", "))) } else { Ok(()) } } /// Check that all named files (in matched steps) are actually present in the /// document. pub fn check_named_files_exist(&mut self, template: &str) -> Result { let filenames: HashSet<_> = self .files() .iter() .map(|f| f.filename().to_lowercase()) .collect(); trace!("Checking that files exist"); let mut okay = true; let scenarios = match self.matched_scenarios(template) { Ok(scenarios) => scenarios, Err(_) => return Ok(true), // We can't do the check, so say it's okay. }; for scenario in scenarios { for step in scenario.steps() { for captured in step.parts() { if let PartialStep::CapturedText { name, text } = captured { if matches!(step.types().get(name.as_str()), Some(CaptureType::File)) && !filenames.contains(&text.to_lowercase()) { self.warnings.push(Warning::UnknownEmbeddedFile( scenario.title().to_string(), text.to_string(), )); okay = false; } } } } } Ok(okay) } /// Check that all embedded files are used by matched steps. pub fn check_embedded_files_are_used(&mut self, template: &str) -> Result { let mut filenames: HashSet<_> = self .files() .iter() .map(|f| f.filename().to_lowercase()) .collect(); trace!("Checking that files are used"); let scenarios = match self.matched_scenarios(template) { Ok(scenarios) => scenarios, Err(_) => return Ok(true), // We can't do the check, so say it's okay. }; for scenario in scenarios { for step in scenario.steps() { for captured in step.parts() { if let PartialStep::CapturedText { name, text } = captured { if matches!(step.types().get(name.as_str()), Some(CaptureType::File)) { filenames.remove(&text.to_lowercase()); } } } } } for filename in filenames.iter() { self.warnings .push(Warning::UnusedEmbeddedFile(filename.to_string())); } // We always succeed. Subplot's own subplot had valid cases of // an embedded file being used and we need to develop a way to // mark such uses as OK, before we can make it an error to not // use an embedded file in a scenario. Ok(true) } /// Check that all matched steps actually have function implementations pub fn check_matched_steps_have_impl(&mut self, template: &str) -> bool { trace!("Checking that steps have implementations"); let mut okay = true; let scenarios = match self.matched_scenarios(template) { Ok(scenarios) => scenarios, Err(_) => return true, // No matches means no missing impls }; trace!("Found {} scenarios", scenarios.len()); for scenario in scenarios { trace!("Checking that steps in scenario"); for step in scenario.steps() { if step.function().is_none() { trace!("Missing step implementation: {:?}", step.text()); self.warnings.push(Warning::MissingStepImplementation( scenario.title().to_string(), step.text().to_string(), )); okay = false; } } } okay } /// Typeset a Subplot document. pub fn typeset(&mut self) { let warnings = self.md.typeset(self.style.clone(), self.meta.bindings()); for w in warnings { self.warnings.push(w); } } /// Return all scenarios in a document. pub fn scenarios(&mut self) -> Result, SubplotError> { self.md.scenarios() } /// Return matched scenarios in a document. pub fn matched_scenarios( &mut self, template: &str, ) -> Result, SubplotError> { let scenarios = self.scenarios()?; trace!( "Found {} scenarios, checking their bindings", scenarios.len() ); let bindings = self.meta().bindings(); scenarios .iter() .map(|scen| MatchedScenario::new(template, scen, bindings)) .collect() } /// Extract a template name from this document pub fn template(&self) -> Result<&str, SubplotError> { let templates: Vec<_> = self.meta().templates().collect(); if templates.len() == 1 { Ok(templates[0]) } else if templates.is_empty() { Err(SubplotError::MissingTemplate) } else { Err(SubplotError::AmbiguousTemplate) } } } fn load_metadata_from_yaml_file(filename: &Path) -> Result { let yaml = read(filename).map_err(|e| SubplotError::ReadFile(filename.into(), e))?; trace!("Parsing YAML metadata from {}", filename.display()); let meta: ast::YamlMetadata = serde_yaml::from_slice(&yaml) .map_err(|e| SubplotError::MetadataFile(filename.into(), e))?; Ok(meta) } /// Load a `Document` from a file. /// /// This version uses Pandoc to parse the Markdown. pub fn load_document

( filename: P, style: Style, template: Option<&str>, ) -> Result where P: AsRef + Debug, { let filename = filename.as_ref(); let base_path = get_basedir_from(filename); trace!( "Loading document based at `{}` called `{}` with {:?}", base_path.display(), filename.display(), style ); let doc = Document::from_file(&base_path, filename, style, template)?; trace!("Loaded doc from file OK"); Ok(doc) } /// Load a `Document` from a file. /// /// This version uses the `cmark-pullmark` crate to parse Markdown. pub fn load_document_with_pullmark

( filename: P, style: Style, template: Option<&str>, ) -> Result where P: AsRef + Debug, { let filename = filename.as_ref(); let base_path = get_basedir_from(filename); trace!( "Loading document based at `{}` called `{}` with {:?} using pullmark-cmark", base_path.display(), filename.display(), style ); crate::resource::add_search_path(filename.parent().unwrap()); let doc = Document::from_file(&base_path, filename, style, template)?; trace!("Loaded doc from file OK"); Ok(doc) } /// Generate code for one document. pub fn codegen( filename: &Path, output: &Path, template: Option<&str>, ) -> Result { let r = load_document_with_pullmark(filename, Style::default(), template); let mut doc = match r { Ok(doc) => doc, Err(err) => { return Err(err); } }; doc.lint()?; let template = template .map(Ok) .unwrap_or_else(|| doc.template())? .to_string(); trace!("Template: {:?}", template); if !doc.meta().templates().any(|t| t == template) { return Err(SubplotError::TemplateSupportNotPresent); } if !doc.check_named_files_exist(&template)? || !doc.check_matched_steps_have_impl(&template) || !doc.check_embedded_files_are_used(&template)? { error!("Found problems in document, cannot continue"); std::process::exit(1); } trace!("Generating code"); generate_test_program(&mut doc, output, &template)?; trace!("Finished generating code"); Ok(CodegenOutput::new(template, doc)) } pub struct CodegenOutput { pub template: String, pub doc: Document, } impl CodegenOutput { fn new(template: String, doc: Document) -> Self { Self { template, doc } } }