Skip to main content

rstest_bdd_patterns/
keyword.rs

1//! Shared step keyword type and parsing utilities.
2//!
3//! This module provides the canonical [`StepKeyword`] enum used by both the
4//! runtime and proc-macro crates, ensuring consistent keyword handling across
5//! compile-time validation and runtime execution.
6
7use gherkin::StepType;
8use std::fmt;
9use std::str::FromStr;
10
11/// Keyword used to categorise a step definition.
12///
13/// The enum includes `And` and `But` variants for completeness, but feature
14/// parsing resolves them against the preceding `Given`/`When`/`Then` using
15/// the [`resolve`](Self::resolve) method.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum StepKeyword {
18    /// Setup preconditions for a scenario.
19    Given,
20    /// Perform an action when testing behaviour.
21    When,
22    /// Assert the expected outcome of a scenario.
23    Then,
24    /// Additional conditions that share context with the previous step.
25    And,
26    /// Negative or contrasting conditions.
27    But,
28}
29
30impl StepKeyword {
31    /// Return the keyword as a string slice.
32    ///
33    /// # Examples
34    ///
35    /// ```
36    /// use rstest_bdd_patterns::StepKeyword;
37    ///
38    /// assert_eq!(StepKeyword::Given.as_str(), "Given");
39    /// assert_eq!(StepKeyword::And.as_str(), "And");
40    /// ```
41    #[must_use]
42    pub const fn as_str(&self) -> &'static str {
43        match self {
44            Self::Given => "Given",
45            Self::When => "When",
46            Self::Then => "Then",
47            Self::And => "And",
48            Self::But => "But",
49        }
50    }
51
52    /// Resolve conjunctions to the semantic keyword of the previous step.
53    ///
54    /// When the current keyword is `And` or `But`, returns the value stored in
55    /// `prev`. For primary keywords (`Given`/`When`/`Then`), updates `prev` and
56    /// returns the keyword unchanged.
57    ///
58    /// Callers typically seed `prev` with the first primary keyword in a
59    /// sequence, defaulting to `Given` when none is found.
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use rstest_bdd_patterns::StepKeyword;
65    ///
66    /// let mut prev = Some(StepKeyword::Given);
67    /// assert_eq!(StepKeyword::And.resolve(&mut prev), StepKeyword::Given);
68    /// assert_eq!(StepKeyword::When.resolve(&mut prev), StepKeyword::When);
69    /// assert_eq!(prev, Some(StepKeyword::When));
70    /// ```
71    #[must_use]
72    pub fn resolve(self, prev: &mut Option<Self>) -> Self {
73        if matches!(self, Self::And | Self::But) {
74            prev.as_ref().copied().unwrap_or(Self::Given)
75        } else {
76            *prev = Some(self);
77            self
78        }
79    }
80}
81
82/// Error returned when parsing a [`StepKeyword`] from a string fails.
83///
84/// Contains the unrecognised keyword text for diagnostic purposes.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct StepKeywordParseError(pub String);
87
88impl fmt::Display for StepKeywordParseError {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        write!(f, "invalid step keyword: {}", self.0)
91    }
92}
93
94impl std::error::Error for StepKeywordParseError {}
95
96impl FromStr for StepKeyword {
97    type Err = StepKeywordParseError;
98
99    fn from_str(value: &str) -> Result<Self, Self::Err> {
100        let trimmed = value.trim();
101        if trimmed.eq_ignore_ascii_case("given") {
102            Ok(Self::Given)
103        } else if trimmed.eq_ignore_ascii_case("when") {
104            Ok(Self::When)
105        } else if trimmed.eq_ignore_ascii_case("then") {
106            Ok(Self::Then)
107        } else if trimmed.eq_ignore_ascii_case("and") {
108            Ok(Self::And)
109        } else if trimmed.eq_ignore_ascii_case("but") {
110            Ok(Self::But)
111        } else {
112            Err(StepKeywordParseError(trimmed.to_string()))
113        }
114    }
115}
116
117impl TryFrom<&str> for StepKeyword {
118    type Error = StepKeywordParseError;
119
120    fn try_from(value: &str) -> Result<Self, Self::Error> {
121        value.parse()
122    }
123}
124
125/// Error raised when converting a parsed Gherkin [`StepType`] into a
126/// [`StepKeyword`] fails.
127///
128/// Captures the offending [`StepType`] to help callers diagnose missing
129/// language support.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct UnsupportedStepType(pub StepType);
132
133impl fmt::Display for UnsupportedStepType {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        write!(f, "unsupported step type: {:?}", self.0)
136    }
137}
138
139impl std::error::Error for UnsupportedStepType {}
140
141impl TryFrom<StepType> for StepKeyword {
142    type Error = UnsupportedStepType;
143
144    /// Convert a parsed Gherkin [`StepType`] to a [`StepKeyword`].
145    ///
146    /// # Gherkin coupling
147    ///
148    /// The `gherkin` crate (v0.14) defines `StepType` with only `Given`, `When`,
149    /// and `Then` variants. Gherkin syntax also includes `And` and `But`
150    /// keywords, but the parser resolves these to the preceding primary keyword
151    /// before exposing them via `StepType`.
152    ///
153    /// If a future `gherkin` release adds new `StepType` variants (e.g., `And`,
154    /// `But`, or others), the `#[expect(unreachable_patterns)]` guard below will
155    /// trigger a compiler warning, prompting an update to this match arm.
156    fn try_from(ty: StepType) -> Result<Self, Self::Error> {
157        match ty {
158            StepType::Given => Ok(Self::Given),
159            StepType::When => Ok(Self::When),
160            StepType::Then => Ok(Self::Then),
161            // Guard against future StepType variants. If gherkin adds new
162            // variants, this pattern becomes reachable and the `expect`
163            // attribute fires a compiler warning, prompting maintainers to
164            // update this conversion.
165            #[expect(unreachable_patterns, reason = "guard future StepType variants")]
166            other => Err(UnsupportedStepType(other)),
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use rstest::rstest;
175
176    #[expect(clippy::expect_used, reason = "test helper with descriptive failures")]
177    fn parse_kw(input: &str) -> StepKeyword {
178        input
179            .parse()
180            .expect("test input should parse to a valid keyword")
181    }
182
183    #[rstest]
184    #[case("Given", StepKeyword::Given)]
185    #[case("given", StepKeyword::Given)]
186    #[case(" WhEn ", StepKeyword::When)]
187    #[case("THEN", StepKeyword::Then)]
188    #[case("AND", StepKeyword::And)]
189    #[case(" but ", StepKeyword::But)]
190    fn parses_case_insensitively(#[case] input: &str, #[case] expected: StepKeyword) {
191        assert_eq!(parse_kw(input), expected);
192    }
193
194    #[test]
195    #[expect(
196        clippy::expect_used,
197        reason = "test verifies error case with descriptive failure"
198    )]
199    fn rejects_invalid_keyword() {
200        let result = "invalid".parse::<StepKeyword>();
201        assert!(result.is_err());
202        let err = result.expect_err("expected parse error for invalid keyword");
203        assert_eq!(err.0, "invalid");
204    }
205
206    #[expect(clippy::expect_used, reason = "test helper with descriptive failures")]
207    fn kw_from_type(ty: StepType) -> StepKeyword {
208        StepKeyword::try_from(ty).expect("test StepType should convert to StepKeyword")
209    }
210
211    #[rstest]
212    #[case(StepType::Given, StepKeyword::Given)]
213    #[case(StepType::When, StepKeyword::When)]
214    #[case(StepType::Then, StepKeyword::Then)]
215    fn maps_step_type(#[case] ty: StepType, #[case] expected: StepKeyword) {
216        assert_eq!(kw_from_type(ty), expected);
217    }
218
219    #[test]
220    fn as_str_returns_canonical_name() {
221        assert_eq!(StepKeyword::Given.as_str(), "Given");
222        assert_eq!(StepKeyword::When.as_str(), "When");
223        assert_eq!(StepKeyword::Then.as_str(), "Then");
224        assert_eq!(StepKeyword::And.as_str(), "And");
225        assert_eq!(StepKeyword::But.as_str(), "But");
226    }
227
228    #[test]
229    fn resolve_returns_previous_for_conjunctions() {
230        let mut prev = Some(StepKeyword::When);
231        assert_eq!(StepKeyword::And.resolve(&mut prev), StepKeyword::When);
232        assert_eq!(StepKeyword::But.resolve(&mut prev), StepKeyword::When);
233        // prev unchanged for conjunctions
234        assert_eq!(prev, Some(StepKeyword::When));
235    }
236
237    #[test]
238    fn resolve_updates_previous_for_primary_keywords() {
239        let mut prev = Some(StepKeyword::Given);
240        assert_eq!(StepKeyword::When.resolve(&mut prev), StepKeyword::When);
241        assert_eq!(prev, Some(StepKeyword::When));
242        assert_eq!(StepKeyword::Then.resolve(&mut prev), StepKeyword::Then);
243        assert_eq!(prev, Some(StepKeyword::Then));
244    }
245
246    #[test]
247    fn resolve_defaults_to_given_when_unseeded() {
248        let mut prev = None;
249        assert_eq!(StepKeyword::And.resolve(&mut prev), StepKeyword::Given);
250        assert_eq!(prev, None); // conjunctions don't update prev
251    }
252}