auto_discovery/
types.rs

1//! Type definitions for the auto-discovery library
2
3use crate::service::ServiceInfo;
4use crate::error::{DiscoveryError, Result};
5use serde::{Deserialize, Serialize};
6use std::{
7    collections::HashMap,
8    fmt,
9    net::{IpAddr, Ipv4Addr, Ipv6Addr},
10    str::FromStr,
11};
12
13/// Represents a service type for discovery
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct ServiceType {
16    /// The service string without protocol (e.g., "_http", "_myservice")
17    service_name: String,
18    /// The protocol string (e.g., "_tcp", "_udp")
19    protocol: String,
20    /// Optional domain for the service
21    domain: Option<String>,
22}
23
24impl ServiceType {
25    /// Create a new service type with default TCP protocol
26    pub fn new<S: Into<String>>(service: S) -> Result<Self> {
27        let service_type_str = service.into();
28
29        if service_type_str.is_empty() {
30            return Err(DiscoveryError::invalid_service("Service type cannot be empty"));
31        }
32
33        // Handle UPnP URN format (urn:schemas-upnp-org:service:ContentDirectory:1)
34        if service_type_str.starts_with("urn:") {
35            return Ok(ServiceType {
36                service_name: service_type_str.clone(),
37                protocol: "".to_string(), // UPnP doesn't use traditional protocols
38                domain: None,
39            });
40        }
41
42        // Parse service type like "_http._tcp.local" or "_http._tcp"
43        let parts: Vec<&str> = service_type_str.split('.').collect();
44        
45        if parts.len() < 2 {
46            return Err(DiscoveryError::invalid_service(
47                "Service type must contain protocol (e.g., '._tcp')",
48            ));
49        }
50
51        // Extract service name (first part)
52        let service_name = parts[0].to_string();
53        
54        // Extract protocol (second part, should start with _)
55        let protocol_part = parts[1];
56        if !protocol_part.starts_with('_') {
57            return Err(DiscoveryError::invalid_service(
58                "Service type must contain protocol (e.g., '._tcp')",
59            ));
60        }
61        let protocol = format!(".{}", protocol_part);
62        
63        // Extract domain if present (third part and beyond)
64        let domain = if parts.len() > 2 {
65            Some(parts[2..].join("."))
66        } else {
67            None
68        };
69
70        // Ensure service has leading underscore
71        let final_service_name = if service_name.starts_with('_') {
72            service_name
73        } else {
74            format!("_{}", service_name)
75        };
76
77        Ok(ServiceType {
78            service_name: final_service_name,
79            protocol,
80            domain,
81        })
82    }
83
84    /// Create a new service type with specified protocol
85    pub fn with_protocol<S: Into<String>>(service: S, protocol: S) -> Result<Self> {
86        let mut protocol_str = protocol.into();
87        if !protocol_str.starts_with('_') {
88            protocol_str = format!("_{}", protocol_str);
89        }
90
91        Ok(ServiceType {
92            service_name: service.into(),
93            protocol: protocol_str,
94            domain: None,
95        })
96    }
97
98    /// Create a new service type with specified domain
99    pub fn with_domain<S: Into<String>>(service: S, domain: S) -> Result<Self> {
100        Ok(ServiceType {
101            service_name: service.into(),
102            protocol: "_tcp".to_string(),
103            domain: Some(domain.into()),
104        })
105    }
106
107    /// Get the service string
108    pub fn service_name(&self) -> &str {
109        &self.service_name
110    }
111
112    /// Get the protocol string
113    pub fn protocol(&self) -> &str {
114        &self.protocol
115    }
116
117    /// Get the domain if present
118    pub fn domain(&self) -> Option<&str> {
119        self.domain.as_deref()
120    }
121
122    /// Convert to a fully qualified service string
123    pub fn full_name(&self) -> String {
124        // For UPnP URNs, return the service name as-is since it's already complete
125        if self.service_name.starts_with("urn:") {
126            return self.service_name.clone();
127        }
128        
129        match &self.domain {
130            None => format!("{}_{}", self.service_name, self.protocol),
131            Some(domain) => format!("{}_{}.{}", self.service_name, self.protocol, domain),
132        }
133    }
134
135    /// Convert to a string
136    pub fn to_string(&self) -> String {
137        let mut result = self.service_name.clone();
138        result.push_str(&self.protocol);
139        if let Some(domain) = &self.domain {
140            result.push('.');
141            result.push_str(domain);
142        }
143        result
144    }
145
146    /// Check if the service type is valid
147    pub fn is_valid(&self) -> bool {
148        !self.service_name.is_empty() && !self.protocol.is_empty()
149    }
150}
151
152impl FromStr for ServiceType {
153    type Err = DiscoveryError;
154
155    fn from_str(s: &str) -> Result<Self> {
156        ServiceType::new(s)
157    }
158}
159
160impl fmt::Display for ServiceType {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        if let Some(domain) = &self.domain {
163            write!(
164                f,
165                "{}{}.{}",
166                self.service_name, self.protocol, domain
167            )
168        } else {
169            write!(f, "{}{}", self.service_name, self.protocol)
170        }
171    }
172}
173
174impl From<ServiceType> for String {
175    fn from(service_type: ServiceType) -> Self {
176        service_type.to_string()
177    }
178}
179
180/// Protocol type for service discovery
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
182pub enum ProtocolType {
183    /// Multicast DNS
184    Mdns,
185    /// DNS Service Discovery
186    DnsSd,
187    /// Universal Plug and Play
188    Upnp,
189}
190
191impl Default for ProtocolType {
192    fn default() -> Self {
193        ProtocolType::Mdns
194    }
195}
196
197impl fmt::Display for ProtocolType {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        match self {
200            ProtocolType::Mdns => write!(f, "mDNS"),
201            ProtocolType::DnsSd => write!(f, "DNS-SD"),
202            ProtocolType::Upnp => write!(f, "UPnP"),
203        }
204    }
205}
206
207/// Network interface information
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209pub struct NetworkInterface {
210    /// Interface name
211    pub name: String,
212    /// IPv4 addresses
213    pub ipv4_addresses: Vec<Ipv4Addr>,
214    /// IPv6 addresses
215    pub ipv6_addresses: Vec<Ipv6Addr>,
216    /// Whether the interface is active
217    pub is_up: bool,
218    /// Whether the interface supports multicast
219    pub supports_multicast: bool,
220}
221
222impl NetworkInterface {
223    /// Create a new network interface
224    pub fn new<S: Into<String>>(name: S) -> Self {
225        Self {
226            name: name.into(),
227            ipv4_addresses: Vec::new(),
228            ipv6_addresses: Vec::new(),
229            is_up: false,
230            supports_multicast: false,
231        }
232    }
233
234    /// Add an IPv4 address
235    pub fn with_ipv4(mut self, addr: Ipv4Addr) -> Self {
236        self.ipv4_addresses.push(addr);
237        self
238    }
239
240    /// Add an IPv6 address
241    pub fn with_ipv6(mut self, addr: Ipv6Addr) -> Self {
242        self.ipv6_addresses.push(addr);
243        self
244    }
245
246    /// Set interface status
247    pub fn with_status(mut self, is_up: bool, supports_multicast: bool) -> Self {
248        self.is_up = is_up;
249        self.supports_multicast = supports_multicast;
250        self
251    }
252
253    /// Get all IP addresses
254    pub fn all_addresses(&self) -> Vec<IpAddr> {
255        let mut addresses = Vec::new();
256        addresses.extend(self.ipv4_addresses.iter().map(|&addr| IpAddr::V4(addr)));
257        addresses.extend(self.ipv6_addresses.iter().map(|&addr| IpAddr::V6(addr)));
258        addresses
259    }
260}
261
262/// Service attributes as key-value pairs
263pub type ServiceAttributes = HashMap<String, String>;
264
265/// Filter for discovered services
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct DiscoveryFilter {
268    /// Service type filters
269    pub service_type_filters: Vec<ServiceType>,
270    /// Protocol type filters
271    pub protocol_filters: Vec<ProtocolType>,
272    /// Custom attribute filter patterns (key-value regex patterns)
273    pub attribute_patterns: Vec<(String, String)>,
274}
275
276impl DiscoveryFilter {
277    /// Create a new empty filter
278    pub fn new() -> Self {
279        Self {
280            service_type_filters: Vec::new(),
281            protocol_filters: Vec::new(),
282            attribute_patterns: Vec::new(),
283        }
284    }
285
286    /// Add a service type filter
287    pub fn with_service_type(mut self, service_type: ServiceType) -> Self {
288        self.service_type_filters.push(service_type);
289        self
290    }
291
292    /// Add a protocol filter
293    pub fn with_protocol(mut self, protocol: ProtocolType) -> Self {
294        self.protocol_filters.push(protocol);
295        self
296    }
297
298    /// Add an attribute pattern filter (key regex, value regex)
299    pub fn with_attribute_pattern(mut self, key_pattern: String, value_pattern: String) -> Self {
300        self.attribute_patterns.push((key_pattern, value_pattern));
301        self
302    }
303
304    /// Check if a service matches this filter
305    pub fn matches(&self, service: &ServiceInfo) -> bool {
306        // Check service type filters
307        if !self.service_type_filters.is_empty() 
308            && !self.service_type_filters.contains(&service.service_type) {
309            return false;
310        }
311
312        // Check protocol filters
313        if !self.protocol_filters.is_empty() 
314            && !self.protocol_filters.contains(&service.protocol_type) {
315            return false;
316        }
317
318        // Check attribute pattern filters
319        for (key_pattern, value_pattern) in &self.attribute_patterns {
320            let mut matches = false;
321            for (key, value) in &service.attributes {
322                // Simple string matching for now (could be enhanced with regex)
323                if key.contains(key_pattern) && value.contains(value_pattern) {
324                    matches = true;
325                    break;
326                }
327            }
328            if !matches {
329                return false;
330            }
331        }
332
333        true
334    }
335}
336
337impl Default for DiscoveryFilter {
338    fn default() -> Self {
339        Self::new()
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use std::time::Duration;
347
348    #[test]
349    fn test_service_type() -> Result<()> {
350        let service = ServiceType::new("_http._tcp")?;
351        assert_eq!(service.service_name, "_http");
352        assert_eq!(service.protocol, "._tcp");
353        assert_eq!(service.domain, None);
354        assert_eq!(service.to_string(), "_http._tcp");
355        Ok(())
356    }
357
358    #[test]
359    fn test_service_type_with_domain() -> Result<()> {
360        let service = ServiceType::new("_http._tcp.local")?;
361        assert_eq!(service.service_name, "_http");
362        assert_eq!(service.protocol, "._tcp");
363        assert_eq!(service.domain, Some("local".to_string()));
364        assert_eq!(service.to_string(), "_http._tcp.local");
365        Ok(())
366    }
367
368    #[test]
369    fn test_invalid_service_type() {
370        assert!(ServiceType::new("").is_err());
371        assert!(ServiceType::new("invalid").is_err());
372        assert!(ServiceType::new("_http").is_err()); // Missing protocol
373    }
374
375    #[test] 
376    fn test_discovery_filter() -> Result<()> {
377        use std::net::{IpAddr, Ipv4Addr};
378        use crate::service::ServiceInfo;
379        
380        let filter = DiscoveryFilter::new()
381            .with_service_type(ServiceType::new("_http._tcp")?);
382
383        let service = ServiceInfo::new(
384            "Test Service",
385            "_http._tcp",
386            8080,
387            Some(vec![("version", "1.0")]),
388        )?;
389
390        assert!(filter.matches(&service));
391        Ok(())
392    }
393
394    #[test]
395    fn test_protocol_type_default() {
396        assert_eq!(ProtocolType::default(), ProtocolType::Mdns);
397    }
398}