1#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
16#[serde(rename_all = "snake_case")]
17pub enum Pattern {
18 Ubiquitous,
20 Event,
22 State,
24 Optional,
26 Unwanted,
28}
29
30impl Pattern {
31 pub fn as_str(self) -> &'static str {
32 match self {
33 Pattern::Ubiquitous => "ubiquitous",
34 Pattern::Event => "event",
35 Pattern::State => "state",
36 Pattern::Optional => "optional",
37 Pattern::Unwanted => "unwanted",
38 }
39 }
40}
41
42const SHALL: &str = "THE SYSTEM SHALL ";
44
45pub fn classify(text: &str) -> Option<Pattern> {
51 let normalized = text.split_whitespace().collect::<Vec<_>>().join(" ");
52 let upper = normalized.to_uppercase();
53 let marker = upper.find(SHALL)?;
54
55 if marker == 0 {
57 return has_response(&upper, marker).then_some(Pattern::Ubiquitous);
58 }
59
60 let (keyword, pattern) = [
61 ("WHEN ", Pattern::Event),
62 ("WHILE ", Pattern::State),
63 ("WHERE ", Pattern::Optional),
64 ("IF ", Pattern::Unwanted),
65 ]
66 .into_iter()
67 .find(|(keyword, _)| upper.starts_with(keyword))?;
68
69 let condition = upper[keyword.len()..marker]
72 .trim()
73 .trim_end_matches(',')
74 .trim()
75 .trim_end_matches("THEN")
76 .trim()
77 .trim_end_matches(',')
78 .trim()
79 .to_string();
80 (!condition.is_empty() && has_response(&upper, marker)).then_some(pattern)
81}
82
83fn has_response(upper: &str, marker: usize) -> bool {
85 !upper[marker + SHALL.len()..].trim().is_empty()
86}
87
88pub const ISO25010: [&str; 9] = [
90 "Functional Suitability",
91 "Performance Efficiency",
92 "Compatibility",
93 "Interaction Capability",
94 "Reliability",
95 "Security",
96 "Maintainability",
97 "Flexibility",
98 "Safety",
99];
100
101pub fn normalize_iso(tag: &str) -> Option<&'static str> {
105 let key = |s: &str| {
106 s.chars()
107 .filter(|c| c.is_ascii_alphanumeric())
108 .map(|c| c.to_ascii_lowercase())
109 .collect::<String>()
110 };
111 let wanted = key(tag);
112 ISO25010.into_iter().find(|c| key(c) == wanted)
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn classifies_the_five_patterns() {
121 let cases = [
122 (
123 "THE SYSTEM SHALL retain a cart for 7 days.",
124 Pattern::Ubiquitous,
125 ),
126 (
127 "WHEN a cart is abandoned, THE SYSTEM SHALL retain it for 7 days.",
128 Pattern::Event,
129 ),
130 (
131 "WHILE a sync is running, THE SYSTEM SHALL reject a second sync.",
132 Pattern::State,
133 ),
134 (
135 "WHERE the mirror is enabled, THE SYSTEM SHALL update the issue.",
136 Pattern::Optional,
137 ),
138 (
139 "IF gh is not installed, THEN THE SYSTEM SHALL complete the write.",
140 Pattern::Unwanted,
141 ),
142 ];
143 for (text, expected) in cases {
144 assert_eq!(classify(text), Some(expected), "{text}");
145 }
146 assert_eq!(
149 classify("when a cart is abandoned, the system shall retain it."),
150 Some(Pattern::Event)
151 );
152 }
153
154 #[test]
155 fn rejects_text_that_cannot_be_tested() {
156 assert_eq!(classify("The cart should be user-friendly."), None);
158 assert_eq!(classify("THE SYSTEM SHALL"), None);
160 assert_eq!(
161 classify("WHEN a cart is abandoned, THE SYSTEM SHALL "),
162 None
163 );
164 assert_eq!(classify("WHEN, THE SYSTEM SHALL retain the cart."), None);
166 assert_eq!(classify("IF THEN THE SYSTEM SHALL retain the cart."), None);
167 assert_eq!(
169 classify("AFTER a cart is abandoned, THE SYSTEM SHALL retain it."),
170 None
171 );
172 }
173
174 #[test]
175 fn iso_tags_are_canonicalized_and_unknown_ones_are_reported() {
176 assert_eq!(
177 normalize_iso("performance-efficiency"),
178 Some("Performance Efficiency")
179 );
180 assert_eq!(normalize_iso(" SECURITY "), Some("Security"));
181 assert_eq!(normalize_iso("Reliability"), Some("Reliability"));
182 assert_eq!(normalize_iso("Usability"), None);
185 assert_eq!(normalize_iso("Fastness"), None);
186 }
187}
188
189#[cfg(test)]
192mod readme {
193 use super::*;
194
195 #[test]
196 fn the_readme_example_is_true() {
197 assert_eq!(
198 classify("WHEN the archive is downloaded, THE SYSTEM SHALL verify its checksum"),
199 Some(Pattern::Event)
200 );
201 assert_eq!(classify("it should probably be fast"), None);
202 assert_eq!(
203 normalize_iso("performance-efficiency"),
204 Some("Performance Efficiency")
205 );
206 assert_eq!(normalize_iso("Speed"), None);
207 assert_eq!(ISO25010.len(), 9);
208 }
209}