rstest_bdd_patterns/
keyword.rs1use gherkin::StepType;
8use std::fmt;
9use std::str::FromStr;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum StepKeyword {
18 Given,
20 When,
22 Then,
24 And,
26 But,
28}
29
30impl StepKeyword {
31 #[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 #[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#[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#[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 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 #[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 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); }
252}