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}