1use crate::core::row::Row;
2use std::collections::HashSet;
3
4use crate::dsl::parse::key_spec::ExactMode;
5use crate::dsl::parse::path::{PathExpression, Selector, parse_path};
6
7#[derive(Debug, Clone, Default, PartialEq, Eq)]
8pub struct KeyMatches {
9 pub exact: Vec<String>,
10 pub partial: Vec<String>,
11}
12
13pub fn match_row_keys<'a>(row: &'a Row, token: &str, exact: ExactMode) -> Vec<&'a str> {
14 let matches = compute_key_matches(row, token, exact);
15 let selected = if !matches.exact.is_empty() {
16 matches.exact
17 } else {
18 matches.partial
19 };
20 if selected.is_empty() {
21 return Vec::new();
22 }
23
24 let selected = selected.into_iter().collect::<HashSet<_>>();
25 row.keys()
26 .filter_map(|key| {
27 if selected.contains(key) {
28 Some(key.as_str())
29 } else {
30 None
31 }
32 })
33 .collect()
34}
35
36pub fn match_row_keys_detailed(row: &Row, token: &str, exact: ExactMode) -> KeyMatches {
37 compute_key_matches(row, token, exact)
38}
39
40pub fn value_contains(value: &serde_json::Value, query: &str, case_sensitive: bool) -> bool {
41 match value {
42 serde_json::Value::Array(values) => values
43 .iter()
44 .any(|item| value_contains(item, query, case_sensitive)),
45 _ => {
46 let rendered = render_value(value);
47 if case_sensitive {
48 rendered.contains(query)
49 } else {
50 rendered
51 .to_ascii_lowercase()
52 .contains(&query.to_ascii_lowercase())
53 }
54 }
55 }
56}
57
58pub fn render_value(value: &serde_json::Value) -> String {
59 match value {
60 serde_json::Value::Null => "null".to_string(),
61 serde_json::Value::Bool(v) => v.to_string(),
62 serde_json::Value::Number(v) => v.to_string(),
63 serde_json::Value::String(v) => v.clone(),
64 serde_json::Value::Array(_) | serde_json::Value::Object(_) => value.to_string(),
65 }
66}
67
68fn expression_segments(expr: &PathExpression, case_sensitive: bool) -> Vec<String> {
69 expr.segments
70 .iter()
71 .filter_map(|segment| segment.name.as_ref())
72 .map(|name| {
73 if case_sensitive {
74 name.clone()
75 } else {
76 name.to_ascii_lowercase()
77 }
78 })
79 .collect()
80}
81
82fn key_segments(key: &str, case_sensitive: bool) -> Vec<String> {
83 let mut segments = Vec::new();
84 let mut current = String::new();
85 let mut in_brackets = false;
86
87 for ch in key.chars() {
88 match ch {
89 '.' if !in_brackets => {
90 push_segment(&mut segments, &mut current, case_sensitive);
91 }
92 '[' => {
93 push_segment(&mut segments, &mut current, case_sensitive);
94 in_brackets = true;
95 current.clear();
96 }
97 ']' => {
98 in_brackets = false;
99 current.clear();
100 }
101 _ => {
102 if !in_brackets {
103 current.push(ch);
104 }
105 }
106 }
107 }
108
109 push_segment(&mut segments, &mut current, case_sensitive);
110 segments
111}
112
113fn push_segment(segments: &mut Vec<String>, current: &mut String, case_sensitive: bool) {
114 if current.is_empty() {
115 return;
116 }
117 let segment = if case_sensitive {
118 current.clone()
119 } else {
120 current.to_ascii_lowercase()
121 };
122 segments.push(segment);
123 current.clear();
124}
125
126fn segments_match(seq: &[String], pattern: &[String], absolute: bool) -> bool {
127 if pattern.is_empty() {
128 return false;
129 }
130 if absolute {
131 if seq.len() < pattern.len() {
132 return false;
133 }
134 return seq[..pattern.len()] == *pattern;
135 }
136
137 let mut pos = 0usize;
140 for segment in pattern {
141 while pos < seq.len() && &seq[pos] != segment {
142 pos += 1;
143 }
144 if pos == seq.len() {
145 return false;
146 }
147 pos += 1;
148 }
149 true
150}
151
152fn matches_expression_with_selectors(
153 key: &str,
154 expr: &PathExpression,
155 case_sensitive: bool,
156) -> bool {
157 let Ok(key_expr) = parse_path(key) else {
158 return false;
159 };
160 if key_expr.segments.len() != expr.segments.len() {
161 return false;
162 }
163
164 for (key_segment, expr_segment) in key_expr.segments.iter().zip(expr.segments.iter()) {
165 if !names_match(&key_segment.name, &expr_segment.name, case_sensitive) {
166 return false;
167 }
168 if !selectors_match(&key_segment.selectors, &expr_segment.selectors) {
169 return false;
170 }
171 }
172 true
173}
174
175fn names_match(left: &Option<String>, right: &Option<String>, case_sensitive: bool) -> bool {
176 match (left, right) {
177 (Some(left), Some(right)) => {
178 if case_sensitive {
179 left == right
180 } else {
181 left.eq_ignore_ascii_case(right)
182 }
183 }
184 (None, None) => true,
185 _ => false,
186 }
187}
188
189fn selectors_match(keys: &[Selector], exprs: &[Selector]) -> bool {
190 if exprs.is_empty() {
191 return keys.is_empty();
192 }
193
194 let mut key_iter = keys.iter();
195 for expr in exprs {
196 match expr {
197 Selector::Index(target) => match key_iter.next() {
198 Some(Selector::Index(actual)) if actual == target => {}
199 _ => return false,
200 },
201 Selector::Fanout => return true,
202 Selector::Slice { start, stop, step } => {
203 let is_full = start.is_none() && stop.is_none() && step.is_none();
204 if is_full {
205 return true;
206 }
207 return false;
208 }
209 }
210 }
211
212 key_iter.next().is_none()
213}
214
215fn compute_key_matches(row: &Row, token: &str, exact: ExactMode) -> KeyMatches {
216 let trimmed = token.trim();
217 if trimmed.is_empty() {
218 return KeyMatches::default();
219 }
220
221 let expr = parse_path(trimmed)
222 .ok()
223 .filter(|expr| !expr.segments.is_empty());
224 let case_sensitive = matches!(exact, ExactMode::CaseSensitive);
225 let allow_partial = matches!(exact, ExactMode::None);
226 let expr_has_selectors = expr.as_ref().is_some_and(|expr| {
227 expr.segments
228 .iter()
229 .any(|segment| !segment.selectors.is_empty())
230 });
231 let expr_segments = expr
232 .as_ref()
233 .map(|expr| expression_segments(expr, case_sensitive));
234 let should_try_plain_label_match = expr.is_none()
235 || (!expr.as_ref().is_some_and(|expr| expr.absolute)
236 && !expr_has_selectors
237 && expr_segments
238 .as_ref()
239 .map(|segments| segments.len())
240 .unwrap_or(0)
241 == 1);
242
243 let token_cmp = if case_sensitive {
244 trimmed.to_string()
245 } else {
246 trimmed.to_ascii_lowercase()
247 };
248
249 let mut exact_keys = Vec::new();
250 let mut partial_keys = Vec::new();
251
252 for key in row.keys() {
253 if let Some(expr) = &expr {
254 if expr_has_selectors {
255 if matches_expression_with_selectors(key, expr, case_sensitive) {
256 exact_keys.push(key.clone());
257 continue;
258 }
259 } else if let Some(pattern) = &expr_segments {
260 let segments = key_segments(key, case_sensitive);
261 if segments_match(&segments, pattern, expr.absolute) {
262 exact_keys.push(key.clone());
263 continue;
264 }
265 }
266 }
267
268 if !should_try_plain_label_match {
269 continue;
270 }
271
272 let segments = key_segments(key, case_sensitive);
273 let Some(last_segment) = segments.last() else {
274 continue;
275 };
276
277 let last_cmp = if case_sensitive {
278 last_segment.clone()
279 } else {
280 last_segment.to_ascii_lowercase()
281 };
282
283 let exact_match = match exact {
284 ExactMode::CaseSensitive => last_segment == trimmed,
285 ExactMode::CaseInsensitive => last_segment.eq_ignore_ascii_case(trimmed),
286 ExactMode::None => last_segment.eq_ignore_ascii_case(trimmed),
287 };
288 if exact_match {
289 exact_keys.push(key.clone());
290 continue;
291 }
292
293 if allow_partial {
294 let key_cmp = if case_sensitive {
295 key.clone()
296 } else {
297 key.to_ascii_lowercase()
298 };
299 if key_cmp.contains(&token_cmp) || last_cmp.contains(&token_cmp) {
300 partial_keys.push(key.clone());
301 }
302 }
303 }
304
305 let mut seen_partial = partial_keys.iter().cloned().collect::<HashSet<_>>();
306 for key in &exact_keys {
307 if seen_partial.insert(key.clone()) {
308 partial_keys.push(key.clone());
309 }
310 }
311
312 KeyMatches {
313 exact: exact_keys,
314 partial: partial_keys,
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use serde_json::json;
321
322 use crate::dsl::parse::key_spec::ExactMode;
323
324 use super::{match_row_keys, match_row_keys_detailed, value_contains};
325
326 #[test]
327 fn matches_last_segment_case_insensitive() {
328 let row = json!({"ldap.uid": "oistes", "mail": "o@uio.no"})
329 .as_object()
330 .cloned()
331 .expect("object");
332
333 let matched = match_row_keys(&row, "UID", ExactMode::CaseInsensitive);
334 assert_eq!(matched, vec!["ldap.uid"]);
335 }
336
337 #[test]
338 fn matches_subsequence_dotted_paths() {
339 let row = json!({
340 "metadata.asset.id": 42,
341 "asset.id": 7,
342 "metadata.owner.id": 9
343 })
344 .as_object()
345 .cloned()
346 .expect("object");
347
348 let matched = match_row_keys(&row, "asset.id", ExactMode::None);
349 assert_eq!(matched, vec!["metadata.asset.id", "asset.id"]);
350 }
351
352 #[test]
353 fn absolute_paths_require_prefix_match() {
354 let row = json!({
355 "metadata.asset.id": 42,
356 "asset.id": 7
357 })
358 .as_object()
359 .cloned()
360 .expect("object");
361
362 let matched = match_row_keys(&row, ".asset.id", ExactMode::None);
363 assert_eq!(matched, vec!["asset.id"]);
364 }
365
366 #[test]
367 fn selector_paths_match_exact_index() {
368 let row = json!({
369 "items[0].id": 1,
370 "items[1].id": 2
371 })
372 .as_object()
373 .cloned()
374 .expect("object");
375
376 let matched = match_row_keys(&row, "items[0].id", ExactMode::None);
377 assert_eq!(matched, vec!["items[0].id"]);
378 }
379
380 #[test]
381 fn detailed_matching_reports_partial_hits_when_exact_match_is_absent() {
382 let row = json!({
383 "metadata.asset.id": 42,
384 "metadata.asset.name": "vm-01"
385 })
386 .as_object()
387 .cloned()
388 .expect("object");
389
390 let matches = match_row_keys_detailed(&row, "nam", ExactMode::None);
391 assert!(matches.exact.is_empty());
392 assert_eq!(matches.partial, vec!["metadata.asset.name".to_string()]);
393 }
394
395 #[test]
396 fn value_contains_handles_arrays_and_case_sensitivity() {
397 let value = json!(["Alpha", {"name": "Bravo"}]);
398
399 assert!(value_contains(&value, "bravo", false));
400 assert!(!value_contains(&value, "bravo", true));
401 assert!(value_contains(&value, "Alpha", true));
402 }
403}