1use std::collections::BTreeSet;
36use std::str::FromStr;
37
38use crate::ip::network::{Network, NetworkObj, NetworkObjGroup};
39use crate::service::{ApplicationObj, ServiceObj, ServiceObjGroup, TransportService};
40
41#[derive(Debug, Clone)]
44pub enum BuilderEntry {
45 Network(NetworkObj),
46 NetworkGroup(NetworkObjGroup),
47 Service(ServiceObj),
48 ServiceGroup(ServiceObjGroup),
49 Application(ApplicationObj),
50}
51
52impl From<NetworkObj> for BuilderEntry {
53 fn from(obj: NetworkObj) -> Self {
54 BuilderEntry::Network(obj)
55 }
56}
57
58impl From<NetworkObjGroup> for BuilderEntry {
59 fn from(group: NetworkObjGroup) -> Self {
60 BuilderEntry::NetworkGroup(group)
61 }
62}
63
64impl From<ServiceObj> for BuilderEntry {
65 fn from(obj: ServiceObj) -> Self {
66 BuilderEntry::Service(obj)
67 }
68}
69
70impl From<ServiceObjGroup> for BuilderEntry {
71 fn from(group: ServiceObjGroup) -> Self {
72 BuilderEntry::ServiceGroup(group)
73 }
74}
75
76impl From<ApplicationObj> for BuilderEntry {
77 fn from(app: ApplicationObj) -> Self {
78 BuilderEntry::Application(app)
79 }
80}
81
82pub fn address(name: &str, value: &str) -> Result<NetworkObj, String> {
85 let network = Network::new(value)?;
86 let trimmed = name.trim();
87 let final_name = if trimmed.is_empty() {
88 network.to_string()
89 } else {
90 trimmed.to_string()
91 };
92 Ok(NetworkObj::new(final_name, network))
93}
94
95pub fn network_group(name: &str) -> Result<NetworkGroupBuilder, String> {
97 NetworkGroupBuilder::new(name)
98}
99
100pub fn service_group(name: &str) -> Result<ServiceGroupBuilder, String> {
102 ServiceGroupBuilder::new(name)
103}
104
105pub fn application(name: &str, category: &str) -> Result<ApplicationBuilder, String> {
107 ApplicationBuilder::new(name, category)
108}
109
110#[derive(Debug, Clone)]
112pub struct NetworkGroupBuilder {
113 name: String,
114 members: BTreeSet<NetworkObj>,
115}
116
117impl NetworkGroupBuilder {
118 pub fn new(name: &str) -> Result<Self, String> {
119 let trimmed = name.trim();
120 if trimmed.is_empty() {
121 return Err("network group name cannot be empty".into());
122 }
123 Ok(Self {
124 name: trimmed.to_string(),
125 members: BTreeSet::new(),
126 })
127 }
128
129 pub fn add<N>(mut self, entry: N) -> Result<Self, String>
131 where
132 N: IntoNetworkObj,
133 {
134 let obj = entry.into_network_obj();
135 if self.members.iter().any(|existing| existing.name == obj.name) {
136 return Err(format!(
137 "network object '{}' already exists in group '{}'",
138 obj.name, self.name
139 ));
140 }
141 self.members.insert(obj);
142 Ok(self)
143 }
144
145 pub fn build(self) -> Result<NetworkObjGroup, String> {
146 NetworkObjGroup::new(&self.name, self.members)
147 }
148}
149
150#[derive(Debug, Clone)]
152pub struct ServiceGroupBuilder {
153 name: String,
154 members: BTreeSet<ServiceObj>,
155}
156
157impl ServiceGroupBuilder {
158 pub fn new(name: &str) -> Result<Self, String> {
159 let trimmed = name.trim();
160 if trimmed.is_empty() {
161 return Err("service group name cannot be empty".into());
162 }
163 Ok(Self {
164 name: trimmed.to_string(),
165 members: BTreeSet::new(),
166 })
167 }
168
169 pub fn add<S>(mut self, entry: S) -> Result<Self, String>
171 where
172 S: IntoServiceObj,
173 {
174 let obj = entry.into_service_obj();
175 if self.members.iter().any(|existing| existing.name == obj.name) {
176 return Err(format!(
177 "service object '{}' already exists in group '{}'",
178 obj.name, self.name
179 ));
180 }
181 self.members.insert(obj);
182 Ok(self)
183 }
184
185 pub fn build(self) -> Result<ServiceObjGroup, String> {
186 ServiceObjGroup::new(&self.name, self.members)
187 }
188}
189
190pub trait IntoNetworkObj {
192 fn into_network_obj(self) -> NetworkObj;
193}
194
195impl IntoNetworkObj for NetworkObj {
196 fn into_network_obj(self) -> NetworkObj {
197 self
198 }
199}
200
201pub trait IntoServiceObj {
203 fn into_service_obj(self) -> ServiceObj;
204}
205
206impl IntoServiceObj for ServiceObj {
207 fn into_service_obj(self) -> ServiceObj {
208 self
209 }
210}
211
212pub mod service {
214 use super::*;
215
216 pub fn tcp(port: u16) -> ServiceObj {
218 ServiceObj::new(format!("tcp/{port}"), TransportService::tcp(port))
219 }
220
221 pub fn udp(port: u16) -> ServiceObj {
223 ServiceObj::new(format!("udp/{port}"), TransportService::udp(port))
224 }
225
226 pub fn icmpv4(ty: u8, code: Option<u8>) -> ServiceObj {
228 let svc = TransportService::icmp(crate::service::icmp::IcmpVersion::V4, ty, code);
229 ServiceObj::new(svc.to_string(), svc)
230 }
231
232 pub fn icmpv6(ty: u8, code: Option<u8>) -> ServiceObj {
234 let svc = TransportService::icmp(crate::service::icmp::IcmpVersion::V6, ty, code);
235 ServiceObj::new(svc.to_string(), svc)
236 }
237
238 pub fn parse(definition: &str) -> Result<ServiceObj, String> {
240 let svc = TransportService::from_str(definition)?;
241 let name = definition.trim();
242 let final_name = if name.is_empty() {
243 svc.to_string()
244 } else {
245 name.to_string()
246 };
247 Ok(ServiceObj::new(final_name, svc))
248 }
249}
250
251#[derive(Debug, Clone)]
253pub struct ApplicationBuilder {
254 name: String,
255 category: String,
256 transports: Vec<TransportService>,
257 dns_suffixes: Vec<String>,
258 tls_sni_suffixes: Vec<String>,
259 http_hosts: Vec<String>,
260}
261
262impl ApplicationBuilder {
263 pub fn new(name: &str, category: &str) -> Result<Self, String> {
264 let name = name.trim();
265 let category = category.trim();
266
267 if name.is_empty() {
268 return Err("application name cannot be empty".into());
269 }
270 if category.is_empty() {
271 return Err("application category cannot be empty".into());
272 }
273
274 Ok(Self {
275 name: name.to_string(),
276 category: category.to_string(),
277 transports: Vec::new(),
278 dns_suffixes: Vec::new(),
279 tls_sni_suffixes: Vec::new(),
280 http_hosts: Vec::new(),
281 })
282 }
283
284 pub fn transport(mut self, descriptor: &str) -> Result<Self, String> {
286 self.transports.push(TransportService::from_str(descriptor)?);
287 Ok(self)
288 }
289
290 pub fn transport_value(mut self, svc: TransportService) -> Self {
292 self.transports.push(svc);
293 self
294 }
295
296 pub fn dns_suffix<S: Into<String>>(mut self, suffix: S) -> Self {
297 self.dns_suffixes.push(suffix.into());
298 self
299 }
300
301 pub fn tls_sni_suffix<S: Into<String>>(mut self, suffix: S) -> Self {
302 self.tls_sni_suffixes.push(suffix.into());
303 self
304 }
305
306 pub fn http_host<S: Into<String>>(mut self, host: S) -> Self {
307 self.http_hosts.push(host.into());
308 self
309 }
310
311 pub fn build(self) -> ApplicationObj {
312 ApplicationObj {
313 name: self.name,
314 category: self.category,
315 transports: self.transports,
316 dns_suffixes: self.dns_suffixes,
317 tls_sni_suffixes: self.tls_sni_suffixes,
318 http_hosts: self.http_hosts,
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn address_defaults_name_when_empty() {
329 let obj = address("", "192.0.2.10").unwrap();
330 assert_eq!(obj.name, "192.0.2.10");
331 }
332
333 #[test]
334 fn service_group_builder_rejects_duplicates() {
335 let service = service::tcp(443);
336 let err = service_group("web")
337 .unwrap()
338 .add(service.clone())
339 .unwrap()
340 .add(service)
341 .unwrap_err();
342 assert!(err.contains("web"));
343 }
344
345 #[test]
346 fn application_builder_collects_values() {
347 let app = application("github", "developer")
348 .unwrap()
349 .transport("tcp/443")
350 .unwrap()
351 .dns_suffix(".github.com")
352 .http_host("github.com")
353 .build();
354
355 assert_eq!(app.name, "github");
356 assert_eq!(app.category, "developer");
357 assert_eq!(app.transports.len(), 1);
358 assert_eq!(app.dns_suffixes, vec![".github.com"]);
359 assert_eq!(app.http_hosts, vec!["github.com"]);
360 }
361}