libcoap_rs/message/
response.rs

1// SPDX-License-Identifier: BSD-2-Clause
2/*
3 * response.rs - Types wrapping messages into responses.
4 * This file is part of the libcoap-rs crate, see the README and LICENSE files for
5 * more information and terms of use.
6 * Copyright © 2021-2023 The NAMIB Project Developers, all rights reserved.
7 * See the README as well as the LICENSE file for more information.
8 */
9
10use crate::error::{MessageConversionError, MessageTypeError, OptionValueError};
11use crate::message::{CoapMessage, CoapMessageCommon, CoapOption};
12use crate::protocol::{
13    CoapMessageCode, CoapMessageType, CoapOptionType, CoapResponseCode, ContentFormat, ETag, MaxAge, Observe,
14};
15use crate::types::CoapUri;
16use std::fmt::Display;
17use std::fmt::Formatter;
18
19/// Internal representation of a CoAP URI that can be used as a response location.
20#[derive(Clone, Debug, Eq, PartialEq, Hash)]
21pub struct CoapResponseLocation(CoapUri);
22
23impl Display for CoapResponseLocation {
24    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
25        f.write_fmt(format_args!("Response Location: {}", self.0))
26    }
27}
28
29impl CoapResponseLocation {
30    /// Creates a new response location from the given [CoapUri], returning an [OptionValueError] if
31    /// the URI contains invalid values for response locations.
32    pub fn new_response_location(uri: CoapUri) -> Result<CoapResponseLocation, OptionValueError> {
33        if uri.scheme().is_some() || uri.host().is_some() || uri.port().is_some() {
34            return Err(OptionValueError::IllegalValue);
35        }
36        Ok(CoapResponseLocation(uri))
37    }
38
39    /// Converts this response location into a [`Vec<CoapOption>`] that can be added to a message.
40    pub fn into_options(self) -> Vec<CoapOption> {
41        let mut options = Vec::new();
42        let mut uri = self.0;
43        if let Some(path) = uri.drain_path_iter() {
44            options.extend(path.map(CoapOption::LocationPath));
45        }
46        if let Some(query) = uri.drain_query_iter() {
47            options.extend(query.map(CoapOption::LocationQuery));
48        }
49        options
50    }
51
52    /// Returns an immutable reference to the underlying URI.
53    pub fn as_uri(&self) -> &CoapUri {
54        &self.0
55    }
56}
57
58impl TryFrom<CoapUri> for CoapResponseLocation {
59    type Error = OptionValueError;
60
61    fn try_from(value: CoapUri) -> Result<Self, Self::Error> {
62        CoapResponseLocation::new_response_location(value)
63    }
64}
65
66#[derive(Debug, Clone, Eq, PartialEq, Hash)]
67pub struct CoapResponse {
68    pdu: CoapMessage,
69    content_format: Option<ContentFormat>,
70    max_age: Option<MaxAge>,
71    etag: Option<ETag>,
72    location: Option<CoapResponseLocation>,
73    observe: Option<Observe>,
74}
75
76impl CoapResponse {
77    /// Creates a new CoAP response with the given message type and code.
78    ///
79    /// Returns an error if the given message type is not allowed for CoAP responses (the allowed
80    /// message types are [CoapMessageType::Con] and [CoapMessageType::Non] and [CoapMessageType::Ack]).
81    pub fn new(type_: CoapMessageType, code: CoapResponseCode) -> Result<CoapResponse, MessageTypeError> {
82        match type_ {
83            CoapMessageType::Con | CoapMessageType::Non | CoapMessageType::Ack => {},
84            v => return Err(MessageTypeError::InvalidForMessageCode(v)),
85        }
86        Ok(CoapResponse {
87            pdu: CoapMessage::new(type_, code.into()),
88            content_format: None,
89            max_age: None,
90            etag: None,
91            location: None,
92            observe: None,
93        })
94    }
95
96    /// Returns the "Max-Age" option value for this response.
97    pub fn max_age(&self) -> Option<MaxAge> {
98        self.max_age
99    }
100
101    /// Sets the "Max-Age" option value for this response.
102    ///
103    /// This option indicates the maximum time a response may be cached (in seconds).
104    ///
105    /// See [RFC 7252, Section 5.10.5](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.5)
106    /// for more information.
107    pub fn set_max_age(&mut self, max_age: Option<MaxAge>) {
108        self.max_age = max_age
109    }
110
111    /// Returns the "Content-Format" option value for this request.
112    pub fn content_format(&self) -> Option<ContentFormat> {
113        self.content_format
114    }
115
116    /// Sets the "Content-Format" option value for this response.
117    ///
118    /// This option indicates the content format of the body of this message.
119    ///
120    /// See [RFC 7252, Section 5.10.3](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.3)
121    /// for more information.
122    pub fn set_content_format(&mut self, content_format: Option<ContentFormat>) {
123        self.content_format = content_format;
124    }
125
126    /// Returns the "ETag" option value for this request.
127    pub fn etag(&self) -> Option<&ETag> {
128        self.etag.as_ref()
129    }
130
131    /// Sets the "ETag" option value for this response.
132    ///
133    /// This option can be used by clients to request a specific representation of the requested
134    /// resource.
135    ///
136    /// The server may send an ETag value alongside a response, which the client can then set here
137    /// to request the given representation.
138    ///
139    /// See [RFC 7252, Section 5.10.6](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.6)
140    /// for more information.
141    pub fn set_etag(&mut self, etag: Option<ETag>) {
142        self.etag = etag
143    }
144
145    /// Returns the "Observe" option value for this request.
146    pub fn observe(&self) -> Option<Observe> {
147        self.observe
148    }
149
150    /// Sets the "Observe" option value for this response.
151    ///
152    /// This option indicates that this response is a notification for a previously requested
153    /// resource observation.
154    ///
155    /// This option is defined in [RFC 7641](https://datatracker.ietf.org/doc/html/rfc7641) and is
156    /// not part of the main CoAP spec. Some peers may therefore not support this option.
157    pub fn set_observe(&mut self, observe: Option<Observe>) {
158        self.observe = observe;
159    }
160
161    /// Returns the "Location" option value for this request.
162    pub fn location(&self) -> Option<&CoapResponseLocation> {
163        self.location.as_ref()
164    }
165
166    /// Sets the "Location-Path" and "Location-Query" option values for this response.
167    ///
168    /// These options indicate a relative URI for a resource created in response of a POST or PUT
169    /// request.
170    ///
171    /// The supplied URI must be relative to the requested path and must therefore also not contain
172    /// a scheme, host or port. Also, each path component must be smaller than 255 characters.
173    ///
174    /// If an invalid URI is provided, an [OptionValueError] is returned
175    ///
176    /// See [RFC 7252, Section 5.10.7](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.7)
177    /// for more information.
178    pub fn set_location<U: Into<CoapUri>>(&mut self, uri: Option<U>) -> Result<(), OptionValueError> {
179        let uri = uri.map(Into::into);
180        if let Some(uri) = uri {
181            self.location = Some(CoapResponseLocation::new_response_location(uri)?)
182        }
183        Ok(())
184    }
185
186    /// Converts this request into a [CoapMessage] that can be sent over a [CoapSession](crate::session::CoapSession).
187    pub fn into_message(mut self) -> CoapMessage {
188        if let Some(loc) = self.location {
189            loc.into_options().into_iter().for_each(|v| self.pdu.add_option(v));
190        }
191        if let Some(max_age) = self.max_age {
192            self.pdu.add_option(CoapOption::MaxAge(max_age));
193        }
194        if let Some(content_format) = self.content_format {
195            self.pdu.add_option(CoapOption::ContentFormat(content_format));
196        }
197        if let Some(etag) = self.etag {
198            self.pdu.add_option(CoapOption::ETag(etag));
199        }
200        if let Some(observe) = self.observe {
201            self.pdu.add_option(CoapOption::Observe(observe));
202        }
203        self.pdu
204    }
205
206    /// Parses the given [CoapMessage] into a CoapResponse.
207    ///
208    /// Returns a [MessageConversionError] if the provided PDU cannot be parsed into a response.
209    pub fn from_message(pdu: CoapMessage) -> Result<CoapResponse, MessageConversionError> {
210        let mut location_path = None;
211        let mut location_query = None;
212        let mut max_age = None;
213        let mut etag = None;
214        let mut observe = None;
215        let mut content_format = None;
216        let mut additional_opts = Vec::new();
217        for option in pdu.options_iter() {
218            match option {
219                CoapOption::LocationPath(value) => {
220                    if location_path.is_none() {
221                        location_path = Some(Vec::new());
222                    }
223                    location_path.as_mut().unwrap().push(value.clone());
224                },
225                CoapOption::LocationQuery(value) => {
226                    if location_query.is_none() {
227                        location_query = Some(Vec::new());
228                    }
229                    location_query.as_mut().unwrap().push(value.clone());
230                },
231                CoapOption::ETag(value) => {
232                    if etag.is_some() {
233                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
234                            CoapOptionType::ETag,
235                        ));
236                    }
237                    etag = Some(value.clone());
238                },
239                CoapOption::MaxAge(value) => {
240                    if max_age.is_some() {
241                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
242                            CoapOptionType::MaxAge,
243                        ));
244                    }
245                    max_age = Some(*value);
246                },
247                CoapOption::Observe(value) => {
248                    if observe.is_some() {
249                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
250                            CoapOptionType::Observe,
251                        ));
252                    }
253                    observe = Some(*value)
254                },
255                CoapOption::IfMatch(_) => {
256                    return Err(MessageConversionError::InvalidOptionForMessageType(
257                        CoapOptionType::IfMatch,
258                    ));
259                },
260                CoapOption::IfNoneMatch => {
261                    return Err(MessageConversionError::InvalidOptionForMessageType(
262                        CoapOptionType::IfNoneMatch,
263                    ));
264                },
265                CoapOption::UriHost(_) => {
266                    return Err(MessageConversionError::InvalidOptionForMessageType(
267                        CoapOptionType::UriHost,
268                    ));
269                },
270                CoapOption::UriPort(_) => {
271                    return Err(MessageConversionError::InvalidOptionForMessageType(
272                        CoapOptionType::UriPort,
273                    ));
274                },
275                CoapOption::UriPath(_) => {
276                    return Err(MessageConversionError::InvalidOptionForMessageType(
277                        CoapOptionType::UriPath,
278                    ));
279                },
280                CoapOption::UriQuery(_) => {
281                    return Err(MessageConversionError::InvalidOptionForMessageType(
282                        CoapOptionType::UriQuery,
283                    ));
284                },
285                CoapOption::ProxyUri(_) => {
286                    return Err(MessageConversionError::InvalidOptionForMessageType(
287                        CoapOptionType::ProxyUri,
288                    ));
289                },
290                CoapOption::ProxyScheme(_) => {
291                    return Err(MessageConversionError::InvalidOptionForMessageType(
292                        CoapOptionType::ProxyScheme,
293                    ));
294                },
295                CoapOption::ContentFormat(value) => {
296                    if content_format.is_some() {
297                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
298                            CoapOptionType::ContentFormat,
299                        ));
300                    }
301                    content_format = Some(*value)
302                },
303                CoapOption::Accept(_) => {
304                    return Err(MessageConversionError::InvalidOptionForMessageType(
305                        CoapOptionType::Accept,
306                    ));
307                },
308                CoapOption::Size1(_) => {
309                    return Err(MessageConversionError::InvalidOptionForMessageType(
310                        CoapOptionType::Size1,
311                    ));
312                },
313                CoapOption::Size2(_) => {},
314                CoapOption::Block1(_) => {
315                    return Err(MessageConversionError::InvalidOptionForMessageType(
316                        CoapOptionType::Block1,
317                    ));
318                },
319                CoapOption::Block2(_) => {},
320                CoapOption::HopLimit(_) => {
321                    return Err(MessageConversionError::InvalidOptionForMessageType(
322                        CoapOptionType::HopLimit,
323                    ));
324                },
325                CoapOption::NoResponse(_) => {
326                    return Err(MessageConversionError::InvalidOptionForMessageType(
327                        CoapOptionType::NoResponse,
328                    ));
329                },
330                CoapOption::Other(n, v) => additional_opts.push(CoapOption::Other(*n, v.clone())),
331            }
332        }
333        let location = if location_path.is_some() || location_query.is_some() {
334            Some(
335                CoapResponseLocation::new_response_location(CoapUri::new(
336                    None,
337                    None,
338                    None,
339                    location_path,
340                    location_query,
341                ))
342                .map_err(|e| MessageConversionError::InvalidOptionValue(None, e))?,
343            )
344        } else {
345            None
346        };
347        Ok(CoapResponse {
348            pdu,
349            content_format,
350            max_age,
351            etag,
352            location,
353            observe,
354        })
355    }
356}
357
358impl CoapMessageCommon for CoapResponse {
359    /// Sets the message code of this response.
360    ///
361    /// # Panics
362    /// Panics if the provided message code is not a response code.
363    fn set_code<C: Into<CoapMessageCode>>(&mut self, code: C) {
364        match code.into() {
365            CoapMessageCode::Response(req) => self.pdu.set_code(CoapMessageCode::Response(req)),
366            CoapMessageCode::Request(_) | CoapMessageCode::Empty => {
367                panic!("attempted to set message code of response to value that is not a response code")
368            },
369        }
370    }
371
372    fn as_message(&self) -> &CoapMessage {
373        &self.pdu
374    }
375
376    fn as_message_mut(&mut self) -> &mut CoapMessage {
377        &mut self.pdu
378    }
379}