1use crate::types::{PlaceholderSyntaxError, StepPatternError};
6use regex::Regex;
7use rstest_bdd_patterns::{PatternError, SpecificityScore, compile_regex_from_pattern};
8use std::hash::{Hash, Hasher};
9use std::sync::OnceLock;
10
11#[derive(Debug)]
13pub struct StepPattern {
14 text: &'static str,
15 pub(crate) regex: OnceLock<Regex>,
16 specificity: OnceLock<SpecificityScore>,
17}
18
19impl PartialEq for StepPattern {
23 fn eq(&self, other: &Self) -> bool {
24 self.text == other.text
25 }
26}
27
28impl Eq for StepPattern {}
29
30impl Hash for StepPattern {
31 fn hash<H: Hasher>(&self, state: &mut H) {
32 self.text.hash(state);
33 }
34}
35
36impl From<PatternError> for StepPatternError {
37 fn from(err: PatternError) -> Self {
38 match err {
39 PatternError::Placeholder(info) => Self::PlaceholderSyntax(
40 PlaceholderSyntaxError::new(info.message, info.position, info.placeholder),
41 ),
42 PatternError::Regex(e) => Self::InvalidPattern(e),
43 }
44 }
45}
46
47impl StepPattern {
48 #[must_use]
50 pub const fn new(value: &'static str) -> Self {
51 Self {
52 text: value,
53 regex: OnceLock::new(),
54 specificity: OnceLock::new(),
55 }
56 }
57
58 #[must_use]
60 pub const fn as_str(&self) -> &'static str {
61 self.text
62 }
63
64 pub fn compile(&self) -> Result<(), StepPatternError> {
76 if self.regex.get().is_some() {
77 return Ok(());
78 }
79 let regex = compile_regex_from_pattern(self.text)?;
80 let _ = self.regex.set(regex);
81 Ok(())
82 }
83
84 #[must_use]
89 #[expect(
90 clippy::expect_used,
91 reason = "internal method; callers guarantee prior compilation"
92 )]
93 pub(crate) fn regex_unchecked(&self) -> &Regex {
94 self.regex.get().expect("regex accessed before compilation")
95 }
96
97 pub fn specificity(&self) -> Result<SpecificityScore, StepPatternError> {
126 if let Some(score) = self.specificity.get() {
127 return Ok(*score);
128 }
129 let score = SpecificityScore::calculate(self.text)?;
130 let _ = self.specificity.set(score);
131 Ok(score)
132 }
133}
134
135impl From<&'static str> for StepPattern {
136 fn from(value: &'static str) -> Self {
137 Self::new(value)
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use std::ptr;
145
146 #[test]
147 #[expect(clippy::expect_used, reason = "test helper validates success path")]
148 fn regex_unchecked_returns_cached_regex_after_compilation() {
149 let pattern = StepPattern::from("literal text");
150 pattern.compile().expect("literal pattern should compile");
151
152 let re1 = pattern.regex_unchecked();
154 let re2 = pattern.regex_unchecked();
155
156 assert!(ptr::eq(re1, re2));
157 assert!(re1.is_match("literal text"));
158 }
159
160 #[test]
161 #[expect(clippy::expect_used, reason = "test validates compilation")]
162 fn compile_is_idempotent() {
163 let pattern = StepPattern::from("literal text");
164
165 pattern.compile().expect("literal pattern should compile");
167 let re1 = pattern.regex_unchecked();
168
169 pattern.compile().expect("recompile should succeed");
171 let re2 = pattern.regex_unchecked();
172
173 assert!(ptr::eq(re1, re2), "compile should be idempotent");
174 }
175
176 #[test]
177 #[should_panic(expected = "regex accessed before compilation")]
178 fn regex_unchecked_panics_without_prior_compilation() {
179 let pattern = StepPattern::from("literal text");
180 let _ = pattern.regex_unchecked();
182 }
183}