wot_serve/servient.rs
1//! Web of Thing Servient
2
3use std::net::SocketAddr;
4
5use crate::{advertise::Advertiser, advertise::ThingType, hlist::NilPlus};
6use axum::Router;
7use wot_td::{
8 builder::{ThingBuilder, ToExtend},
9 extend::ExtendableThing,
10 hlist::*,
11 thing::Thing,
12};
13
14mod builder;
15
16pub use builder::*;
17
18/// Error type for the Servient.
19#[derive(thiserror::Error, Debug)]
20pub enum Error {
21 /// Error setting up the http application server.
22 #[error("http internal error {0}")]
23 Http(#[from] axum::Error),
24
25 /// Error setting up the mDNS advertiser.
26 #[error("mdns internal error {0}")]
27 Advertise(#[from] crate::advertise::Error),
28}
29
30/// WoT Servient serving a Thing
31///
32/// The application server and the [`Thing`] Description should be built at the same
33/// time using [`Servient::builder`].
34///
35/// [`Thing`]: wot_td::thing::Thing
36pub struct Servient<Other: ExtendableThing = Nil> {
37 /// hostname for the thing
38 ///
39 /// Used in the DNS-SD advertisement by default
40 pub name: String,
41 /// The Thing Description representing the servient
42 pub thing: Thing<Other>,
43 /// The http router for the servient
44 pub router: Router,
45 /// DNS-SD advertisement
46 pub sd: Advertiser,
47 /// Address the http server will bind to
48 pub http_addr: SocketAddr,
49 /// The type of thing advertised
50 pub thing_type: ThingType,
51}
52
53impl Servient<Nil> {
54 /// Instantiate a ThingBuilder with its Form augmented with [`HttpRouter`] methods.
55 ///
56 /// By default it sets the CORS headers to allow any origin, you may disable the behaviour
57 /// by calling [ServientSettings::http_disable_permissive_cors].
58 ///
59 /// # Examples
60 ///
61 /// This should fail:
62 /// ```compile_fail
63 /// # use wot_serve::{Servient, servient::{BuildServient,HttpRouter}};
64 /// # use wot_td::thing::FormOperation;
65 /// let servient = Servient::builder("test")
66 /// .finish_extend()
67 /// .form(|f| {
68 /// f.href("/ref")
69 /// .http_get(|| async { "Hello, World!" })
70 /// .op(FormOperation::ReadAllProperties)
71 /// .op(FormOperation::WriteAllProperties)
72 /// .into()
73 /// })
74 /// .build_servient()
75 /// .unwrap();
76 /// ```
77 ///
78 /// This should fail as well:
79 /// ```compile_fail
80 /// # use wot_serve::{Servient, servient::{BuildServient,HttpRouter}};
81 /// # use wot_td::thing::FormOperation;
82 /// let servient = Servient::builder("test")
83 /// .finish_extend()
84 /// .form(|f| {
85 /// f.href("/ref")
86 /// .http_get(|| async { "Hello, World!" })
87 /// .http_put(|| async { "Hello, World!" })
88 /// .into()
89 /// })
90 /// .build_servient()
91 /// .unwrap();
92 /// ```
93 ///
94 /// This should work instead.
95 /// ```
96 /// # use wot_serve::{Servient, servient::{BuildServient,HttpRouter}};
97 /// # use wot_td::thing::FormOperation;
98 /// let servient = Servient::builder("test")
99 /// .finish_extend()
100 /// .form(|f| {
101 /// f.href("/ref")
102 /// .http_get(|| async { "Hello, World!" })
103 /// .op(FormOperation::ReadAllProperties)
104 /// })
105 /// .build_servient()
106 /// .unwrap();
107 /// ```
108 pub fn builder(title: impl Into<String>) -> ThingBuilder<NilPlus<ServientExtension>, ToExtend> {
109 ThingBuilder::<NilPlus<ServientExtension>, ToExtend>::new(title)
110 }
111}
112
113impl<O: ExtendableThing> Servient<O> {
114 /// Start a listening server and advertise for it.
115 pub async fn serve(&self) -> Result<(), Error> {
116 let ip = self.http_addr.ip();
117
118 if !ip.is_loopback() {
119 let mut ad = self
120 .sd
121 .add_service(&self.name)
122 .thing_type(self.thing_type)
123 .port(self.http_addr.port());
124
125 if !ip.is_unspecified() {
126 ad = ad.ips([ip]);
127 }
128 ad.build()?;
129 }
130
131 let listener = tokio::net::TcpListener::bind(&self.http_addr)
132 .await
133 .map_err(axum::Error::new)?;
134 axum::serve(listener, self.router.clone())
135 .await
136 .map_err(axum::Error::new)?;
137
138 Ok(())
139 }
140}
141
142#[cfg(test)]
143mod test {
144 use wot_td::{builder::affordance::*, builder::data_schema::*, thing::FormOperation};
145
146 use crate::advertise::ThingType;
147
148 use super::*;
149
150 #[derive(serde::Serialize)]
151 struct E {}
152
153 impl ExtendableThing for E {
154 type Form = ();
155 type DataSchema = ();
156 type ArraySchema = ();
157 type ObjectSchema = ();
158 type EventAffordance = ();
159 type ActionAffordance = ();
160 type ExpectedResponse = ();
161 type PropertyAffordance = ();
162 type InteractionAffordance = ();
163 }
164
165 #[test]
166 fn build_servient() {
167 let servient = Servient::builder("test")
168 .ext(E {})
169 .finish_extend()
170 .security(|b| b.basic())
171 .form(|f| {
172 f.ext(())
173 .href("/ref")
174 .http_get(|| async { "Hello, World!" })
175 .op(FormOperation::ReadAllProperties)
176 })
177 .form(|f| {
178 f.href("/ref2")
179 .http_get(|| async { "Hello, World! 2" })
180 .ext(())
181 .security("basic")
182 .op(FormOperation::ReadAllProperties)
183 })
184 .build_servient()
185 .unwrap();
186
187 dbg!(&servient.router);
188 }
189
190 #[test]
191 fn build_servient_property() {
192 let servient = Servient::builder("test")
193 .finish_extend()
194 .property("hello", |b| {
195 b.finish_extend_data_schema()
196 .null()
197 .form(|f| {
198 f.href("/hello")
199 .http_get(|| async { "Reading Hello, World!" })
200 .op(FormOperation::ReadProperty)
201 })
202 .form(|f| {
203 f.href("/hello")
204 .http_put(|| async { "Writing Hello, World!" })
205 .op(FormOperation::WriteProperty)
206 })
207 })
208 .build_servient()
209 .unwrap();
210
211 dbg!(&servient.router);
212 }
213
214 #[test]
215 fn build_servient_action() {
216 let servient = Servient::builder("test")
217 .finish_extend()
218 .action("hello", |b| {
219 b.input(|i| i.finish_extend().number()).form(|f| {
220 f.href("/say_hello")
221 .http_post(|| async { "Saying Hello, World!" })
222 })
223 })
224 .action("update", |b| {
225 b.form(|f| {
226 f.href("/update_hello")
227 .http_patch(|| async { "Updating Hello, World!" })
228 })
229 })
230 .action("delete", |b| {
231 b.form(|f| {
232 f.href("/delete_hello")
233 .http_delete(|| async { "Goodbye, World!" })
234 })
235 })
236 .build_servient()
237 .unwrap();
238
239 dbg!(&servient.router);
240 }
241
242 #[test]
243 fn servient_setup() {
244 let addr = "0.0.0.0:3000".parse().unwrap();
245 let servient = Servient::builder("test me")
246 .finish_extend()
247 .http_bind(addr)
248 .thing_type(ThingType::Directory)
249 .http_disable_permissive_cors()
250 .build_servient()
251 .unwrap();
252
253 assert_eq!(servient.http_addr, addr);
254 assert_eq!(servient.thing_type, ThingType::Directory);
255 }
256}