1use serde::{Deserialize, Serialize};
6
7use crate::names::{ItemName, RenameMap};
8
9fn is_false(v: &bool) -> bool {
10 !*v
11}
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
15pub struct FilterConfig {
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub agents: Option<Vec<ItemName>>,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub skills: Option<Vec<ItemName>>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub exclude: Option<Vec<ItemName>>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub rename: Option<RenameMap>,
24 #[serde(default, skip_serializing_if = "is_false")]
25 pub only_skills: bool,
26 #[serde(default, skip_serializing_if = "is_false")]
27 pub only_agents: bool,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "kebab-case")]
33pub enum FilterMode {
34 All,
36 Include {
38 agents: Vec<ItemName>,
39 skills: Vec<ItemName>,
40 },
41 Exclude(Vec<ItemName>),
43 OnlySkills,
45 OnlyAgents,
47}
48
49#[derive(Debug, thiserror::Error, PartialEq, Eq)]
51pub enum FilterError {
52 #[error("dependency `{name}`: only_skills and only_agents are mutually exclusive")]
53 BothCategories { name: String },
54 #[error("dependency `{name}`: only_skills/only_agents cannot combine with agents/skills lists")]
55 CategoryWithInclude { name: String },
56 #[error("dependency `{name}`: only_skills/only_agents cannot combine with exclude")]
57 CategoryWithExclude { name: String },
58 #[error("source `{name}` uses both agents/skills and exclude — pick one")]
59 IncludeWithExclude { name: String },
60}
61
62impl FilterError {
63 pub fn code(&self) -> &'static str {
65 "filter.inconsistent"
66 }
67}
68
69impl FilterConfig {
70 pub fn validate(&self, dep_name: &str) -> Result<(), FilterError> {
73 let name = || dep_name.to_string();
74 let has_include = self.agents.is_some() || self.skills.is_some();
75 let has_exclude = self.exclude.is_some();
76 let has_category = self.only_skills || self.only_agents;
77
78 if self.only_skills && self.only_agents {
79 return Err(FilterError::BothCategories { name: name() });
80 }
81 if has_category && has_include {
82 return Err(FilterError::CategoryWithInclude { name: name() });
83 }
84 if has_category && has_exclude {
85 return Err(FilterError::CategoryWithExclude { name: name() });
86 }
87 if has_include && has_exclude {
88 return Err(FilterError::IncludeWithExclude { name: name() });
89 }
90 Ok(())
91 }
92
93 pub fn to_mode(&self) -> FilterMode {
95 if self.only_skills {
96 FilterMode::OnlySkills
97 } else if self.only_agents {
98 FilterMode::OnlyAgents
99 } else if self.agents.is_some() || self.skills.is_some() {
100 FilterMode::Include {
101 agents: self.agents.clone().unwrap_or_default(),
102 skills: self.skills.clone().unwrap_or_default(),
103 }
104 } else if self.exclude.is_some() {
105 FilterMode::Exclude(self.exclude.clone().unwrap_or_default())
106 } else {
107 FilterMode::All
108 }
109 }
110
111 pub fn has_any_filter(&self) -> bool {
113 self.agents.is_some()
114 || self.skills.is_some()
115 || self.exclude.is_some()
116 || self.only_skills
117 || self.only_agents
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn filter_config_has_any_filter() {
127 assert!(!FilterConfig::default().has_any_filter());
128 assert!(
129 FilterConfig {
130 only_skills: true,
131 ..FilterConfig::default()
132 }
133 .has_any_filter()
134 );
135 assert!(
136 FilterConfig {
137 agents: Some(vec!["coder".into()]),
138 ..FilterConfig::default()
139 }
140 .has_any_filter()
141 );
142 }
143
144 #[test]
145 fn filter_config_to_mode() {
146 assert!(matches!(FilterConfig::default().to_mode(), FilterMode::All));
147 assert!(matches!(
148 FilterConfig {
149 only_skills: true,
150 ..FilterConfig::default()
151 }
152 .to_mode(),
153 FilterMode::OnlySkills
154 ));
155 assert!(matches!(
156 FilterConfig {
157 only_agents: true,
158 ..FilterConfig::default()
159 }
160 .to_mode(),
161 FilterMode::OnlyAgents
162 ));
163 assert!(matches!(
164 FilterConfig {
165 agents: Some(vec!["coder".into()]),
166 ..FilterConfig::default()
167 }
168 .to_mode(),
169 FilterMode::Include { .. }
170 ));
171 assert!(matches!(
172 FilterConfig {
173 exclude: Some(vec!["old".into()]),
174 ..FilterConfig::default()
175 }
176 .to_mode(),
177 FilterMode::Exclude(_)
178 ));
179 }
180}