use crate::api::ApiResponse;
use serde_json::Value;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum FilterError {
#[error("Invalid filter format: {0}")]
InvalidFormat(String),
#[error("Invalid boolean value: {0}")]
InvalidBoolean(String),
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConversationFilter {
Name(String),
IsMember(bool),
IsPrivate(bool),
}
impl ConversationFilter {
pub fn parse(s: &str) -> Result<Self, FilterError> {
let parts: Vec<&str> = s.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(FilterError::InvalidFormat(format!(
"Expected format 'key:value', got '{}'",
s
)));
}
match parts[0] {
"name" => Ok(ConversationFilter::Name(parts[1].to_string())),
"is_member" => {
let value = parts[1].parse::<bool>().map_err(|_| {
FilterError::InvalidBoolean(format!(
"Expected 'true' or 'false', got '{}'",
parts[1]
))
})?;
Ok(ConversationFilter::IsMember(value))
}
"is_private" => {
let value = parts[1].parse::<bool>().map_err(|_| {
FilterError::InvalidBoolean(format!(
"Expected 'true' or 'false', got '{}'",
parts[1]
))
})?;
Ok(ConversationFilter::IsPrivate(value))
}
_ => Err(FilterError::InvalidFormat(format!(
"Unknown filter key: {}",
parts[0]
))),
}
}
pub fn matches(&self, conv: &Value) -> bool {
match self {
ConversationFilter::Name(pattern) => {
if let Some(name) = conv.get("name").and_then(|v| v.as_str()) {
glob_match(pattern, name)
} else {
false
}
}
ConversationFilter::IsMember(expected) => {
if let Some(is_member) = conv.get("is_member").and_then(|v| v.as_bool()) {
is_member == *expected
} else {
false
}
}
ConversationFilter::IsPrivate(expected) => {
if let Some(is_private) = conv.get("is_private").and_then(|v| v.as_bool()) {
is_private == *expected
} else {
false
}
}
}
}
}
fn glob_match(pattern: &str, text: &str) -> bool {
if pattern == "*" {
return true;
}
if !pattern.contains('*') {
return text.to_lowercase().contains(&pattern.to_lowercase());
}
let pattern_parts: Vec<&str> = pattern.split('*').collect();
if pattern.starts_with('*') && pattern_parts.len() == 2 && pattern_parts[1].is_empty() {
return true; }
if pattern.ends_with('*') && pattern_parts.len() == 2 && pattern_parts[0].is_empty() {
return true; }
let mut text_pos = 0;
for (i, part) in pattern_parts.iter().enumerate() {
if part.is_empty() {
continue;
}
if i == 0 && !pattern.starts_with('*') {
if !text[text_pos..].starts_with(part) {
return false;
}
text_pos += part.len();
} else if i == pattern_parts.len() - 1 && !pattern.ends_with('*') {
if !text.ends_with(part) {
return false;
}
} else {
if let Some(pos) = text[text_pos..].find(part) {
text_pos += pos + part.len();
} else {
return false;
}
}
}
true
}
pub fn apply_filters(response: &mut ApiResponse, filters: &[ConversationFilter]) {
if filters.is_empty() {
return;
}
if let Some(channels) = response.data.get_mut("channels") {
if let Some(channels_array) = channels.as_array_mut() {
channels_array.retain(|conv| filters.iter().all(|filter| filter.matches(conv)));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::collections::HashMap;
#[test]
fn test_filter_parse_name() {
let filter = ConversationFilter::parse("name:test*").unwrap();
assert_eq!(filter, ConversationFilter::Name("test*".to_string()));
}
#[test]
fn test_filter_parse_is_member() {
let filter = ConversationFilter::parse("is_member:true").unwrap();
assert_eq!(filter, ConversationFilter::IsMember(true));
let filter = ConversationFilter::parse("is_member:false").unwrap();
assert_eq!(filter, ConversationFilter::IsMember(false));
}
#[test]
fn test_filter_parse_is_private() {
let filter = ConversationFilter::parse("is_private:true").unwrap();
assert_eq!(filter, ConversationFilter::IsPrivate(true));
let filter = ConversationFilter::parse("is_private:false").unwrap();
assert_eq!(filter, ConversationFilter::IsPrivate(false));
}
#[test]
fn test_filter_parse_invalid_format() {
let result = ConversationFilter::parse("invalid");
assert!(result.is_err());
}
#[test]
fn test_filter_parse_invalid_key() {
let result = ConversationFilter::parse("unknown:value");
assert!(result.is_err());
}
#[test]
fn test_filter_parse_invalid_boolean() {
let result = ConversationFilter::parse("is_member:maybe");
assert!(result.is_err());
}
#[test]
fn test_glob_match_case_insensitive_substring() {
assert!(glob_match("test", "test"));
assert!(glob_match("test", "TEST"));
assert!(glob_match("test", "Test"));
assert!(glob_match("test", "mytest"));
assert!(glob_match("test", "testing"));
assert!(glob_match("test", "mytestchannel"));
assert!(!glob_match("test", "other"));
}
#[test]
fn test_glob_match_wildcard() {
assert!(glob_match("*", "anything"));
assert!(glob_match("test*", "test"));
assert!(glob_match("test*", "test123"));
assert!(!glob_match("test*", "other"));
assert!(glob_match("*test", "test"));
assert!(glob_match("*test", "mytest"));
assert!(!glob_match("*test", "testing"));
assert!(glob_match("*test*", "test"));
assert!(glob_match("*test*", "mytest123"));
assert!(!glob_match("*test*", "other"));
}
#[test]
fn test_filter_matches_name() {
let filter = ConversationFilter::Name("general".to_string());
let conv = json!({"name": "general", "id": "C123"});
assert!(filter.matches(&conv));
let conv = json!({"name": "GENERAL", "id": "C124"});
assert!(filter.matches(&conv));
let conv = json!({"name": "my-general-channel", "id": "C125"});
assert!(filter.matches(&conv));
let conv = json!({"name": "random", "id": "C126"});
assert!(!filter.matches(&conv));
}
#[test]
fn test_filter_matches_name_glob() {
let filter = ConversationFilter::Name("test*".to_string());
let conv = json!({"name": "test-channel", "id": "C123"});
assert!(filter.matches(&conv));
let conv = json!({"name": "other", "id": "C124"});
assert!(!filter.matches(&conv));
}
#[test]
fn test_filter_matches_is_member() {
let filter = ConversationFilter::IsMember(true);
let conv = json!({"name": "general", "is_member": true});
assert!(filter.matches(&conv));
let conv = json!({"name": "general", "is_member": false});
assert!(!filter.matches(&conv));
}
#[test]
fn test_filter_matches_is_private() {
let filter = ConversationFilter::IsPrivate(true);
let conv = json!({"name": "private", "is_private": true});
assert!(filter.matches(&conv));
let conv = json!({"name": "public", "is_private": false});
assert!(!filter.matches(&conv));
}
#[test]
fn test_apply_filters_and_condition() {
let mut response = ApiResponse {
ok: true,
data: HashMap::from([(
"channels".to_string(),
json!([
{"id": "C1", "name": "test-public", "is_member": true, "is_private": false},
{"id": "C2", "name": "test-private", "is_member": true, "is_private": true},
{"id": "C3", "name": "other", "is_member": true, "is_private": false},
{"id": "C4", "name": "test-nomember", "is_member": false, "is_private": false},
]),
)]),
error: None,
};
let filters = vec![
ConversationFilter::Name("test*".to_string()),
ConversationFilter::IsMember(true),
];
apply_filters(&mut response, &filters);
let channels = response.data.get("channels").unwrap().as_array().unwrap();
assert_eq!(channels.len(), 2); assert_eq!(channels[0].get("id").unwrap().as_str().unwrap(), "C1");
assert_eq!(channels[1].get("id").unwrap().as_str().unwrap(), "C2");
}
}