rstest_bdd_patterns/
specificity.rs1use crate::PatternError;
8use crate::pattern::lexer::{Token, lex_pattern};
9use std::cmp::Ordering;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub struct SpecificityScore {
36 pub literal_chars: usize,
38 pub placeholder_count: usize,
40 pub typed_placeholder_count: usize,
42}
43
44impl SpecificityScore {
45 pub fn calculate(pattern: &str) -> Result<Self, PatternError> {
63 let tokens = lex_pattern(pattern)?;
64
65 let mut literal_chars = 0usize;
66 let mut placeholder_count = 0usize;
67 let mut typed_placeholder_count = 0usize;
68
69 for token in tokens {
70 match token {
71 Token::Literal(text) => {
72 literal_chars += text.chars().count();
73 }
74 Token::Placeholder { hint, .. } => {
75 placeholder_count += 1;
76 if hint.is_some() {
77 typed_placeholder_count += 1;
78 }
79 }
80 Token::OpenBrace { .. } | Token::CloseBrace { .. } => {
82 literal_chars += 1;
83 }
84 }
85 }
86
87 Ok(Self {
88 literal_chars,
89 placeholder_count,
90 typed_placeholder_count,
91 })
92 }
93}
94
95impl Ord for SpecificityScore {
96 fn cmp(&self, other: &Self) -> Ordering {
97 match self.literal_chars.cmp(&other.literal_chars) {
99 Ordering::Equal => {}
100 ord => return ord,
101 }
102
103 match other.placeholder_count.cmp(&self.placeholder_count) {
105 Ordering::Equal => {}
106 ord => return ord,
107 }
108
109 self.typed_placeholder_count
111 .cmp(&other.typed_placeholder_count)
112 }
113}
114
115impl PartialOrd for SpecificityScore {
116 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
117 Some(self.cmp(other))
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 fn score(pattern: &str) -> SpecificityScore {
127 match SpecificityScore::calculate(pattern) {
128 Ok(s) => s,
129 Err(e) => panic!("pattern {pattern:?} should calculate successfully: {e}"),
130 }
131 }
132
133 #[test]
134 fn literal_only_pattern_has_highest_specificity() {
135 let literal = score("overlap apples");
136 let with_placeholder = score("overlap {item}");
137
138 assert!(literal > with_placeholder);
139 assert_eq!(literal.placeholder_count, 0);
140 assert_eq!(with_placeholder.placeholder_count, 1);
141 }
142
143 #[test]
144 fn more_literal_chars_wins() {
145 let more_literal = score("the stdlib output is the workspace executable {path}");
146 let less_literal = score("the stdlib output is {expected}");
147
148 assert!(more_literal > less_literal);
149 }
150
151 #[test]
152 fn fewer_placeholders_wins_with_equal_literals() {
153 let a = score("ab {x}");
155 let b = score("a {x} {y}");
156
157 assert_eq!(a.literal_chars, 3); assert_eq!(b.literal_chars, 3); assert!(a > b, "fewer placeholders should win when literals equal");
160 }
161
162 #[test]
163 fn typed_placeholder_wins_as_tiebreaker() {
164 let typed = score("count is {n:u32}");
165 let untyped = score("count is {n}");
166
167 assert_eq!(typed.literal_chars, untyped.literal_chars);
168 assert_eq!(typed.placeholder_count, untyped.placeholder_count);
169 assert!(
170 typed > untyped,
171 "typed placeholder should win as tiebreaker"
172 );
173 }
174
175 #[test]
176 fn empty_pattern_has_zero_specificity() {
177 let empty = score("");
178
179 assert_eq!(empty.literal_chars, 0);
180 assert_eq!(empty.placeholder_count, 0);
181 assert_eq!(empty.typed_placeholder_count, 0);
182 }
183
184 #[test]
185 fn all_placeholder_pattern_has_lowest_specificity() {
186 let all_placeholders = score("{a} {b} {c}");
187 let mixed = score("prefix {a}");
188
189 assert!(mixed > all_placeholders);
190 assert_eq!(all_placeholders.literal_chars, 2); assert_eq!(all_placeholders.placeholder_count, 3);
192 }
193
194 #[test]
195 fn stray_braces_count_as_literal_chars() {
196 let with_stray = score("{ literal }");
197
198 assert_eq!(with_stray.literal_chars, 11); assert_eq!(with_stray.placeholder_count, 0);
201 }
202
203 #[test]
204 fn escaped_braces_count_as_literals() {
205 let escaped = score("value is {{x}}");
206
207 assert_eq!(escaped.literal_chars, 12); assert_eq!(escaped.placeholder_count, 0);
210 }
211
212 #[test]
213 fn multibyte_characters_counted_correctly() {
214 let unicode = score("café {value}");
215
216 assert_eq!(unicode.literal_chars, 5);
218 assert_eq!(unicode.placeholder_count, 1);
219 }
220
221 #[test]
222 fn real_world_example_from_issue() {
223 let specific = score("the stdlib output is the workspace executable {path}");
225 let generic = score("the stdlib output is {expected}");
226
227 assert!(
228 specific > generic,
229 "workspace executable pattern ({} literals) should beat generic ({} literals)",
230 specific.literal_chars,
231 generic.literal_chars
232 );
233 }
234}