1use crate::models::{Priority, Spec, SpecQuery, SpecType};
4use regex::Regex;
5
6pub struct SpecQueryEngine;
8
9impl SpecQueryEngine {
10 pub fn query(specs: &[Spec], query: &SpecQuery) -> Vec<Spec> {
24 specs
25 .iter()
26 .filter(|spec| Self::matches_query(spec, query))
27 .cloned()
28 .collect()
29 }
30
31 fn matches_query(spec: &Spec, query: &SpecQuery) -> bool {
33 if let Some(ref name_filter) = query.name {
35 if !Self::matches_name(spec, name_filter) {
36 return false;
37 }
38 }
39
40 if let Some(spec_type) = query.spec_type {
42 if !Self::matches_type(spec, spec_type) {
43 return false;
44 }
45 }
46
47 if let Some(status) = query.status {
49 if spec.metadata.status != status {
50 return false;
51 }
52 }
53
54 if let Some(priority) = query.priority {
56 if !Self::matches_priority(spec, priority) {
57 return false;
58 }
59 }
60
61 if let Some(phase) = query.phase {
63 if spec.metadata.phase != phase {
64 return false;
65 }
66 }
67
68 for (field, value) in &query.custom_filters {
70 if !Self::matches_custom_filter(spec, field, value) {
71 return false;
72 }
73 }
74
75 true
76 }
77
78 fn matches_name(spec: &Spec, filter: &str) -> bool {
80 if spec.name.eq_ignore_ascii_case(filter) {
82 return true;
83 }
84
85 if spec.name.to_lowercase().contains(&filter.to_lowercase()) {
87 return true;
88 }
89
90 if spec.id.eq_ignore_ascii_case(filter) {
92 return true;
93 }
94
95 if let Ok(regex) = Regex::new(filter) {
97 if regex.is_match(&spec.name) || regex.is_match(&spec.id) {
98 return true;
99 }
100 }
101
102 false
103 }
104
105 fn matches_type(spec: &Spec, spec_type: SpecType) -> bool {
107 let inferred_type = if !spec.requirements.is_empty() {
109 SpecType::Feature
110 } else if spec.design.is_some() {
111 SpecType::Component
112 } else if !spec.tasks.is_empty() {
113 SpecType::Task
114 } else {
115 SpecType::Feature };
117
118 inferred_type == spec_type
119 }
120
121 fn matches_priority(spec: &Spec, priority: Priority) -> bool {
123 spec.requirements.iter().any(|req| req.priority == priority)
124 }
125
126 fn matches_custom_filter(spec: &Spec, field: &str, value: &str) -> bool {
128 match field.to_lowercase().as_str() {
129 "author" => spec
130 .metadata
131 .author
132 .as_ref()
133 .map(|a| a.contains(value))
134 .unwrap_or(false),
135 "version" => spec.version.contains(value),
136 "id" => spec.id.contains(value),
137 _ => false,
138 }
139 }
140
141 pub fn resolve_dependencies(spec: &Spec, all_specs: &[Spec]) -> Vec<String> {
154 let mut dependencies = Vec::new();
155
156 let mut referenced_req_ids = std::collections::HashSet::new();
158 for task in &spec.tasks {
159 Self::collect_requirement_ids(task, &mut referenced_req_ids);
160 }
161
162 for other_spec in all_specs {
164 if other_spec.id == spec.id {
165 continue;
166 }
167
168 for req in &other_spec.requirements {
169 if referenced_req_ids.contains(&req.id) {
170 dependencies.push(other_spec.id.clone());
171 break;
172 }
173 }
174 }
175
176 dependencies.sort();
177 dependencies.dedup();
178 dependencies
179 }
180
181 fn collect_requirement_ids(
183 task: &crate::models::Task,
184 ids: &mut std::collections::HashSet<String>,
185 ) {
186 for req_id in &task.requirements {
187 ids.insert(req_id.clone());
188 }
189 for subtask in &task.subtasks {
190 Self::collect_requirement_ids(subtask, ids);
191 }
192 }
193
194 pub fn detect_circular_dependencies(specs: &[Spec]) -> Vec<Vec<String>> {
207 let mut cycles = Vec::new();
208 let mut visited = std::collections::HashSet::new();
209 let mut rec_stack = std::collections::HashSet::new();
210
211 for spec in specs {
212 if !visited.contains(&spec.id) {
213 Self::dfs_detect_cycle(
214 &spec.id,
215 specs,
216 &mut visited,
217 &mut rec_stack,
218 &mut Vec::new(),
219 &mut cycles,
220 );
221 }
222 }
223
224 cycles
225 }
226
227 fn dfs_detect_cycle(
229 spec_id: &str,
230 all_specs: &[Spec],
231 visited: &mut std::collections::HashSet<String>,
232 rec_stack: &mut std::collections::HashSet<String>,
233 path: &mut Vec<String>,
234 cycles: &mut Vec<Vec<String>>,
235 ) {
236 visited.insert(spec_id.to_string());
237 rec_stack.insert(spec_id.to_string());
238 path.push(spec_id.to_string());
239
240 if let Some(spec) = all_specs.iter().find(|s| s.id == spec_id) {
242 let dependencies = Self::resolve_dependencies(spec, all_specs);
243
244 for dep_id in dependencies {
245 if !visited.contains(&dep_id) {
246 Self::dfs_detect_cycle(&dep_id, all_specs, visited, rec_stack, path, cycles);
247 } else if rec_stack.contains(&dep_id) {
248 if let Some(pos) = path.iter().position(|id| id == &dep_id) {
250 let cycle = path[pos..].to_vec();
251 cycles.push(cycle);
252 }
253 }
254 }
255 }
256
257 rec_stack.remove(spec_id);
258 path.pop();
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::models::{SpecMetadata, SpecPhase, SpecStatus};
266 use chrono::Utc;
267
268 fn create_test_spec(id: &str, name: &str, status: SpecStatus, phase: SpecPhase) -> Spec {
269 Spec {
270 id: id.to_string(),
271 name: name.to_string(),
272 version: "1.0.0".to_string(),
273 requirements: vec![],
274 design: None,
275 tasks: vec![],
276 metadata: SpecMetadata {
277 author: Some("Test Author".to_string()),
278 created_at: Utc::now(),
279 updated_at: Utc::now(),
280 phase,
281 status,
282 },
283 inheritance: None,
284 }
285 }
286
287 #[test]
292 fn test_query_empty_specs() {
293 let specs = vec![];
294 let query = SpecQuery::default();
295 let results = SpecQueryEngine::query(&specs, &query);
296 assert_eq!(results.len(), 0);
297 }
298
299 #[test]
300 fn test_query_no_filters() {
301 let specs = vec![
302 create_test_spec(
303 "spec-1",
304 "Feature One",
305 SpecStatus::Draft,
306 SpecPhase::Requirements,
307 ),
308 create_test_spec(
309 "spec-2",
310 "Feature Two",
311 SpecStatus::Approved,
312 SpecPhase::Design,
313 ),
314 ];
315 let query = SpecQuery::default();
316 let results = SpecQueryEngine::query(&specs, &query);
317 assert_eq!(results.len(), 2);
318 }
319
320 #[test]
321 fn test_query_by_name_exact_match() {
322 let specs = vec![
323 create_test_spec(
324 "spec-1",
325 "Feature One",
326 SpecStatus::Draft,
327 SpecPhase::Requirements,
328 ),
329 create_test_spec(
330 "spec-2",
331 "Feature Two",
332 SpecStatus::Approved,
333 SpecPhase::Design,
334 ),
335 ];
336 let query = SpecQuery {
337 name: Some("Feature One".to_string()),
338 ..Default::default()
339 };
340 let results = SpecQueryEngine::query(&specs, &query);
341 assert_eq!(results.len(), 1);
342 assert_eq!(results[0].id, "spec-1");
343 }
344
345 #[test]
346 fn test_query_by_name_partial_match() {
347 let specs = vec![
348 create_test_spec(
349 "spec-1",
350 "Feature One",
351 SpecStatus::Draft,
352 SpecPhase::Requirements,
353 ),
354 create_test_spec(
355 "spec-2",
356 "Feature Two",
357 SpecStatus::Approved,
358 SpecPhase::Design,
359 ),
360 ];
361 let query = SpecQuery {
362 name: Some("Feature".to_string()),
363 ..Default::default()
364 };
365 let results = SpecQueryEngine::query(&specs, &query);
366 assert_eq!(results.len(), 2);
367 }
368
369 #[test]
370 fn test_query_by_name_case_insensitive() {
371 let specs = vec![create_test_spec(
372 "spec-1",
373 "Feature One",
374 SpecStatus::Draft,
375 SpecPhase::Requirements,
376 )];
377 let query = SpecQuery {
378 name: Some("feature one".to_string()),
379 ..Default::default()
380 };
381 let results = SpecQueryEngine::query(&specs, &query);
382 assert_eq!(results.len(), 1);
383 }
384
385 #[test]
386 fn test_query_by_status() {
387 let specs = vec![
388 create_test_spec(
389 "spec-1",
390 "Feature One",
391 SpecStatus::Draft,
392 SpecPhase::Requirements,
393 ),
394 create_test_spec(
395 "spec-2",
396 "Feature Two",
397 SpecStatus::Approved,
398 SpecPhase::Design,
399 ),
400 ];
401 let query = SpecQuery {
402 status: Some(SpecStatus::Approved),
403 ..Default::default()
404 };
405 let results = SpecQueryEngine::query(&specs, &query);
406 assert_eq!(results.len(), 1);
407 assert_eq!(results[0].id, "spec-2");
408 }
409
410 #[test]
411 fn test_query_by_phase() {
412 let specs = vec![
413 create_test_spec(
414 "spec-1",
415 "Feature One",
416 SpecStatus::Draft,
417 SpecPhase::Requirements,
418 ),
419 create_test_spec(
420 "spec-2",
421 "Feature Two",
422 SpecStatus::Approved,
423 SpecPhase::Design,
424 ),
425 ];
426 let query = SpecQuery {
427 phase: Some(SpecPhase::Design),
428 ..Default::default()
429 };
430 let results = SpecQueryEngine::query(&specs, &query);
431 assert_eq!(results.len(), 1);
432 assert_eq!(results[0].id, "spec-2");
433 }
434
435 #[test]
436 fn test_query_by_multiple_filters() {
437 let specs = vec![
438 create_test_spec(
439 "spec-1",
440 "Feature One",
441 SpecStatus::Draft,
442 SpecPhase::Requirements,
443 ),
444 create_test_spec(
445 "spec-2",
446 "Feature Two",
447 SpecStatus::Approved,
448 SpecPhase::Design,
449 ),
450 create_test_spec(
451 "spec-3",
452 "Feature Three",
453 SpecStatus::Approved,
454 SpecPhase::Requirements,
455 ),
456 ];
457 let query = SpecQuery {
458 status: Some(SpecStatus::Approved),
459 phase: Some(SpecPhase::Design),
460 ..Default::default()
461 };
462 let results = SpecQueryEngine::query(&specs, &query);
463 assert_eq!(results.len(), 1);
464 assert_eq!(results[0].id, "spec-2");
465 }
466
467 #[test]
468 fn test_query_by_custom_filter_author() {
469 let mut spec1 = create_test_spec(
470 "spec-1",
471 "Feature One",
472 SpecStatus::Draft,
473 SpecPhase::Requirements,
474 );
475 spec1.metadata.author = Some("Alice".to_string());
476
477 let mut spec2 = create_test_spec(
478 "spec-2",
479 "Feature Two",
480 SpecStatus::Approved,
481 SpecPhase::Design,
482 );
483 spec2.metadata.author = Some("Bob".to_string());
484
485 let specs = vec![spec1, spec2];
486 let query = SpecQuery {
487 custom_filters: vec![("author".to_string(), "Alice".to_string())],
488 ..Default::default()
489 };
490 let results = SpecQueryEngine::query(&specs, &query);
491 assert_eq!(results.len(), 1);
492 assert_eq!(results[0].id, "spec-1");
493 }
494
495 #[test]
496 fn test_query_by_custom_filter_version() {
497 let mut spec1 = create_test_spec(
498 "spec-1",
499 "Feature One",
500 SpecStatus::Draft,
501 SpecPhase::Requirements,
502 );
503 spec1.version = "1.0.0".to_string();
504
505 let mut spec2 = create_test_spec(
506 "spec-2",
507 "Feature Two",
508 SpecStatus::Approved,
509 SpecPhase::Design,
510 );
511 spec2.version = "2.0.0".to_string();
512
513 let specs = vec![spec1, spec2];
514 let query = SpecQuery {
515 custom_filters: vec![("version".to_string(), "2.0".to_string())],
516 ..Default::default()
517 };
518 let results = SpecQueryEngine::query(&specs, &query);
519 assert_eq!(results.len(), 1);
520 assert_eq!(results[0].id, "spec-2");
521 }
522
523 #[test]
528 fn test_resolve_dependencies_no_dependencies() {
529 let spec = create_test_spec(
530 "spec-1",
531 "Feature One",
532 SpecStatus::Draft,
533 SpecPhase::Requirements,
534 );
535 let all_specs = vec![spec.clone()];
536 let deps = SpecQueryEngine::resolve_dependencies(&spec, &all_specs);
537 assert_eq!(deps.len(), 0);
538 }
539
540 #[test]
541 fn test_resolve_dependencies_with_task_requirements() {
542 let mut spec1 = create_test_spec(
543 "spec-1",
544 "Feature One",
545 SpecStatus::Draft,
546 SpecPhase::Requirements,
547 );
548 spec1.requirements = vec![crate::models::Requirement {
549 id: "REQ-1".to_string(),
550 user_story: "Test".to_string(),
551 acceptance_criteria: vec![],
552 priority: Priority::Must,
553 }];
554
555 let mut spec2 = create_test_spec(
556 "spec-2",
557 "Feature Two",
558 SpecStatus::Approved,
559 SpecPhase::Design,
560 );
561 spec2.tasks = vec![crate::models::Task {
562 id: "1".to_string(),
563 description: "Task 1".to_string(),
564 subtasks: vec![],
565 requirements: vec!["REQ-1".to_string()],
566 status: crate::models::TaskStatus::NotStarted,
567 optional: false,
568 }];
569
570 let all_specs = vec![spec1, spec2.clone()];
571 let deps = SpecQueryEngine::resolve_dependencies(&spec2, &all_specs);
572 assert_eq!(deps.len(), 1);
573 assert_eq!(deps[0], "spec-1");
574 }
575
576 #[test]
581 fn test_detect_circular_dependencies_no_cycles() {
582 let specs = vec![
583 create_test_spec(
584 "spec-1",
585 "Feature One",
586 SpecStatus::Draft,
587 SpecPhase::Requirements,
588 ),
589 create_test_spec(
590 "spec-2",
591 "Feature Two",
592 SpecStatus::Approved,
593 SpecPhase::Design,
594 ),
595 ];
596 let cycles = SpecQueryEngine::detect_circular_dependencies(&specs);
597 assert_eq!(cycles.len(), 0);
598 }
599
600 #[test]
601 fn test_query_consistency_multiple_executions() {
602 let specs = vec![
603 create_test_spec(
604 "spec-1",
605 "Feature One",
606 SpecStatus::Draft,
607 SpecPhase::Requirements,
608 ),
609 create_test_spec(
610 "spec-2",
611 "Feature Two",
612 SpecStatus::Approved,
613 SpecPhase::Design,
614 ),
615 ];
616 let query = SpecQuery {
617 status: Some(SpecStatus::Approved),
618 ..Default::default()
619 };
620
621 let results1 = SpecQueryEngine::query(&specs, &query);
622 let results2 = SpecQueryEngine::query(&specs, &query);
623
624 assert_eq!(results1.len(), results2.len());
625 for (r1, r2) in results1.iter().zip(results2.iter()) {
626 assert_eq!(r1.id, r2.id);
627 }
628 }
629}