iroh_http_discovery/engine/
types.rs1use std::net::{IpAddr, SocketAddr};
2
3use super::TransportError;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum Protocol {
8 #[default]
10 Udp,
11 Tcp,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ServiceConfig {
18 pub service_name: String,
20 pub instance_name: String,
22 pub port: u16,
24 pub addrs: Vec<IpAddr>,
26 pub txt: Vec<(String, String)>,
28 pub protocol: Protocol,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct BrowseConfig {
35 pub service_name: String,
37 pub protocol: Protocol,
39}
40
41impl BrowseConfig {
42 pub fn udp(service_name: impl Into<String>) -> Self {
44 Self {
45 service_name: service_name.into(),
46 protocol: Protocol::Udp,
47 }
48 }
49}
50
51impl Protocol {
52 fn label(self) -> &'static str {
53 match self {
54 Self::Udp => "_udp",
55 Self::Tcp => "_tcp",
56 }
57 }
58}
59
60pub fn service_type(service_name: &str, protocol: Protocol) -> Result<String, TransportError> {
62 if service_name.is_empty() {
63 return Err(TransportError::new("service name must not be empty"));
64 }
65 if !service_name
66 .chars()
67 .all(|character| character.is_ascii_alphanumeric() || character == '-')
68 {
69 return Err(TransportError::new(format!(
70 "{service_name:?} must contain only ASCII letters, digits, and '-'"
71 )));
72 }
73 Ok(format!("_{service_name}.{}.local.", protocol.label()))
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct ServiceRecord {
82 pub is_active: bool,
84 pub service_type: String,
87 pub instance_name: String,
89 pub host: Option<String>,
91 pub port: u16,
93 pub addrs: Vec<SocketAddr>,
95 pub txt: Vec<(String, String)>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum RawEvent {
102 Upsert(ServiceRecord),
104 Remove {
106 service_type: String,
108 instance_name: String,
110 },
111}
112
113impl RawEvent {
114 pub fn identity(&self) -> (&str, &str) {
116 match self {
117 Self::Upsert(record) => (&record.service_type, &record.instance_name),
118 Self::Remove {
119 service_type,
120 instance_name,
121 } => (service_type, instance_name),
122 }
123 }
124}
125
126impl From<RawEvent> for ServiceRecord {
127 fn from(event: RawEvent) -> Self {
128 match event {
129 RawEvent::Upsert(mut record) => {
130 record.is_active = true;
131 record
132 }
133 RawEvent::Remove {
134 service_type,
135 instance_name,
136 } => Self {
137 is_active: false,
138 service_type,
139 instance_name,
140 host: None,
141 port: 0,
142 addrs: Vec::new(),
143 txt: Vec::new(),
144 },
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn upsert_and_remove_have_the_same_explicit_identity() {
155 let record = ServiceRecord {
156 is_active: true,
157 service_type: "_web._tcp.local.".into(),
158 instance_name: "docs".into(),
159 host: Some("docs.local.".into()),
160 port: 443,
161 addrs: vec![],
162 txt: vec![("path".into(), "/manual".into())],
163 };
164 let upsert = RawEvent::Upsert(record);
165 let remove = RawEvent::Remove {
166 service_type: "_web._tcp.local.".into(),
167 instance_name: "docs".into(),
168 };
169
170 assert_eq!(upsert.identity(), remove.identity());
171 }
172
173 #[test]
174 fn remove_converts_to_the_public_compatibility_tombstone() {
175 let record = ServiceRecord::from(RawEvent::Remove {
176 service_type: "_web._tcp.local.".into(),
177 instance_name: "docs".into(),
178 });
179
180 assert!(!record.is_active);
181 assert_eq!(record.service_type, "_web._tcp.local.");
182 assert_eq!(record.instance_name, "docs");
183 assert_eq!(record.host, None);
184 assert_eq!(record.port, 0);
185 assert!(record.addrs.is_empty());
186 assert!(record.txt.is_empty());
187 }
188
189 #[test]
190 fn service_type_preserves_existing_validation_and_protocol_mapping() {
191 assert!(matches!(
192 service_type("iroh-http", Protocol::Udp),
193 Ok(value) if value == "_iroh-http._udp.local."
194 ));
195 assert!(matches!(
196 service_type("web", Protocol::Tcp),
197 Ok(value) if value == "_web._tcp.local."
198 ));
199 assert!(service_type("bad.name", Protocol::Udp).is_err());
200 }
201}