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 with_network<N>(mut self, entry: N) -> Result<Self, String>
131 where
132 N: IntoNetworkObj,
133 {
134 let obj = entry.into_network_obj();
135 if self
136 .members
137 .iter()
138 .any(|existing| existing.name == obj.name)
139 {
140 return Err(format!(
141 "network object '{}' already exists in group '{}'",
142 obj.name, self.name
143 ));
144 }
145 self.members.insert(obj);
146 Ok(self)
147 }
148
149 pub fn build(self) -> Result<NetworkObjGroup, String> {
150 NetworkObjGroup::new(&self.name, self.members)
151 }
152}
153
154#[derive(Debug, Clone)]
156pub struct ServiceGroupBuilder {
157 name: String,
158 members: BTreeSet<ServiceObj>,
159}
160
161impl ServiceGroupBuilder {
162 pub fn new(name: &str) -> Result<Self, String> {
163 let trimmed = name.trim();
164 if trimmed.is_empty() {
165 return Err("service group name cannot be empty".into());
166 }
167 Ok(Self {
168 name: trimmed.to_string(),
169 members: BTreeSet::new(),
170 })
171 }
172
173 pub fn with_service<S>(mut self, entry: S) -> Result<Self, String>
175 where
176 S: IntoServiceObj,
177 {
178 let obj = entry.into_service_obj();
179 if self
180 .members
181 .iter()
182 .any(|existing| existing.name == obj.name)
183 {
184 return Err(format!(
185 "service object '{}' already exists in group '{}'",
186 obj.name, self.name
187 ));
188 }
189 self.members.insert(obj);
190 Ok(self)
191 }
192
193 pub fn build(self) -> Result<ServiceObjGroup, String> {
194 ServiceObjGroup::new(&self.name, self.members)
195 }
196}
197
198pub trait IntoNetworkObj {
200 fn into_network_obj(self) -> NetworkObj;
201}
202
203impl IntoNetworkObj for NetworkObj {
204 fn into_network_obj(self) -> NetworkObj {
205 self
206 }
207}
208
209pub trait IntoServiceObj {
211 fn into_service_obj(self) -> ServiceObj;
212}
213
214impl IntoServiceObj for ServiceObj {
215 fn into_service_obj(self) -> ServiceObj {
216 self
217 }
218}
219
220pub mod service {
222 use super::*;
223
224 pub fn tcp(port: u16) -> ServiceObj {
226 ServiceObj::new(format!("tcp/{port}"), TransportService::tcp(port))
227 }
228
229 pub fn udp(port: u16) -> ServiceObj {
231 ServiceObj::new(format!("udp/{port}"), TransportService::udp(port))
232 }
233
234 pub fn icmpv4(ty: u8, code: Option<u8>) -> ServiceObj {
236 let svc = TransportService::icmp(crate::service::icmp::IcmpVersion::V4, ty, code);
237 ServiceObj::new(svc.to_string(), svc)
238 }
239
240 pub fn icmpv6(ty: u8, code: Option<u8>) -> ServiceObj {
242 let svc = TransportService::icmp(crate::service::icmp::IcmpVersion::V6, ty, code);
243 ServiceObj::new(svc.to_string(), svc)
244 }
245
246 pub fn parse(definition: &str) -> Result<ServiceObj, String> {
248 let svc = TransportService::from_str(definition)?;
249 let name = definition.trim();
250 let final_name = if name.is_empty() {
251 svc.to_string()
252 } else {
253 name.to_string()
254 };
255 Ok(ServiceObj::new(final_name, svc))
256 }
257}
258
259#[derive(Debug, Clone)]
261pub struct ApplicationBuilder {
262 name: String,
263 category: String,
264 transports: Vec<TransportService>,
265 dns_suffixes: Vec<String>,
266 tls_sni_suffixes: Vec<String>,
267 http_hosts: Vec<String>,
268}
269
270impl ApplicationBuilder {
271 pub fn new(name: &str, category: &str) -> Result<Self, String> {
272 let name = name.trim();
273 let category = category.trim();
274
275 if name.is_empty() {
276 return Err("application name cannot be empty".into());
277 }
278 if category.is_empty() {
279 return Err("application category cannot be empty".into());
280 }
281
282 Ok(Self {
283 name: name.to_string(),
284 category: category.to_string(),
285 transports: Vec::new(),
286 dns_suffixes: Vec::new(),
287 tls_sni_suffixes: Vec::new(),
288 http_hosts: Vec::new(),
289 })
290 }
291
292 pub fn transport(mut self, descriptor: &str) -> Result<Self, String> {
294 self.transports
295 .push(TransportService::from_str(descriptor)?);
296 Ok(self)
297 }
298
299 pub fn transport_value(mut self, svc: TransportService) -> Self {
301 self.transports.push(svc);
302 self
303 }
304
305 pub fn dns_suffix<S: Into<String>>(mut self, suffix: S) -> Self {
306 self.dns_suffixes.push(suffix.into());
307 self
308 }
309
310 pub fn tls_sni_suffix<S: Into<String>>(mut self, suffix: S) -> Self {
311 self.tls_sni_suffixes.push(suffix.into());
312 self
313 }
314
315 pub fn http_host<S: Into<String>>(mut self, host: S) -> Self {
316 self.http_hosts.push(host.into());
317 self
318 }
319
320 pub fn build(self) -> ApplicationObj {
321 ApplicationObj {
322 name: self.name,
323 category: self.category,
324 transports: self.transports,
325 dns_suffixes: self.dns_suffixes,
326 tls_sni_suffixes: self.tls_sni_suffixes,
327 http_hosts: self.http_hosts,
328 }
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn address_defaults_name_when_empty() {
338 let obj = address("", "192.0.2.10").unwrap();
339 assert_eq!(obj.name, "192.0.2.10");
340 }
341
342 #[test]
343 fn service_group_builder_rejects_duplicates() {
344 let service = service::tcp(443);
345 let err = service_group("web")
346 .unwrap()
347 .with_service(service.clone())
348 .unwrap()
349 .with_service(service)
350 .unwrap_err();
351 assert!(err.contains("web"));
352 }
353
354 #[test]
355 fn application_builder_collects_values() {
356 let app = application("github", "developer")
357 .unwrap()
358 .transport("tcp/443")
359 .unwrap()
360 .dns_suffix(".github.com")
361 .http_host("github.com")
362 .build();
363
364 assert_eq!(app.name, "github");
365 assert_eq!(app.category, "developer");
366 assert_eq!(app.transports.len(), 1);
367 assert_eq!(app.dns_suffixes, vec![".github.com"]);
368 assert_eq!(app.http_hosts, vec!["github.com"]);
369 }
370}