feat: AND/OR filter engine for Tuleap artifact filtering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1e69cf16a9
commit
6e1356ba1b
2 changed files with 213 additions and 0 deletions
212
src-tauri/src/services/filter_engine.rs
Normal file
212
src-tauri/src/services/filter_engine.rs
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
use crate::models::tracker::{Filter, FilterGroup};
|
||||||
|
use crate::services::tuleap_client::extract_artifact_field_values;
|
||||||
|
|
||||||
|
pub fn apply_filters(
|
||||||
|
artifacts: &[serde_json::Value],
|
||||||
|
filter_groups: &[FilterGroup],
|
||||||
|
) -> Vec<serde_json::Value> {
|
||||||
|
if filter_groups.is_empty() {
|
||||||
|
return artifacts.to_vec();
|
||||||
|
}
|
||||||
|
artifacts
|
||||||
|
.iter()
|
||||||
|
.filter(|a| matches_all_groups(a, filter_groups))
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_all_groups(artifact: &serde_json::Value, groups: &[FilterGroup]) -> bool {
|
||||||
|
groups.iter().all(|g| matches_any_condition(artifact, &g.conditions))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_any_condition(artifact: &serde_json::Value, conditions: &[Filter]) -> bool {
|
||||||
|
if conditions.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
conditions.iter().any(|c| matches_condition(artifact, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_condition(artifact: &serde_json::Value, condition: &Filter) -> bool {
|
||||||
|
let field_values = extract_artifact_field_values(artifact, &condition.field);
|
||||||
|
match condition.operator.as_str() {
|
||||||
|
"Equals" => {
|
||||||
|
condition.value.len() == 1
|
||||||
|
&& field_values.iter().any(|v| v == &condition.value[0])
|
||||||
|
}
|
||||||
|
"NotEquals" => {
|
||||||
|
condition.value.len() == 1
|
||||||
|
&& !field_values.iter().any(|v| v == &condition.value[0])
|
||||||
|
}
|
||||||
|
"In" => field_values.iter().any(|v| condition.value.contains(v)),
|
||||||
|
"NotIn" => !field_values.iter().any(|v| condition.value.contains(v)),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
fn make_artifact(status: &str, assigned: &str, priority: &str) -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"id": 123,
|
||||||
|
"title": "Test ticket",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"field_id": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"type": "sb",
|
||||||
|
"values": [{ "id": 1, "label": status }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field_id": 2,
|
||||||
|
"label": "Assigned to",
|
||||||
|
"type": "msb",
|
||||||
|
"bind_value_objects": [{ "id": 2, "display_name": assigned }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field_id": 3,
|
||||||
|
"label": "Priority",
|
||||||
|
"type": "sb",
|
||||||
|
"values": [{ "id": 3, "label": priority }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_filters_returns_all() {
|
||||||
|
let artifacts = vec![
|
||||||
|
make_artifact("Nouveau", "Alice", "Haute"),
|
||||||
|
make_artifact("Ferme", "Bob", "Basse"),
|
||||||
|
];
|
||||||
|
let result = apply_filters(&artifacts, &[]);
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_single_in_filter() {
|
||||||
|
let artifacts = vec![
|
||||||
|
make_artifact("Nouveau", "Alice", "Haute"),
|
||||||
|
make_artifact("Ferme", "Bob", "Basse"),
|
||||||
|
];
|
||||||
|
let groups = vec![FilterGroup {
|
||||||
|
conditions: vec![Filter {
|
||||||
|
field: "Status".to_string(),
|
||||||
|
operator: "In".to_string(),
|
||||||
|
value: vec!["Nouveau".to_string()],
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
let result = apply_filters(&artifacts, &groups);
|
||||||
|
assert_eq!(result.len(), 1);
|
||||||
|
assert_eq!(result[0]["values"][0]["values"][0]["label"], "Nouveau");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_or_within_group() {
|
||||||
|
let artifacts = vec![
|
||||||
|
make_artifact("Nouveau", "Alice", "Haute"),
|
||||||
|
make_artifact("A traiter", "Bob", "Moyenne"),
|
||||||
|
make_artifact("Ferme", "Carol", "Basse"),
|
||||||
|
];
|
||||||
|
let groups = vec![FilterGroup {
|
||||||
|
conditions: vec![
|
||||||
|
Filter {
|
||||||
|
field: "Status".to_string(),
|
||||||
|
operator: "Equals".to_string(),
|
||||||
|
value: vec!["Nouveau".to_string()],
|
||||||
|
},
|
||||||
|
Filter {
|
||||||
|
field: "Status".to_string(),
|
||||||
|
operator: "Equals".to_string(),
|
||||||
|
value: vec!["A traiter".to_string()],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
let result = apply_filters(&artifacts, &groups);
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_and_across_groups() {
|
||||||
|
let artifacts = vec![
|
||||||
|
make_artifact("Nouveau", "Team Maintenance", "Haute"),
|
||||||
|
make_artifact("A traiter", "Other Team", "Moyenne"),
|
||||||
|
make_artifact("Ferme", "Team Maintenance", "Basse"),
|
||||||
|
];
|
||||||
|
let groups = vec![
|
||||||
|
FilterGroup {
|
||||||
|
conditions: vec![Filter {
|
||||||
|
field: "Status".to_string(),
|
||||||
|
operator: "In".to_string(),
|
||||||
|
value: vec!["Nouveau".to_string(), "A traiter".to_string()],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
FilterGroup {
|
||||||
|
conditions: vec![Filter {
|
||||||
|
field: "Assigned to".to_string(),
|
||||||
|
operator: "In".to_string(),
|
||||||
|
value: vec!["Team Maintenance".to_string()],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let result = apply_filters(&artifacts, &groups);
|
||||||
|
assert_eq!(result.len(), 1);
|
||||||
|
assert_eq!(result[0]["values"][0]["values"][0]["label"], "Nouveau");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_not_in_filter() {
|
||||||
|
let artifacts = vec![
|
||||||
|
make_artifact("Nouveau", "Alice", "Haute"),
|
||||||
|
make_artifact("Ferme", "Bob", "Basse"),
|
||||||
|
make_artifact("Ferme", "Carol", "Moyenne"),
|
||||||
|
];
|
||||||
|
let groups = vec![FilterGroup {
|
||||||
|
conditions: vec![Filter {
|
||||||
|
field: "Status".to_string(),
|
||||||
|
operator: "NotIn".to_string(),
|
||||||
|
value: vec!["Ferme".to_string()],
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
let result = apply_filters(&artifacts, &groups);
|
||||||
|
assert_eq!(result.len(), 1);
|
||||||
|
assert_eq!(result[0]["values"][0]["values"][0]["label"], "Nouveau");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_equals_filter() {
|
||||||
|
let artifacts = vec![
|
||||||
|
make_artifact("Nouveau", "Alice", "Haute"),
|
||||||
|
make_artifact("Nouveau", "Bob", "Basse"),
|
||||||
|
make_artifact("Ferme", "Carol", "Haute"),
|
||||||
|
];
|
||||||
|
let groups = vec![FilterGroup {
|
||||||
|
conditions: vec![Filter {
|
||||||
|
field: "Priority".to_string(),
|
||||||
|
operator: "Equals".to_string(),
|
||||||
|
value: vec!["Haute".to_string()],
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
let result = apply_filters(&artifacts, &groups);
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_match_returns_empty() {
|
||||||
|
let artifacts = vec![
|
||||||
|
make_artifact("Nouveau", "Alice", "Haute"),
|
||||||
|
make_artifact("A traiter", "Bob", "Moyenne"),
|
||||||
|
];
|
||||||
|
let groups = vec![FilterGroup {
|
||||||
|
conditions: vec![Filter {
|
||||||
|
field: "Status".to_string(),
|
||||||
|
operator: "Equals".to_string(),
|
||||||
|
value: vec!["Ferme".to_string()],
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
let result = apply_filters(&artifacts, &groups);
|
||||||
|
assert_eq!(result.len(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
|
pub mod filter_engine;
|
||||||
pub mod tuleap_client;
|
pub mod tuleap_client;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue