subplot 0.2.0

tools for specifying, documenting, and implementing automated acceptance tests for systems and software
Documentation
use crate::Result;
use crate::Scenario;
use crate::StepKind;
use crate::{bindings::CaptureType, Bindings};

use std::collections::HashMap;

use serde::{Deserialize, Serialize};

/// A scenario that has all of its steps matched with steps using bindings.
#[derive(Debug, Serialize, Deserialize)]
pub struct MatchedScenario {
    title: String,
    steps: Vec<MatchedStep>,
}

impl MatchedScenario {
    /// Construct a new matched scenario
    pub fn new(scen: &Scenario, bindings: &Bindings) -> Result<MatchedScenario> {
        let steps: Result<Vec<MatchedStep>> = scen
            .steps()
            .iter()
            .map(|step| bindings.find(step))
            .collect();
        Ok(MatchedScenario {
            title: scen.title().to_string(),
            steps: steps?,
        })
    }

    /// Get the steps in this matched scenario
    pub fn steps(&self) -> &[MatchedStep] {
        &self.steps
    }
}

/// A matched binding and scenario step, with captured parts.
///
/// A MatchedStep is a sequence of partial steps, each representing
/// either a captured or a matching part of the text of the step.
#[derive(Debug, Serialize, Deserialize)]
pub struct MatchedStep {
    kind: StepKind,
    text: String,
    parts: Vec<PartialStep>,
    function: String,
    cleanup: Option<String>,
    types: HashMap<String, CaptureType>,
}

impl MatchedStep {
    /// Return a new empty match. Empty means it has no step parts.
    pub fn new(
        kind: StepKind,
        function: &str,
        cleanup: Option<&str>,
        types: &HashMap<String, CaptureType>,
    ) -> MatchedStep {
        MatchedStep {
            kind,
            text: "".to_string(),
            parts: vec![],
            function: function.to_string(),
            cleanup: cleanup.map(String::from),
            types: types.clone(),
        }
    }

    /// The kind of step that has been matched.
    pub fn kind(&self) -> StepKind {
        self.kind
    }

    /// The name of the function to call for the step.
    pub fn function(&self) -> &str {
        &self.function
    }

    /// Append a partial step to the match.
    pub fn append_part(&mut self, part: PartialStep) {
        self.parts.push(part);
        self.text = self.text();
    }

    /// Iterate over all partial steps in the match.
    pub fn parts(&self) -> impl Iterator<Item = &PartialStep> {
        self.parts.iter()
    }

    fn text(&self) -> String {
        let mut t = String::new();
        for part in self.parts() {
            t.push_str(part.as_text());
        }
        t
    }

    /// Return the typemap for the matched step
    pub fn types(&self) -> &HashMap<String, CaptureType> {
        &self.types
    }
}

/// Part of a scenario step, possibly captured by a pattern.
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum PartialStep {
    /// The text of a step part that isn't captured.
    UncapturedText(StepSnippet),

    /// A captured step part that is just text, and the regex capture
    /// name that corresponds to it.
    CapturedText {
        /// Name of the capture.
        name: String,
        /// Text of the capture.
        text: String,
    },
}

impl PartialStep {
    /// Construct an uncaptured part of a scenario step.
    pub fn uncaptured(text: &str) -> PartialStep {
        PartialStep::UncapturedText(StepSnippet::new(text))
    }

    /// Construct a textual captured part of a scenario step.
    pub fn text(name: &str, text: &str) -> PartialStep {
        PartialStep::CapturedText {
            name: name.to_string(),
            text: text.to_string(),
        }
    }

    fn as_text(&self) -> &str {
        match self {
            PartialStep::UncapturedText(snippet) => snippet.text(),
            PartialStep::CapturedText { text, .. } => &text,
        }
    }
}

#[cfg(test)]
mod test_partial_steps {
    use super::PartialStep;

    #[test]
    fn identical_uncaptured_texts_match() {
        let p1 = PartialStep::uncaptured("foo");
        let p2 = PartialStep::uncaptured("foo");
        assert_eq!(p1, p2);
    }

    #[test]
    fn different_uncaptured_texts_dont_match() {
        let p1 = PartialStep::uncaptured("foo");
        let p2 = PartialStep::uncaptured("bar");
        assert_ne!(p1, p2);
    }

    #[test]
    fn identical_captured_texts_match() {
        let p1 = PartialStep::text("xxx", "foo");
        let p2 = PartialStep::text("xxx", "foo");
        assert_eq!(p1, p2);
    }

    #[test]
    fn different_captured_texts_dont_match() {
        let p1 = PartialStep::text("xxx", "foo");
        let p2 = PartialStep::text("xxx", "bar");
        assert_ne!(p1, p2);
    }

    #[test]
    fn differently_named_captured_texts_dont_match() {
        let p1 = PartialStep::text("xxx", "foo");
        let p2 = PartialStep::text("yyy", "foo");
        assert_ne!(p1, p2);
    }

    #[test]
    fn differently_captured_texts_dont_match() {
        let p1 = PartialStep::uncaptured("foo");
        let p2 = PartialStep::text("xxx", "foo");
        assert_ne!(p1, p2);
    }
}

/// The text of a part of a scenario step.
///
/// This is used by both unmatched and matched parts of a step. This
/// will later grow to include more information for better error
/// messges etc.
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct StepSnippet {
    text: String,
}

impl StepSnippet {
    /// Create a new snippet.
    pub fn new(text: &str) -> StepSnippet {
        StepSnippet {
            text: text.to_owned(),
        }
    }

    /// Return the text of the snippet.
    pub fn text(&self) -> &str {
        &self.text
    }
}

#[cfg(test)]
mod test {
    use super::PartialStep;

    #[test]
    fn returns_uncaptured() {
        let p = PartialStep::uncaptured("foo");
        match p {
            PartialStep::UncapturedText(s) => assert_eq!(s.text(), "foo"),
            _ => panic!("expected UncapturedText: {:?}", p),
        }
    }

    #[test]
    fn returns_text() {
        let p = PartialStep::text("xxx", "foo");
        match p {
            PartialStep::CapturedText { name, text } => {
                assert_eq!(name, "xxx");
                assert_eq!(text, "foo");
            }
            _ => panic!("expected CapturedText: {:?}", p),
        }
    }
}