use serde::{Deserialize, Serialize};
use super::http::Client;
#[derive(Debug, Deserialize, Serialize)]
pub struct FilterSummary {
pub content_hash: String,
pub command_pattern: String,
pub author: String,
pub savings_pct: f64,
pub total_commands: i64,
#[serde(default)]
pub created_at: String,
#[serde(default)]
pub test_count: i64,
#[serde(default)]
pub is_stdlib: bool,
#[serde(default)]
pub introduced_at: Option<String>,
#[serde(default)]
pub deprecated_at: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct FilterDetails {
pub content_hash: String,
pub command_pattern: String,
pub author: String,
pub savings_pct: f64,
pub total_commands: i64,
pub created_at: String,
#[serde(default)]
pub test_count: i64,
pub registry_url: String,
#[serde(default)]
pub is_stdlib: bool,
#[serde(default)]
pub introduced_at: Option<String>,
#[serde(default)]
pub deprecated_at: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct TestFilePayload {
pub filename: String,
pub content: String,
}
#[derive(Debug, Deserialize)]
pub struct DownloadedFilter {
pub filter_toml: String,
pub test_files: Vec<TestFilePayload>,
}
pub fn search_filters(
client: &Client,
query: &str,
limit: usize,
) -> anyhow::Result<Vec<FilterSummary>> {
let limit_str = limit.to_string();
client.get_with_query("/api/filters", &[("q", query), ("limit", &limit_str)])
}
pub fn get_filter(client: &Client, hash: &str) -> anyhow::Result<FilterDetails> {
client.get(&format!("/api/filters/{hash}"))
}
pub fn download_filter(client: &Client, hash: &str) -> anyhow::Result<DownloadedFilter> {
client.get(&format!("/api/filters/{hash}/download"))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn deserialize_filter_summary() {
let json = r#"{
"content_hash": "abc123def456abc123def456abc123def456abc123def456abc123def456abc1",
"command_pattern": "git push",
"author": "alice",
"savings_pct": 42.3,
"total_commands": 1234
}"#;
let summary: FilterSummary = serde_json::from_str(json).unwrap();
assert_eq!(
summary.content_hash,
"abc123def456abc123def456abc123def456abc123def456abc123def456abc1"
);
assert_eq!(summary.command_pattern, "git push");
assert_eq!(summary.author, "alice");
assert!((summary.savings_pct - 42.3).abs() < 0.001);
assert_eq!(summary.total_commands, 1234);
assert_eq!(summary.created_at, "", "created_at defaults to empty");
}
#[test]
fn deserialize_filter_summary_with_created_at() {
let json = r#"{
"content_hash": "abc123",
"command_pattern": "git push",
"author": "alice",
"savings_pct": 0.0,
"total_commands": 0,
"created_at": "2026-02-26T00:00:00"
}"#;
let summary: FilterSummary = serde_json::from_str(json).unwrap();
assert_eq!(summary.created_at, "2026-02-26T00:00:00");
}
#[test]
fn deserialize_downloaded_filter() {
let json = r#"{
"filter_toml": "command = \"git push\"\n",
"test_files": [
{"filename": "basic.toml", "content": "name = \"basic\"\n"},
{"filename": "edge.toml", "content": "name = \"edge\"\n"}
]
}"#;
let dl: DownloadedFilter = serde_json::from_str(json).unwrap();
assert!(dl.filter_toml.contains("git push"));
assert_eq!(dl.test_files.len(), 2);
assert_eq!(dl.test_files[0].filename, "basic.toml");
assert_eq!(dl.test_files[1].filename, "edge.toml");
}
#[test]
fn deserialize_downloaded_filter_no_tests() {
let json = r#"{"filter_toml": "command = \"cargo build\"\n", "test_files": []}"#;
let dl: DownloadedFilter = serde_json::from_str(json).unwrap();
assert!(dl.test_files.is_empty());
}
#[test]
fn deserialize_filter_summary_with_is_stdlib() {
let json = r#"{
"content_hash": "abc123",
"command_pattern": "git push",
"author": "alice",
"savings_pct": 0.0,
"total_commands": 0,
"is_stdlib": true
}"#;
let summary: FilterSummary = serde_json::from_str(json).unwrap();
assert!(summary.is_stdlib);
}
#[test]
fn deserialize_filter_summary_with_test_count() {
let json = r#"{
"content_hash": "abc123",
"command_pattern": "git push",
"author": "alice",
"savings_pct": 0.0,
"total_commands": 0,
"test_count": 5
}"#;
let summary: FilterSummary = serde_json::from_str(json).unwrap();
assert_eq!(summary.test_count, 5);
}
#[test]
fn deserialize_filter_summary_test_count_defaults_zero() {
let json = r#"{
"content_hash": "abc123",
"command_pattern": "git push",
"author": "alice",
"savings_pct": 0.0,
"total_commands": 0
}"#;
let summary: FilterSummary = serde_json::from_str(json).unwrap();
assert_eq!(summary.test_count, 0);
}
#[test]
fn deserialize_filter_details_test_count_defaults_zero() {
let json = r#"{
"content_hash": "abc123",
"command_pattern": "git push",
"author": "alice",
"savings_pct": 0.0,
"total_commands": 0,
"created_at": "2026-01-01T00:00:00",
"registry_url": "https://tokf.net/filters/abc123"
}"#;
let details: FilterDetails = serde_json::from_str(json).unwrap();
assert_eq!(details.test_count, 0, "test_count should default to 0");
}
#[test]
fn deserialize_filter_summary_with_version_fields() {
let json = r#"{
"content_hash": "abc123",
"command_pattern": "git push",
"author": "alice",
"savings_pct": 0.0,
"total_commands": 0,
"introduced_at": "0.2.3",
"deprecated_at": "0.3.0"
}"#;
let summary: FilterSummary = serde_json::from_str(json).unwrap();
assert_eq!(summary.introduced_at.as_deref(), Some("0.2.3"));
assert_eq!(summary.deprecated_at.as_deref(), Some("0.3.0"));
}
#[test]
fn deserialize_filter_summary_version_fields_default_none() {
let json = r#"{
"content_hash": "abc123",
"command_pattern": "git push",
"author": "alice",
"savings_pct": 0.0,
"total_commands": 0
}"#;
let summary: FilterSummary = serde_json::from_str(json).unwrap();
assert!(summary.introduced_at.is_none());
assert!(summary.deprecated_at.is_none());
}
#[test]
fn deserialize_filter_details_with_version_fields() {
let json = r#"{
"content_hash": "abc123",
"command_pattern": "git push",
"author": "alice",
"savings_pct": 0.0,
"total_commands": 0,
"created_at": "2026-01-01T00:00:00",
"registry_url": "https://tokf.net/filters/abc123",
"introduced_at": "0.2.3",
"deprecated_at": "0.3.0"
}"#;
let details: FilterDetails = serde_json::from_str(json).unwrap();
assert_eq!(details.introduced_at.as_deref(), Some("0.2.3"));
assert_eq!(details.deprecated_at.as_deref(), Some("0.3.0"));
}
#[test]
fn deserialize_filter_summary_is_stdlib_defaults_false() {
let json = r#"{
"content_hash": "abc123",
"command_pattern": "git push",
"author": "alice",
"savings_pct": 0.0,
"total_commands": 0
}"#;
let summary: FilterSummary = serde_json::from_str(json).unwrap();
assert!(!summary.is_stdlib);
}
}