subplot 0.4.0

tools for specifying, documenting, and implementing automated acceptance tests for systems and software
Documentation
use crate::matches::MatchedSteps;

use std::path::PathBuf;
use std::process::Output;

use thiserror::Error;

/// Define all the kinds of errors any part of this crate can return.
#[derive(Debug, Error)]
pub enum SubplotError {
    /// Document has non-fatal errors.
    #[error("Document has {0} warnings.")]
    Warnings(usize),

    /// Subplot could not find a file named as a bindings file.
    #[error("binding file could not be found: {0}: {1}")]
    BindingsFileNotFound(PathBuf, std::io::Error),

    /// Subplot could not find a file named as a functions file.
    #[error("functions file could not be found: {0}: {1}")]
    FunctionsFileNotFound(PathBuf, std::io::Error),

    /// The simple pattern specifies a kind that is unknown.
    #[error("simple pattern kind {0} is unknown")]
    UnknownSimplePatternKind(String),

    /// The simple pattern and the type map disagree over the kind of a match
    #[error("simple pattern kind for {0} disagrees with the type map")]
    SimplePatternKindMismatch(String),

    /// The simple pattern contains a stray { or }.
    #[error("simple pattern contains a stray {{ or }}")]
    StrayBraceInSimplePattern(String),

    /// The simple pattern has regex metacharacters.
    ///
    /// Simple patterns are not permitted to have regex metacharacters in them
    /// unless the pattern is explicitly marked `regex: false` to indicate that
    /// the binding author understands what they're up to.
    #[error("simple pattern contains regex metacharacters: {0}")]
    SimplePatternHasMetaCharacters(String),

    /// Error while parsing a bindings file
    #[error("binding file failed to parse: {0}")]
    BindingFileParseError(PathBuf, #[source] Box<SubplotError>),

    /// Scenario step does not match a known binding
    ///
    /// This may be due to the binding missing entirely, or that the
    /// step or the binding has a typo, or that a pattern in the
    /// binding doesn't match what the author thinks it matches.
    #[error("do not understand binding: {0}")]
    BindingUnknown(String),

    /// Scenario step matches more than one binding
    ///
    /// THis may be due to bindings being too general, or having unusual
    /// overlaps in their matching
    #[error("more than one binding matches step {0}:\n{1}")]
    BindingNotUnique(String, MatchedSteps),

    /// A binding in the bindings file doesn't specify a known keyword.
    #[error("binding doesn't specify known keyword: {0}")]
    BindingWithoutKnownKeyword(String),

    /// A binding has more than one keyword (given/when/then).
    #[error("binding has more than one keyword (given/when/then)")]
    BindingHasManyKeywords(String),

    /// A binding lists an unknown type in its type map
    #[error("binding has unknown type/kind {0}")]
    UnknownTypeInBinding(String),

    /// Subplot tried to use a program, but couldn't feed it data
    ///
    /// Subplot uses some helper programs to implement some of its
    /// functionality, for example the GraphViz dot program. This
    /// error means that when tried to start a helper program and
    /// write to the helper's standard input stream, it failed to get
    /// the stream.
    ///
    /// This probably implies there's something wrong on your system.
    #[error("couldn't get stdin of child process to write to it")]
    ChildNoStdin,

    /// Subplot helper program failed
    ///
    /// Subplot uses some helper programs to implement some of its
    /// functionality, for example the GraphViz dot program. This
    /// error means that the helper program failed (exit code was not
    /// zero).
    ///
    /// This probably implies there's something wrong in Subplot.
    /// Please report this error to the Subplot project.
    #[error("child process failed: {0}")]
    ChildFailed(String),

    /// Binding doesn't define a function
    ///
    /// All binding must define the name of the function that
    /// implements the step. The bindings file has at least one
    /// binding that doesn't define one. To fix, add a `function:`
    /// field to the binding.
    #[error("binding does not name a function: {0}")]
    NoFunction(String),

    /// Document has no title
    ///
    /// The document YAML metadata does not define a document title.
    /// To fix, add a `title` field.
    #[error("document has no title")]
    NoTitle,

    /// Document has no template
    ///
    /// The document YAML metadata does not define the template to use
    /// during code generation.
    ///
    /// To fix, ensure an appropriate `impl` entry is present.
    #[error("document has no template")]
    MissingTemplate,

    /// Document has more than one template
    ///
    /// The document YAML metadata specifies more than one possible
    /// template implementation to be used during code generation.
    ///
    /// To fix, specify `--template` on the codegen CLI.
    #[error("document has more than one template possibility")]
    AmbiguousTemplate,

    /// Document does not support the requested template
    ///
    /// The document YAML metadata does not specify support for the
    /// stated template.
    ///
    /// To fix, specify a template which is provided for in the document.
    #[error("document lacks specified template support")]
    TemplateSupportNotPresent,

    /// Pandoc AST is not JSON
    ///
    /// Subplot acts as a Pandoc filter, and as part of that Pandoc
    /// constructs an _abstract syntax tree_ from the input document,
    /// and feeds it to the filter as JSON. However, when Subplot was
    /// parsing the AST, it wasn't JSON.
    ///
    /// This probably means there's something wrong with Pandoc, it's
    /// Rust bindings, or Subplot.
    #[error("Pandoc produce AST not in JSON")]
    NotJson,

    /// First scenario is before first heading
    ///
    /// Subplot scenarios are group by the input document's structure.
    /// Each scenario must be in a chapter, section, subsection, or
    /// other structural element with a heading. Subplot found a
    /// scenario block before the first heading in the document.
    ///
    /// To fix, add a heading or move the scenario after a heading.
    #[error("first scenario is before first heading")]
    ScenarioBeforeHeading,

    /// Step does not have a keyword.
    #[error("step has no keyword: {0}")]
    NoStepKeyword(String),

    /// Unknown scenario step keyword.
    ///
    /// Each scenario step must start with a known keyword (given,
    /// when, then, and, but), but Subplot didn't find one it
    /// recognized.
    ///
    /// This is usually due to a typing mistake or similar.
    #[error("unknown step keyword: {0}")]
    UnknownStepKind(String),

    /// Scenario step uses continuation keyword too early
    ///
    /// If a continuation keyword (`and` or `but`) is used too early
    /// in a scenario (i.e. before any other keyword was used) then
    /// it cannot be resolved to whichever keyword it should have been.
    #[error("continuation keyword used too early")]
    ContinuationTooEarly,

    /// Embedded file has the same name as another embedded file
    ///
    /// Names of embedded files must be unique in the input document,
    /// but Subplot found at least one with the same name as another.
    #[error("Duplicate embedded file name: {0:?}")]
    DuplicateEmbeddedFilename(String),

    /// Embedded file has more than one `add-newline` attribute
    ///
    /// The `add-newline` attribute can only be specified once for any given
    /// embedded file
    #[error("Embedded file {0} has more than one `add-newline` attribute")]
    RepeatedAddNewlineAttribute(String),

    /// Unrecognised `add-newline` attribute value on an embedded file
    ///
    /// The `add-newline` attribute can only take the values `auto`, `yes`,
    /// and `no`.
    #[error("Embedded file {0} has unrecognised `add-newline={}` - valid values are auto/yes/no")]
    UnrecognisedAddNewline(String, String),

    /// Couldn't determine base directory from input file name.
    ///
    /// Subplot needs to to determine the base directory for files
    /// referred to by the markdown input file (e.g., bindings and
    /// functions files). It failed to do that from the name of the
    /// input file. Something weird is happening.
    #[error("Could not determine base directory for included files from {0:?}")]
    BasedirError(PathBuf),

    /// Output goes into a directory that does not exist.
    ///
    /// Subplot needs to know in which directory it should write its
    /// output file, since it writes a temporary file first, then
    /// renames it to the final output file. The temporary file is
    /// created in the same directory as the final output file.
    /// However, Subplot could not find that directory.
    #[error("Output going to a directory that does not exist: {0}")]
    OutputDirectoryNotFound(String),

    /// The template.yaml is not in a directory.
    ///
    /// Template specifications reference files relative to the
    /// template.yaml file, but Subplot could not find the name of the
    /// directory containing the template.yaml file. Something is very
    /// weird.
    #[error("Couldn't find name of directory containing template spec: {0}")]
    NoTemplateSpecDirectory(PathBuf),

    /// A code template has an error.
    #[error("Couldn't load template {0}: {1}")]
    TemplateError(String, tera::Error),

    /// Unknown classes in use in document
    #[error("Unknown classes found in the document: {0}")]
    UnknownClasses(String),

    /// Template does not specify how to run generated program
    ///
    /// The template.yaml file used does not specify how to run the
    /// generated program, but user asked codegen to run it.
    #[error("template.yaml does not specify how to run generated program")]
    TemplateNoRun,

    /// An embedded file was not found.
    #[error("embedded file {0} was not found in the subplot document")]
    EmbeddedFileNotFound(String),

    /// When rendering a pikchr, something went wrong.
    #[error("failure rendering pikchr diagram: {0}")]
    PikchrRenderError(String),

    /// When attempting to codegen, no scenarios matched the desired template language
    #[error("no scenarios were found matching the `{0}` template")]
    NoScenariosMatched(String),

    /// I/O error
    ///
    /// Subplot did some I/O, and it failed. This is a generic wrapper
    /// for any kind of I/O error.
    #[error(transparent)]
    IoError {
        /// The wrapped error.
        #[from]
        source: std::io::Error,
    },

    /// Pandoc error
    ///
    /// Subplot got an error from Panoc. This is a generic wrapper for
    /// any kinds of Pandoc errors.
    #[error(transparent)]
    PandocError {
        /// The wrapped error.
        #[from]
        source: pandoc::PandocError,
    },

    /// Regular expression error
    ///
    /// Subplot uses regular expressions. This is a generic wrapper for
    /// any kinds of errors related to that.
    #[error(transparent)]
    RegexError {
        /// The wrapped error.
        #[from]
        source: regex::Error,
    },

    /// JSON error
    ///
    /// Subplot parses and generates JSON. This is a generic wrapper
    /// for any kinds of errors related to that.
    #[error(transparent)]
    JsonError {
        /// The wrapped error.
        #[from]
        source: serde_json::Error,
    },

    /// YAML error
    ///
    /// Subplot parses YAML. This is a generic wrapper for any kinds
    /// of errors related to that.
    #[error(transparent)]
    YamlError {
        /// The wrapped error.
        #[from]
        source: serde_yaml::Error,
    },

    /// Abstract syntax tree error.
    #[error(transparent)]
    Ast(#[from] crate::ast::Error),

    /// UTF8 conversion error.
    #[error(transparent)]
    Utf8Error(#[from] std::str::Utf8Error),
}

impl SubplotError {
    /// Construct a ChildFailed error.
    pub fn child_failed(msg: &str, output: &Output) -> SubplotError {
        let msg = format!(
            "{}: {}: {:?}",
            msg,
            output.status.code().unwrap_or(-1),
            String::from_utf8_lossy(&output.stderr)
        );
        SubplotError::ChildFailed(msg)
    }
}

/// A result type for this crate.
pub type Result<T> = std::result::Result<T, SubplotError>;

/// A warning, or non-fatal error.
///
/// Errors prevent Subplot from producing output. Warnings don't do that.
#[derive(Debug, Clone, thiserror::Error)]
pub enum Warning {
    /// Document refers to an embedded file that doesn't exist.
    #[error(
        "Document refers to an embedded file that doesn't exist: \"{1}\"\n  in scenario \"{0}\""
    )]
    UnknownEmbeddedFile(String, String),

    /// Embedded file is not used by any scenario.
    #[error("Embedded file is not used by any scenario: \"{0}\"")]
    UnusedEmbeddedFile(String),

    /// Missing step implementation.
    #[error("Missing step implementation: \"{1}\"\n  in scenario \"{0}\"")]
    MissingStepImplementation(String, String),

    /// Unknown binding when typesetting a scenario.
    #[error("Unknown binding: {0}")]
    UnknownBinding(String),

    /// Pikchr failed during typesetting.
    #[error("Markup using pikchr failed: {0}")]
    Pikchr(String),

    /// Dot failed during typesetting.
    #[error("Markup using dot failed: {0}")]
    Dot(String),

    /// Plantuml failed during typesetting.
    #[error("Markup using plantuml failed: {0}")]
    Plantuml(String),
}

/// A list of warnings.
///
/// Subplot collects warnings into this structure so that they can be
/// processed at the end.
#[derive(Debug, Default)]
pub struct Warnings {
    warnings: Vec<Warning>,
}

impl Warnings {
    /// Append a warning to the list.
    pub fn push(&mut self, w: Warning) {
        self.warnings.push(w);
    }

    /// Append all warnings from one list to another.
    pub fn push_all(&mut self, mut other: Warnings) {
        self.warnings.append(&mut other.warnings);
    }

    /// Return a slice with all the warnings in the list.
    pub fn warnings(&self) -> &[Warning] {
        &self.warnings
    }
}