use crate::{html::Location, SubplotError};
use serde::{Deserialize, Serialize};
use std::fmt;
/// A scenario step.
///
/// The scenario parser creates these kinds of data structures to
/// represent the parsed scenario step. The step consists of a kind
/// (expressed as a StepKind), and the text of the step.
///
/// This is just the step as it appears in the scenario in the input
/// text. It has not been matched with a binding. See MatchedStep for
/// that.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ScenarioStep {
kind: StepKind,
keyword: String,
text: String,
origin: Location,
}
impl ScenarioStep {
/// Construct a new step.
pub fn new(kind: StepKind, keyword: &str, text: &str, origin: Location) -> ScenarioStep {
ScenarioStep {
kind,
keyword: keyword.to_owned(),
text: text.to_owned(),
origin,
}
}
/// Return the kind of a step.
pub fn kind(&self) -> StepKind {
self.kind
}
/// Return the actual textual keyword of a step.
pub fn keyword(&self) -> &str {
&self.keyword
}
/// Return the text of a step.
pub fn text(&self) -> &str {
&self.text
}
/// Construct a step from a line in a scenario.
///
/// If the step uses the "and" or "but" keyword, use the default
/// step kind instead.
pub fn new_from_str(
text: &str,
default: Option,
origin: Location,
) -> Result {
if text.trim_start() != text {
return Err(SubplotError::NotAtBoln(text.into()));
}
let mut words = text.split_whitespace();
let keyword = match words.next() {
Some(s) => s,
_ => return Err(SubplotError::NoStepKeyword(text.to_string())),
};
let kind = match keyword.to_ascii_lowercase().as_str() {
"given" => StepKind::Given,
"when" => StepKind::When,
"then" => StepKind::Then,
"and" => default.ok_or(SubplotError::ContinuationTooEarly)?,
"but" => default.ok_or(SubplotError::ContinuationTooEarly)?,
_ => return Err(SubplotError::UnknownStepKind(keyword.to_string())),
};
let mut joined = String::new();
for word in words {
joined.push_str(word);
joined.push(' ');
}
if joined.len() > 1 {
joined.pop();
}
Ok(ScenarioStep::new(kind, keyword, &joined, origin))
}
pub(crate) fn origin(&self) -> &Location {
&self.origin
}
}
impl fmt::Display for ScenarioStep {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.keyword(), self.text())
}
}
/// Parse a scenario snippet into a vector of steps.
pub(crate) fn parse_scenario_snippet(
text: &str,
loc: &Location,
) -> Result, SubplotError> {
let mut steps = vec![];
let mut prevkind = None;
for (idx, line) in text.lines().enumerate() {
let line_loc = match loc.clone() {
Location::Known {
filename,
line,
col,
} => Location::Known {
filename,
line: line + idx,
col,
},
Location::Unknown => Location::Unknown,
};
if !line.trim().is_empty() {
let step = ScenarioStep::new_from_str(line, prevkind, line_loc)?;
prevkind = Some(step.kind());
steps.push(step);
}
}
Ok(steps)
}
#[cfg(test)]
mod test_steps_parser {
use super::{parse_scenario_snippet, Location, ScenarioStep, StepKind, SubplotError};
use std::path::Path;
fn parse(text: &str) -> Result, SubplotError> {
let loc = Location::new(Path::new("test"), 1, 1);
parse_scenario_snippet(text, &loc)
}
#[test]
fn empty_string() {
assert_eq!(parse("").unwrap(), vec![]);
}
#[test]
fn simple() {
assert_eq!(
parse("given foo").unwrap(),
vec![ScenarioStep::new(
StepKind::Given,
"given",
"foo",
Location::new(Path::new("test"), 1, 1),
)]
);
}
#[test]
fn two_simple() {
assert_eq!(
parse("given foo\nthen bar\n").unwrap(),
vec![
ScenarioStep::new(
StepKind::Given,
"given",
"foo",
Location::new(Path::new("test"), 1, 1),
),
ScenarioStep::new(
StepKind::Then,
"then",
"bar",
Location::new(Path::new("test"), 2, 1),
)
]
);
}
}
/// The kind of scenario step we have: given, when, or then.
///
/// This needs to be extended if the Subplot language gets extended with other
/// kinds of steps. However, note that the scenario parser will hide aliases,
/// such as "and" to mean the same kind as the previous step.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum StepKind {
/// A "given some precondition" step.
Given,
/// A "when something happens" step.
When,
/// A "then some condition" step.
Then,
}
impl fmt::Display for StepKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
StepKind::Given => "given",
StepKind::When => "when",
StepKind::Then => "then",
};
write!(f, "{s}")
}
}
#[cfg(test)]
mod test {
use crate::html::Location;
use super::{ScenarioStep, StepKind, SubplotError};
#[test]
fn parses_given() {
let step =
ScenarioStep::new_from_str("GIVEN I am Tomjon", None, Location::Unknown).unwrap();
assert_eq!(step.kind(), StepKind::Given);
assert_eq!(step.text(), "I am Tomjon");
}
#[test]
fn parses_given_with_extra_spaces() {
let step =
ScenarioStep::new_from_str("given I am Tomjon ", None, Location::Unknown)
.unwrap();
assert_eq!(step.kind(), StepKind::Given);
assert_eq!(step.text(), "I am Tomjon");
}
#[test]
fn parses_when() {
let step =
ScenarioStep::new_from_str("when I declare myself king", None, Location::Unknown)
.unwrap();
assert_eq!(step.kind(), StepKind::When);
assert_eq!(step.text(), "I declare myself king");
}
#[test]
fn parses_then() {
let step = ScenarioStep::new_from_str("thEN everyone accepts it", None, Location::Unknown)
.unwrap();
assert_eq!(step.kind(), StepKind::Then);
assert_eq!(step.text(), "everyone accepts it");
}
#[test]
fn parses_and() {
let step = ScenarioStep::new_from_str(
"and everyone accepts it",
Some(StepKind::Then),
Location::Unknown,
)
.unwrap();
assert_eq!(step.kind(), StepKind::Then);
assert_eq!(step.text(), "everyone accepts it");
}
#[test]
fn fails_to_parse_and() {
let step = ScenarioStep::new_from_str("and everyone accepts it", None, Location::Unknown);
assert!(step.is_err());
match step.err() {
None => unreachable!(),
Some(SubplotError::ContinuationTooEarly) => (),
Some(e) => panic!("Incorrect error: {}", e),
}
}
}