1use std::{marker::PhantomData, net::IpAddr};
10
11use futures_core::Stream;
12use futures_util::StreamExt;
13use mdns_sd::{ServiceDaemon, ServiceEvent, ServiceInfo};
14use tracing::debug;
15
16use wot_td::{
17 extend::{Extend, ExtendablePiece, ExtendableThing},
18 hlist::Nil,
19 thing::Thing,
20};
21
22#[derive(thiserror::Error, Debug)]
24#[non_exhaustive]
25pub enum Error {
26 #[error("mdns cannot be accessed {0}")]
27 Mdns(#[from] mdns_sd::Error),
28 #[error("reqwest error {0}")]
29 Reqwest(#[from] reqwest::Error),
30 #[error("Missing address")]
31 NoAddress,
32}
33
34pub type Result<T> = std::result::Result<T, Error>;
36
37const WELL_KNOWN: &str = "/.well-known/wot";
38
39pub struct Discoverer<Other: ExtendableThing + ExtendablePiece = Nil> {
41 mdns: ServiceDaemon,
42 service_type: String,
43 _other: PhantomData<Other>,
44}
45
46pub struct Discovered<Other: ExtendableThing + ExtendablePiece> {
48 pub thing: Thing<Other>,
52 info: ServiceInfo,
53 scheme: String,
54}
55
56impl<Other: ExtendableThing + ExtendablePiece> Discovered<Other> {
57 pub fn get_addresses(&self) -> Vec<IpAddr> {
59 self.info
60 .get_addresses()
61 .iter()
62 .map(|ip| ip.to_owned().into())
63 .collect()
64 }
65 pub fn get_port(&self) -> u16 {
67 self.info.get_port()
68 }
69
70 pub fn get_hostname(&self) -> &str {
74 self.info.get_hostname()
75 }
76
77 pub fn get_scheme(&self) -> &str {
79 &self.scheme
80 }
81}
82
83async fn get_thing<Other: ExtendableThing + ExtendablePiece>(
84 info: ServiceInfo,
85) -> Result<Discovered<Other>> {
86 let host = info.get_addresses().iter().next().ok_or(Error::NoAddress)?;
87 let port = info.get_port();
88 let props = info.get_properties();
89 let path = props.get_property_val_str("td").unwrap_or(WELL_KNOWN);
90 let proto = props
91 .get_property_val_str("scheme")
92 .or_else(|| {
93 props
95 .get_property_val_str("tls")
96 .map(|tls| if tls == "1" { "https" } else { "http" })
97 })
98 .unwrap_or("http");
99
100 debug!("Got {proto} {host} {port} {path}");
101
102 let r = reqwest::get(format!("{proto}://{host}:{port}{path}")).await?;
103
104 let thing = r.json().await?;
105 let scheme = proto.to_owned();
106 let d = Discovered {
107 thing,
108 info,
109 scheme,
110 };
111
112 Ok(d)
113}
114
115impl Discoverer {
116 pub fn new() -> Result<Self> {
118 let mdns = ServiceDaemon::new()?;
119 let service_type = "_wot._tcp.local.".to_owned();
120 Ok(Self {
121 mdns,
122 service_type,
123 _other: PhantomData,
124 })
125 }
126}
127
128impl<Other: ExtendableThing + ExtendablePiece> Discoverer<Other> {
129 pub fn ext<T>(self) -> Discoverer<Other::Target>
131 where
132 Other: Extend<T>,
133 Other::Target: ExtendableThing + ExtendablePiece,
134 {
135 let Discoverer {
136 mdns,
137 service_type,
138 _other,
139 } = self;
140
141 Discoverer {
142 mdns,
143 service_type,
144 _other: PhantomData,
145 }
146 }
147
148 pub fn stream(&self) -> Result<impl Stream<Item = Result<Discovered<Other>>>> {
150 let receiver = self.mdns.browse(&self.service_type)?;
151
152 let s = receiver.into_stream().filter_map(|v| async move {
153 tracing::info!("{:?}", v);
154 if let ServiceEvent::ServiceResolved(info) = v {
155 let t = get_thing(info).await;
156 Some(t)
157 } else {
158 None
159 }
160 });
161
162 Ok(s)
163 }
164}