Skip to main content

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}