ros_message/
msg.rs

1use crate::{parse_msg::match_lines, DataType, Error, FieldInfo, MessagePath, Result, Value};
2use serde_derive::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::convert::TryFrom;
5use std::fmt;
6use std::fmt::Formatter;
7
8/// A ROS message parsed from a `msg` file.
9#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(into = "MsgSerde")]
11#[serde(try_from = "MsgSerde")]
12pub struct Msg {
13    path: MessagePath,
14    fields: Vec<FieldInfo>,
15    source: String,
16}
17
18impl fmt::Display for Msg {
19    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
20        self.source.fmt(f)
21    }
22}
23
24impl Msg {
25    /// Create a message from a passed in path and source.
26    ///
27    /// # Errors
28    ///
29    /// Returns an error if there is an error parsing the message source.
30    ///
31    /// # Examples
32    ///
33    /// ```
34    /// # use ros_message::Msg;
35    /// # use std::convert::TryInto;
36    /// #
37    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
38    /// let message = Msg::new(
39    ///     "foo/Bar".try_into()?,
40    ///     r#"# a comment that is ignored
41    ///     Header header
42    ///     uint32 a
43    ///     byte[16] b
44    ///     geometry_msgs/Point[] point
45    ///     uint32 FOO=5
46    ///     string SOME_TEXT=this is # some text, don't be fooled by the hash
47    ///     "#,
48    /// )?;
49    ///
50    /// assert_eq!(message.path(), &"foo/Bar".try_into()?);
51    /// assert_eq!(message.fields().len(), 6);
52    /// # Ok(())
53    /// # }
54    /// ```
55    pub fn new(path: MessagePath, source: &str) -> Result<Msg> {
56        let source = source.trim().to_owned();
57        let fields = match_lines(&source)?;
58        Ok(Msg {
59            path,
60            fields,
61            source,
62        })
63    }
64
65    /// Returns a map of all constant fields inside the message, with their values parsed.
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// # use ros_message::{Msg, Value};
71    /// # use std::convert::TryInto;
72    /// #
73    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
74    /// let message = Msg::new(
75    ///     "foo/Bar".try_into()?,
76    ///     r#"# a comment that is ignored
77    ///     Header header
78    ///     uint32 a
79    ///     byte[16] b
80    ///     geometry_msgs/Point[] point
81    ///     uint32 FOO=5
82    ///     string SOME_TEXT=this is # some text, don't be fooled by the hash
83    ///     "#,
84    /// )?;
85    ///
86    /// let constants = message.constants();
87    ///
88    /// assert_eq!(constants.len(), 2);
89    /// assert_eq!(constants.get("FOO"), Some(&Value::U32(5)));
90    /// assert_eq!(
91    ///     constants.get("SOME_TEXT"),
92    ///     Some(&Value::String("this is # some text, don't be fooled by the hash".into())),
93    /// );
94    /// # Ok(())
95    /// # }
96    /// ```
97    pub fn constants(&self) -> HashMap<String, Value> {
98        self.fields
99            .iter()
100            .filter_map(|field| {
101                let value = field.const_value()?.clone();
102                Some((field.name().into(), value))
103            })
104            .collect()
105    }
106
107    /// Returns the path of the message.
108    pub fn path(&self) -> &MessagePath {
109        &self.path
110    }
111
112    /// Returns a slice of all fields.
113    pub fn fields(&self) -> &[FieldInfo] {
114        &self.fields
115    }
116
117    /// Returns the original source.
118    pub fn source(&self) -> &str {
119        &self.source
120    }
121
122    /// Returns a all message paths that this message directly depends upon.
123    ///
124    /// They are listed in the order that they appear in in the message, and duplicates
125    /// are allowed.
126    ///
127    /// Indirect dependencies are not included, and if you want an exhaustive list of all
128    /// dependencies, you have to manually traverse every message being depended upon.
129    /// # Examples
130    ///
131    /// ```
132    /// # use ros_message::Msg;
133    /// # use std::convert::TryInto;
134    /// #
135    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
136    /// let message = Msg::new(
137    ///     "foo/Bar".try_into()?,
138    ///     r#"
139    ///     Header header
140    ///     geometry_msgs/Point[] point1
141    ///     Point[] point2
142    ///     foo/Point[] point2_but_with_global_path
143    ///     foo/Baz[] baz
144    ///     "#,
145    /// )?;
146    ///
147    /// let dependencies = message.dependencies();
148    ///
149    /// assert_eq!(dependencies, vec![
150    ///     "std_msgs/Header".try_into()?,
151    ///     "geometry_msgs/Point".try_into()?,
152    ///     "foo/Point".try_into()?,
153    ///     "foo/Point".try_into()?,
154    ///     "foo/Baz".try_into()?,
155    /// ]);
156    /// # Ok(())
157    /// # }
158    /// ```
159    pub fn dependencies(&self) -> Vec<MessagePath> {
160        self.fields
161            .iter()
162            .filter_map(|field| match field.datatype() {
163                DataType::LocalMessage(ref name) => Some(self.path.peer(name)),
164                DataType::GlobalMessage(ref message) => Some(message.clone()),
165                _ => None,
166            })
167            .collect()
168    }
169
170    /// Returns the MD5 sum of this message.
171    ///
172    /// Any direct dependency must have its MD5 sum provided in the passed in hashes.
173    ///
174    /// All direct dependencies are returned by the `dependencies()` method.
175    ///
176    /// # Errors
177    ///
178    /// An error is returned if some dependency is missing in the hashes.
179    #[cfg(test)]
180    pub fn calculate_md5(&self, hashes: &HashMap<MessagePath, String>) -> Result<String> {
181        use md5::{Digest, Md5};
182
183        let mut hasher = Md5::new();
184        hasher.update(&self.get_md5_representation(hashes)?);
185        Ok(hex::encode(hasher.finalize()))
186    }
187
188    /// Returns the full MD5 representation of the message.
189    ///
190    /// This is the string that is sent to the MD5 hasher to digest.
191    ///
192    /// # Errors
193    ///
194    /// An error is returned if some dependency is missing in the hashes.
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// # use ros_message::Msg;
200    /// # use std::convert::TryInto;
201    /// # use std::collections::HashMap;
202    /// #
203    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
204    /// let message = Msg::new(
205    ///     "foo/Bar".try_into()?,
206    ///     r#"# a comment that is ignored
207    ///     Header header
208    ///     uint32 a
209    ///     byte[16] b
210    ///     geometry_msgs/Point[] point
211    ///     Baz baz
212    ///     uint32 FOO=5
213    ///     string SOME_TEXT=this is # some text, don't be fooled by the hash
214    ///     "#,
215    /// )?;
216    ///
217    /// let mut hashes = HashMap::new();
218    /// hashes.insert("std_msgs/Header".try_into()?, "hash1".into());
219    /// hashes.insert("geometry_msgs/Point".try_into()?, "hash2".into());
220    /// hashes.insert("foo/Baz".try_into()?, "hash3".into());
221    ///
222    /// let representation = message.get_md5_representation(&hashes)?;
223    ///
224    /// assert_eq!(
225    ///     representation,
226    /// r#"uint32 FOO=5
227    /// string SOME_TEXT=this is # some text, don't be fooled by the hash
228    /// hash1 header
229    /// uint32 a
230    /// byte[16] b
231    /// hash2 point
232    /// hash3 baz"#);
233    /// # Ok(())
234    /// # }
235    /// ```
236    pub fn get_md5_representation(&self, hashes: &HashMap<MessagePath, String>) -> Result<String> {
237        let constants = self
238            .fields
239            .iter()
240            .filter(|v| v.is_constant())
241            .map(|v| v.md5_string(self.path.package(), hashes))
242            .collect::<Result<Vec<String>>>()?;
243        let fields = self
244            .fields
245            .iter()
246            .filter(|v| !v.is_constant())
247            .map(|v| v.md5_string(self.path.package(), hashes))
248            .collect::<Result<Vec<String>>>()?;
249        let representation = constants
250            .into_iter()
251            .chain(fields)
252            .collect::<Vec<_>>()
253            .join("\n");
254        Ok(representation)
255    }
256
257    /// Returns true if the message has a header field.
258    ///
259    /// A header field is a unit value named `header` of type `std_msgs/Header`.
260    /// The package can be elided in this special case, no matter the package that
261    /// the containing message is located in.
262    pub fn has_header(&self) -> bool {
263        self.fields.iter().any(FieldInfo::is_header)
264    }
265}
266
267#[derive(Serialize, Deserialize)]
268struct MsgSerde {
269    path: MessagePath,
270    source: String,
271}
272
273impl TryFrom<MsgSerde> for Msg {
274    type Error = Error;
275
276    fn try_from(src: MsgSerde) -> Result<Self> {
277        Self::new(src.path, &src.source)
278    }
279}
280
281impl From<Msg> for MsgSerde {
282    fn from(src: Msg) -> Self {
283        Self {
284            path: src.path,
285            source: src.source,
286        }
287    }
288}