tosca_controller/
request.rs

1use std::collections::HashMap;
2use std::fmt::Write;
3use std::future::Future;
4
5use serde::Serialize;
6
7use tracing::error;
8
9use tosca::device::DeviceEnvironment;
10use tosca::hazards::Hazards;
11use tosca::parameters::{ParameterValue, ParametersData, ParametersValues};
12use tosca::response::{ResponseKind, SERIALIZATION_ERROR};
13use tosca::route::{RestKind, RouteConfig, RouteConfigs};
14
15use crate::error::{Error, ErrorKind};
16use crate::response::{InfoResponseParser, OkResponseParser, Response, SerialResponseParser};
17
18fn slash_end(s: &str) -> &str {
19    if s.len() > 1 && s.ends_with('/') {
20        &s[..s.len() - 1]
21    } else {
22        s
23    }
24}
25
26fn slash_start(s: &str) -> &str {
27    if s.len() > 1 && s.starts_with('/') {
28        &s[1..]
29    } else {
30        s
31    }
32}
33
34fn slash_start_end(s: &str) -> &str {
35    slash_start(slash_end(s))
36}
37
38fn compare_values_with_params_data(
39    parameter_values: &ParametersValues,
40    parameters_data: &ParametersData,
41) -> Result<(), Error> {
42    for (name, parameter_value) in parameter_values {
43        let Some(parameter_kind) = parameters_data.get(name) else {
44            return Err(parameter_error(format!("`{name}` does not exist")));
45        };
46
47        if !parameter_value.match_kind(parameter_kind) {
48            return Err(parameter_error(format!(
49                "Found type `{}` for `{name}`, expected type `{}`",
50                parameter_value.as_type(),
51                parameter_kind.as_type(),
52            )));
53        }
54    }
55    Ok(())
56}
57
58fn parameter_error(message: String) -> Error {
59    error!(message);
60    Error::new(ErrorKind::InvalidParameter, message)
61}
62
63#[derive(Debug, PartialEq)]
64struct RequestData {
65    request: String,
66    parameters: HashMap<String, String>,
67}
68
69impl RequestData {
70    const fn new(request: String, parameters: HashMap<String, String>) -> Self {
71        Self {
72            request,
73            parameters,
74        }
75    }
76}
77
78pub(crate) fn create_requests(
79    route_configs: RouteConfigs,
80    complete_address: &str,
81    main_route: &str,
82    environment: DeviceEnvironment,
83) -> HashMap<String, Request> {
84    route_configs
85        .into_iter()
86        .map(|route| {
87            (
88                route.data.path.to_string(),
89                Request::new(complete_address, main_route, environment, route),
90            )
91        })
92        .collect()
93}
94
95/// Request information.
96///
97/// Each request is identified by the data of its associated route and the
98/// corresponding response.
99pub struct RequestInfo<'device> {
100    /// Route name.
101    pub route: &'device str,
102    /// Route description.
103    pub description: Option<&'device str>,
104    /// Rest kind.
105    pub rest_kind: RestKind,
106    /// Route hazards.
107    pub hazards: &'device Hazards,
108    /// Parameters data.
109    ///
110    /// If the request has no parameters, the reference will be empty.
111    pub parameters_data: &'device ParametersData,
112    /// Response kind.
113    pub response_kind: ResponseKind,
114}
115
116impl<'device> RequestInfo<'device> {
117    pub(crate) fn new(route: &'device str, request: &'device Request) -> Self {
118        Self {
119            route,
120            description: request.description.as_deref(),
121            rest_kind: request.kind,
122            hazards: &request.hazards,
123            parameters_data: &request.parameters_data,
124            response_kind: request.response_kind,
125        }
126    }
127}
128
129/// A device request to be sent to a device.
130///
131/// A request can either be plain, with no associated parameters, or include
132/// parameters that serve as inputs for device tasks.
133#[derive(Debug, PartialEq, Serialize)]
134pub struct Request {
135    pub(crate) kind: RestKind,
136    pub(crate) hazards: Hazards,
137    pub(crate) route: String,
138    pub(crate) description: Option<String>,
139    pub(crate) parameters_data: ParametersData,
140    pub(crate) response_kind: ResponseKind,
141    pub(crate) device_environment: DeviceEnvironment,
142}
143
144impl Request {
145    /// Returns an immutable reference to the request [`Hazards`].
146    #[must_use]
147    pub fn hazards(&self) -> &Hazards {
148        &self.hazards
149    }
150
151    /// Returns a request [`RestKind`].
152    #[must_use]
153    pub fn kind(&self) -> RestKind {
154        self.kind
155    }
156
157    /// Returns an immutable reference to the [`ParametersData`] associated with
158    /// a request.
159    ///
160    /// If [`None`], the request **does not** contain any [`ParametersData`].
161    #[must_use]
162    pub fn parameters_data(&self) -> Option<&ParametersData> {
163        self.parameters_data
164            .is_empty()
165            .then_some(&self.parameters_data)
166    }
167
168    pub(crate) fn new(
169        address: &str,
170        main_route: &str,
171        device_environment: DeviceEnvironment,
172        route_config: RouteConfig,
173    ) -> Self {
174        let kind = route_config.rest_kind;
175        let route = format!(
176            "{}/{}/{}",
177            slash_end(address),
178            slash_start_end(main_route),
179            slash_start_end(&route_config.data.path)
180        );
181        let hazards = route_config.data.hazards;
182        let parameters_data = route_config.data.parameters;
183        let response_kind = route_config.response_kind;
184
185        Self {
186            kind,
187            hazards,
188            route,
189            description: route_config.data.description.map(|s| s.to_string()),
190            parameters_data,
191            response_kind,
192            device_environment,
193        }
194    }
195
196    pub(crate) async fn retrieve_response<F, Fut>(
197        &self,
198        skip: bool,
199        retrieve_response: F,
200    ) -> Result<Response, Error>
201    where
202        F: FnOnce() -> Fut,
203        Fut: Future<Output = Result<reqwest::Response, Error>>,
204    {
205        if skip {
206            return Ok(Response::Skipped);
207        }
208
209        let response = retrieve_response().await?;
210
211        Ok(match self.response_kind {
212            ResponseKind::Ok => Response::OkBody(OkResponseParser::new(response)),
213            ResponseKind::Serial => Response::SerialBody(SerialResponseParser::new(response)),
214            ResponseKind::Info => Response::InfoBody(InfoResponseParser::new(response)),
215            #[cfg(feature = "stream")]
216            ResponseKind::Stream => {
217                Response::StreamBody(crate::response::StreamResponse::new(response))
218            }
219        })
220    }
221
222    pub(crate) async fn plain_send(&self) -> Result<reqwest::Response, Error> {
223        let request_data =
224            self.request_data(|| self.axum_get_plain(), || self.create_params_plain());
225
226        self.parameters_send(request_data).await
227    }
228
229    pub(crate) async fn create_response(
230        &self,
231        parameters: &ParametersValues<'_>,
232    ) -> Result<reqwest::Response, Error> {
233        let request_data = self.create_request(parameters)?;
234        self.parameters_send(request_data).await
235    }
236
237    async fn parameters_send(&self, request_data: RequestData) -> Result<reqwest::Response, Error> {
238        let RequestData {
239            request,
240            parameters,
241        } = request_data;
242
243        let client = reqwest::Client::new();
244
245        let request_builder = match self.kind {
246            RestKind::Get => client.get(request),
247            RestKind::Post => client.post(request),
248            RestKind::Put => client.put(request),
249            RestKind::Delete => client.delete(request),
250        };
251
252        let request_builder = if self.kind != RestKind::Get && !parameters.is_empty() {
253            request_builder.json(&parameters)
254        } else {
255            request_builder
256        };
257
258        // Close the connection after issuing a request.
259        let response = request_builder.header("Connection", "close").send().await?;
260
261        // TODO: Analyze the response status.
262        // A 404 status (route not found) might be returned when a
263        // device is down or in case of a malformed route.
264        // A 405 status (method not allowed) in case of a wrong REST method.
265        // If the status is 200, the device response is valid. If the
266        // response is 500, an error occurred during a device operation.
267        //
268        //  Add a logger to record the response. In this way, we do not block
269        //  the process returning an error. An app using the controller as
270        //  dependency can then consult the logger to obtain the internal
271        //  problem.
272
273        // Checks whether serialization errors have occurred on the device.
274        // If the serialization error header is present, the response
275        // is considered invalid.
276        // Additionally, the response is invalid if the body
277        // cannot be converted to a string.
278        if response.headers().contains_key(SERIALIZATION_ERROR) {
279            match response.text().await {
280                Ok(serial_error) => {
281                    error!("Serialization error encountered on the device side: {serial_error}");
282                    return Err(Error::new(ErrorKind::Request, serial_error));
283                }
284                Err(err) => {
285                    error!("Error occurred while converting the request into text: {err}");
286                    return Err(Error::new(ErrorKind::Request, err.to_string()));
287                }
288            }
289        }
290
291        Ok(response)
292    }
293
294    fn request_data<A, F>(&self, axum_get: A, params: F) -> RequestData
295    where
296        A: FnOnce() -> String,
297        F: FnOnce() -> HashMap<String, String>,
298    {
299        let request =
300            if self.kind == RestKind::Get && self.device_environment == DeviceEnvironment::Os {
301                axum_get()
302            } else {
303                self.route.clone()
304            };
305
306        let parameters = params();
307
308        RequestData::new(request, parameters)
309    }
310
311    fn create_request(&self, parameters: &ParametersValues) -> Result<RequestData, Error> {
312        // Compare parameters values with parameters data.
313        compare_values_with_params_data(parameters, &self.parameters_data)?;
314
315        Ok(self.request_data(
316            || self.axum_get(parameters),
317            || self.create_params(parameters),
318        ))
319    }
320
321    fn axum_get_plain(&self) -> String {
322        let mut route = self.route.clone();
323        for (_, parameter_kind) in &self.parameters_data {
324            // TODO: Consider returning `Option<String>`
325            if let Err(e) = write!(
326                route,
327                "/{}",
328                ParameterValue::from_parameter_kind(parameter_kind)
329            ) {
330                error!("Error in adding a path to a route : {e}");
331                break;
332            }
333        }
334        route
335    }
336
337    fn create_params_plain(&self) -> HashMap<String, String> {
338        let mut params = HashMap::new();
339        for (name, parameter_kind) in &self.parameters_data {
340            params.insert(
341                name.clone(),
342                format!("{}", ParameterValue::from_parameter_kind(parameter_kind)),
343            );
344        }
345        params
346    }
347
348    // Axum parameters: hello/{{1}}/{{2}}
349    //                  hello/0.5/1
350    fn axum_get(&self, parameters: &ParametersValues) -> String {
351        let mut route = String::from(&self.route);
352        for (name, parameter_kind) in &self.parameters_data {
353            let value = if let Some(value) = parameters.get(name) {
354                format!("{value}")
355            } else {
356                format!("{}", ParameterValue::from_parameter_kind(parameter_kind))
357            };
358            // TODO: Consider returning `Option<String>`
359            if let Err(e) = write!(route, "/{value}") {
360                error!("Error in adding a path to a route : {e}");
361                break;
362            }
363        }
364
365        route
366    }
367
368    fn create_params(&self, parameters: &ParametersValues<'_>) -> HashMap<String, String> {
369        let mut params = HashMap::new();
370        for (name, parameter_kind) in &self.parameters_data {
371            let (name, value) = if let Some(value) = parameters.get(name) {
372                (name, format!("{value}"))
373            } else {
374                (
375                    name,
376                    format!("{}", ParameterValue::from_parameter_kind(parameter_kind)),
377                )
378            };
379            params.insert(name.clone(), value);
380        }
381        params
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use std::collections::HashMap;
388
389    use tosca::device::DeviceEnvironment;
390    use tosca::hazards::{Hazard, Hazards};
391    use tosca::parameters::{ParameterKind, Parameters, ParametersData, ParametersValues};
392    use tosca::route::{RestKind, Route, RouteConfig};
393
394    use super::{Request, RequestData, ResponseKind, parameter_error};
395
396    const ADDRESS_ROUTE: &str = "http://tosca.local/";
397    const ADDRESS_ROUTE_WITHOUT_SLASH: &str = "http://tosca.local/";
398    const COMPLETE_ROUTE: &str = "http://tosca.local/light/route";
399
400    fn plain_request(route: Route, kind: RestKind, hazards: Hazards) {
401        let route = route.serialize_data();
402        let description = route
403            .data
404            .description
405            .as_ref()
406            .map(std::string::ToString::to_string);
407
408        let request = Request::new(ADDRESS_ROUTE, "light/", DeviceEnvironment::Os, route);
409
410        assert_eq!(
411            request,
412            Request {
413                kind,
414                hazards,
415                route: COMPLETE_ROUTE.into(),
416                description,
417                parameters_data: ParametersData::new(),
418                response_kind: ResponseKind::Ok,
419                device_environment: DeviceEnvironment::Os,
420            }
421        );
422    }
423
424    fn request_with_parameters(route: Route, kind: RestKind, hazards: &Hazards) {
425        let route = route
426            .with_parameters(
427                Parameters::new()
428                    .rangeu64_with_default("rangeu64", (0, 20, 1), 5)
429                    .rangef64("rangef64", (0., 20., 0.1)),
430            )
431            .serialize_data();
432        let description = route
433            .data
434            .description
435            .as_ref()
436            .map(std::string::ToString::to_string);
437
438        let parameters_data = ParametersData::new()
439            .insert(
440                "rangeu64".into(),
441                ParameterKind::RangeU64 {
442                    min: 0,
443                    max: 20,
444                    step: 1,
445                    default: 5,
446                },
447            )
448            .insert(
449                "rangef64".into(),
450                ParameterKind::RangeF64 {
451                    min: 0.,
452                    max: 20.,
453                    step: 0.1,
454                    default: 0.,
455                },
456            );
457
458        let request = Request::new(ADDRESS_ROUTE, "light/", DeviceEnvironment::Os, route);
459
460        assert_eq!(
461            request,
462            Request {
463                kind,
464                hazards: hazards.clone(),
465                route: COMPLETE_ROUTE.into(),
466                description,
467                parameters_data,
468                response_kind: ResponseKind::Ok,
469                device_environment: DeviceEnvironment::Os,
470            }
471        );
472
473        // Non-existent parameter.
474        assert_eq!(
475            request.create_request(ParametersValues::new().u64("wrong", 0)),
476            Err(parameter_error("`wrong` does not exist".into()))
477        );
478
479        // Wrong parameter type.
480        assert_eq!(
481            request.create_request(ParametersValues::new().f64("rangeu64", 0.)),
482            Err(parameter_error(
483                "Found type `f64` for `rangeu64`, expected type `u64`".into()
484            ))
485        );
486
487        let mut parameters = HashMap::with_capacity(2);
488        parameters.insert("rangeu64".into(), "3".into());
489        parameters.insert("rangef64".into(), "0".into());
490
491        assert_eq!(
492            request.create_request(ParametersValues::new().u64("rangeu64", 3)),
493            Ok(RequestData {
494                request: if kind == RestKind::Get {
495                    format!("{COMPLETE_ROUTE}/3/0")
496                } else {
497                    COMPLETE_ROUTE.into()
498                },
499                parameters,
500            })
501        );
502    }
503
504    fn request_builder(
505        route: &str,
506        main_route: &str,
507        device_environment: DeviceEnvironment,
508        route_config: RouteConfig,
509    ) {
510        assert_eq!(
511            Request::new(route, main_route, device_environment, route_config),
512            Request {
513                kind: RestKind::Put,
514                hazards: Hazards::new(),
515                route: COMPLETE_ROUTE.into(),
516                description: None,
517                parameters_data: ParametersData::new(),
518                response_kind: ResponseKind::Ok,
519                device_environment: DeviceEnvironment::Os,
520            }
521        );
522    }
523
524    #[test]
525    fn check_request_builder() {
526        let route = Route::put("Route", "/route").serialize_data();
527        let environment = DeviceEnvironment::Os;
528
529        request_builder(ADDRESS_ROUTE, "light/", environment, route.clone());
530        request_builder(ADDRESS_ROUTE_WITHOUT_SLASH, "light", environment, route);
531    }
532
533    #[test]
534    fn create_plain_get_request() {
535        let route = Route::get("Route", "/route").description("A GET route.");
536        plain_request(route, RestKind::Get, Hazards::new());
537    }
538
539    #[test]
540    fn create_plain_post_request() {
541        let route = Route::post("Route", "/route").description("A POST route.");
542        plain_request(route, RestKind::Post, Hazards::new());
543    }
544
545    #[test]
546    fn create_plain_put_request() {
547        let route = Route::put("Route", "/route").description("A PUT route.");
548        plain_request(route, RestKind::Put, Hazards::new());
549    }
550
551    #[test]
552    fn create_plain_delete_request() {
553        let route = Route::delete("Route", "/route").description("A DELETE route.");
554        plain_request(route, RestKind::Delete, Hazards::new());
555    }
556
557    #[test]
558    fn create_plain_get_request_with_hazards() {
559        let hazards = Hazards::new()
560            .insert(Hazard::FireHazard)
561            .insert(Hazard::AirPoisoning);
562        plain_request(
563            Route::get("Route", "/route")
564                .description("A GET route.")
565                .with_hazards(hazards.clone()),
566            RestKind::Get,
567            hazards,
568        );
569    }
570
571    #[test]
572    fn create_get_request_with_parameters() {
573        request_with_parameters(
574            Route::get("Route", "/route").description("A GET route."),
575            RestKind::Get,
576            &Hazards::new(),
577        );
578    }
579
580    #[test]
581    fn create_post_request_with_parameters() {
582        let route = Route::post("Route", "/route").description("A POST route.");
583        request_with_parameters(route, RestKind::Post, &Hazards::new());
584    }
585
586    #[test]
587    fn create_put_request_with_parameters() {
588        let route = Route::put("Route", "/route").description("A PUT route.");
589        request_with_parameters(route, RestKind::Put, &Hazards::new());
590    }
591
592    #[test]
593    fn create_delete_request_with_parameters() {
594        let route = Route::delete("Route", "/route").description("A DELETE route.");
595        request_with_parameters(route, RestKind::Delete, &Hazards::new());
596    }
597
598    #[test]
599    fn create_get_request_with_hazards_and_parameters() {
600        let hazards = Hazards::new()
601            .insert(Hazard::FireHazard)
602            .insert(Hazard::AirPoisoning);
603
604        request_with_parameters(
605            Route::get("Route", "/route")
606                .description("A GET route.")
607                .with_hazards(hazards.clone()),
608            RestKind::Get,
609            &hazards,
610        );
611    }
612}