blather/
telegram.rs

1//! Telegrams are objects that contain a _topic_ and a set of zero or more
2//! parameters.  They can be serialized into a line-based format for
3//! transmission over a network link.
4
5use std::{
6  collections::HashMap,
7  fmt,
8  ops::{Deref, DerefMut}
9};
10
11use bytes::{BufMut, BytesMut};
12
13use crate::err::Error;
14
15use super::{params::Params, validators::validate_topic};
16
17/// Representation of a Telegram; a buffer which contains a _topic_ and a set
18/// of key/value parameters.
19///
20/// Internally the key/value parameters are represented by a [`Params`]
21/// structure.
22#[derive(Debug, Clone)]
23pub struct Telegram {
24  topic: String,
25  params: Params
26}
27
28impl Deref for Telegram {
29  type Target = Params;
30
31  fn deref(&self) -> &Self::Target {
32    &self.params
33  }
34}
35
36impl DerefMut for Telegram {
37  fn deref_mut(&mut self) -> &mut Self::Target {
38    &mut self.params
39  }
40}
41
42impl AsRef<str> for Telegram {
43  fn as_ref(&self) -> &str {
44    &self.topic
45  }
46}
47
48impl Telegram {
49  /// Create a new telegram object.
50  ///
51  /// # Panics
52  /// The `topic` must be valid.
53  #[must_use]
54  #[allow(clippy::needless_pass_by_value)]
55  pub fn new(topic: impl ToString) -> Self {
56    let topic = topic.to_string();
57    assert!(validate_topic(&topic).is_ok());
58    Self {
59      topic,
60      params: Params::default()
61    }
62  }
63
64  /// Fallibly create a new `Telegram` object.
65  ///
66  /// # Errors
67  /// [`Error::BadFormat`] means the topic is invalid.
68  pub fn try_new(topic: impl Into<String>) -> Result<Self, Error> {
69    let topic = topic.into();
70    validate_topic(&topic)?;
71    Ok(Self {
72      topic,
73      params: Params::default()
74    })
75  }
76
77  /// Create a new `Telegram` object with pre-built [`Params`].
78  ///
79  /// # Panics
80  /// The `topic` must be valid.
81  pub fn with_params(topic: impl Into<String>, params: Params) -> Self {
82    let topic = topic.into();
83    validate_topic(&topic).unwrap();
84    Self { topic, params }
85  }
86
87  /// Fallibly create a new `Telegram`, with a pre-built [`Params`].
88  ///
89  /// # Errors
90  /// [`Error::BadFormat`] means the topic is invalid.
91  #[allow(clippy::needless_pass_by_value)]
92  pub fn try_with_params(
93    topic: impl ToString,
94    params: Params
95  ) -> Result<Self, Error> {
96    let topic = topic.to_string();
97    validate_topic(&topic)?;
98    Ok(Self { topic, params })
99  }
100
101  /// Internal factory function for creating a `Telegram` with an empty topic.
102  ///
103  /// This is intended to be used only by the codec.
104  pub(crate) fn new_uninit() -> Self {
105    Self {
106      topic: String::new(),
107      params: Params::default()
108    }
109  }
110
111  /// Return the number of key/value parameters in the Telegram object.
112  ///
113  /// # Examples
114  /// ```
115  /// use blather::Telegram;
116  ///
117  /// let mut tg = Telegram::new("SomeTopic");
118  /// assert_eq!(tg.num_params(), 0);
119  /// tg.add_param("cat", "meow");
120  /// assert_eq!(tg.num_params(), 1);
121  /// ```
122  ///
123  /// # Notes
124  /// This is a wrapper around [`Params::len()`](crate::Params::len).
125  #[must_use]
126  pub fn num_params(&self) -> usize {
127    self.params.len()
128  }
129
130  /// Get a reference to the internal parameters object.
131  #[must_use]
132  pub const fn params(&self) -> &Params {
133    &self.params
134  }
135
136  /// Get a mutable reference to the inner [`Params`] object.
137  ///
138  /// ```
139  /// use blather::Telegram;
140  ///
141  /// let mut tg = Telegram::new("Topic");
142  /// tg.add_param("cat", "meow");
143  /// assert_eq!(tg.num_params(), 1);
144  /// tg.params_mut().clear();
145  /// assert_eq!(tg.num_params(), 0);
146  /// ```
147  pub const fn params_mut(&mut self) -> &mut Params {
148    &mut self.params
149  }
150
151  /// Get a reference the the parameter's internal `HashMap`.
152  ///
153  /// Note: The inner representation of the Params object may change in the
154  /// future.
155  #[must_use]
156  pub const fn get_params_inner(&self) -> &HashMap<String, String> {
157    self.params.inner()
158  }
159
160  /// Set topic for telegram.
161  ///
162  /// Overwrites current topic is one has already been set.
163  ///
164  /// # Examples
165  /// ```
166  /// use blather::{Telegram, Error};
167  ///
168  /// let mut tg = Telegram::new("SomeTopic");
169  /// assert!(matches!(tg.set_topic("Hello"), Ok(())));
170  ///
171  /// let e = Error::BadFormat("Invalid topic character".to_string());
172  /// assert!(matches!(tg.set_topic("Hell o"), Err(e)));
173  /// ```
174  ///
175  /// # Errors
176  /// [`Error::BadFormat`] means the input parameters are invalid.
177  pub fn set_topic(&mut self, topic: &str) -> Result<(), Error> {
178    validate_topic(topic)?;
179    self.topic = topic.to_string();
180    Ok(())
181  }
182
183  /// Get a reference to the topic string, or None if topic is not been set.
184  ///
185  /// # Examples
186  /// ```
187  /// use blather::{Telegram, Error};
188  ///
189  /// let tg = Telegram::new("SomeTopic");
190  /// assert_eq!(tg.get_topic(), "SomeTopic");
191  /// ```
192  #[must_use]
193  pub fn get_topic(&self) -> &str {
194    self.topic.as_ref()
195  }
196
197  /// Calculate the size of a serialized version of this Telegram object.
198  /// If no topic has been set it is simply ignored.  In the future this might
199  /// change to something more dramatic, like a panic.  Telegrams should always
200  /// contain a topic when transmitted.
201  ///
202  /// Each line is terminated by a newline character.
203  /// The last line consists of a single newline character.
204  #[must_use]
205  pub fn calc_buf_size(&self) -> usize {
206    // Calculate the required buffer size
207    let mut size = 0;
208    size += self.topic.len() + 1; // including '\n'
209
210    // Note that the Params method reserves the final terminating newline.
211    size + self.params.calc_buf_size()
212  }
213
214  /// Serialize `Telegram` into a vector of bytes for transmission.
215  ///
216  /// # Errors
217  /// [`Error::BadFormat`] the `Telegram` is missing a topic.
218  pub fn serialize(&self) -> Result<Vec<u8>, Error> {
219    let mut buf = Vec::new();
220
221    if self.topic.is_empty() {
222      return Err(Error::BadFormat("Missing heading".to_string()));
223    }
224
225    // Copy topic
226    let b = self.topic.as_bytes();
227    for a in b {
228      buf.push(*a);
229    }
230    buf.push(b'\n');
231
232    for (key, value) in self.get_params_inner() {
233      let k = key.as_bytes();
234      let v = value.as_bytes();
235      for a in k {
236        buf.push(*a);
237      }
238      buf.push(b' ');
239      for a in v {
240        buf.push(*a);
241      }
242      buf.push(b'\n');
243    }
244
245    buf.push(b'\n');
246
247    Ok(buf)
248  }
249
250  /// Write the Telegram to a `BytesMut` buffer.
251  ///
252  /// # Errors
253  /// [`Error::SerializeError`] the `Telegram` is missing a topic.
254  pub fn encoder_write(&self, buf: &mut BytesMut) -> Result<(), Error> {
255    if self.topic.is_empty() {
256      return Err(Error::SerializeError("Missing Telegram topic".to_string()));
257    }
258
259    // Calculate the required buffer size
260    let size = self.calc_buf_size();
261
262    // Reserve space
263    buf.reserve(size);
264
265    // Write data to output buffer
266    buf.put(self.topic.as_bytes());
267    buf.put_u8(b'\n');
268
269    for (key, value) in self.get_params_inner() {
270      buf.put(key.as_bytes());
271      buf.put_u8(b' ');
272      buf.put(value.as_bytes());
273      buf.put_u8(b'\n');
274    }
275    buf.put_u8(b'\n');
276
277    Ok(())
278  }
279
280  /// Consume the Telegram buffer and return the internal parameters object.
281  #[must_use]
282  pub fn into_params(self) -> Params {
283    self.params
284  }
285
286  /// Unwrap the `Telegram` into a topic and `Params`.
287  #[must_use]
288  pub fn unwrap_topic_params(self) -> (String, Params) {
289    (self.topic, self.params)
290  }
291}
292
293impl From<String> for Telegram {
294  fn from(topic: String) -> Self {
295    Self {
296      topic,
297      params: Params::default()
298    }
299  }
300}
301
302impl TryFrom<(&str, Params)> for Telegram {
303  type Error = Error;
304
305  fn try_from(t: (&str, Params)) -> Result<Self, Self::Error> {
306    validate_topic(t.0)?;
307    Ok(Self {
308      topic: t.0.to_string(),
309      params: t.1
310    })
311  }
312}
313
314impl TryFrom<(String, Params)> for Telegram {
315  type Error = Error;
316
317  fn try_from(t: (String, Params)) -> Result<Self, Self::Error> {
318    validate_topic(&t.0)?;
319    Ok(Self {
320      topic: t.0,
321      params: t.1
322    })
323  }
324}
325
326impl fmt::Display for Telegram {
327  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
328    write!(f, "{}:{}", self.topic, self.params)
329  }
330}
331
332
333#[cfg(test)]
334mod tests {
335  use super::{Error, Params, Telegram};
336
337  #[test]
338  fn simple() {
339    let mut tg = Telegram::new("SomeTopic");
340    assert_eq!(tg.get_topic(), "SomeTopic");
341
342    tg.add_str("Foo", "bar").unwrap();
343    assert_eq!(tg.get_str("Foo").unwrap(), "bar");
344
345    assert_eq!(tg.get_str("Moo"), None);
346  }
347
348  #[test]
349  fn exist() {
350    let mut tg = Telegram::new("SomeTopic");
351
352    tg.add_str("foo", "bar").unwrap();
353    assert!(tg.contains("foo"));
354
355    assert!(!tg.contains("nonexistent"));
356  }
357
358  #[test]
359  fn integer() {
360    let mut tg = Telegram::new("SomeTopic");
361
362    assert_eq!(tg.get_topic(), "SomeTopic");
363
364    tg.add_str("Num", "64").unwrap();
365    assert_eq!(tg.get_fromstr::<u16, _>("Num").unwrap().unwrap(), 64);
366  }
367
368  #[test]
369  fn size() {
370    let mut tg = Telegram::new("SomeTopic");
371
372    tg.add_param("Num", 7_usize).unwrap();
373    assert_eq!(tg.get_fromstr::<usize, _>("Num").unwrap().unwrap(), 7);
374  }
375
376  #[test]
377  fn intoparams() {
378    let mut tg = Telegram::new("SomeTopic");
379
380    tg.add_str("Foo", "bar").unwrap();
381    assert_eq!(tg.get_str("Foo").unwrap(), "bar");
382    assert_eq!(tg.get_str("Moo"), None);
383
384    let params = tg.into_params();
385    let val = params.get_str("Foo");
386    assert_eq!(val.unwrap(), "bar");
387  }
388
389  #[test]
390  fn display() {
391    let mut tg = Telegram::new("hello");
392
393    tg.add_param("foo", "bar").unwrap();
394    let s = format!("{tg}");
395    assert_eq!(s, "hello:{foo=bar}");
396  }
397
398  #[test]
399  fn ser_size() {
400    let mut tg = Telegram::new("hello");
401
402    tg.add_str("foo", "bar").unwrap();
403    tg.add_str("moo", "cow").unwrap();
404
405    let sz = tg.calc_buf_size();
406
407    assert_eq!(sz, 6 + 8 + 8 + 1);
408  }
409
410  #[test]
411  fn bad_topic_leading() {
412    let mut tg = Telegram::new("Hello");
413    let Err(Error::BadFormat(msg)) = tg.set_topic(" SomeTopic") else {
414      panic!("Unexpectedly not Error::BadFormat");
415    };
416    assert_eq!(msg, "Invalid leading topic character");
417  }
418
419  #[test]
420  fn bad_topic() {
421    let mut tg = Telegram::new("Hello");
422    let Err(Error::BadFormat(msg)) = tg.set_topic("Some Topic") else {
423      panic!("Unexpectedly not Error::BadFormat");
424    };
425    assert_eq!(msg, "Invalid topic character");
426  }
427
428  #[test]
429  fn create_from_tuple() {
430    let mut params = Params::new();
431    params.add_str("my", "word").unwrap();
432    let tg = Telegram::try_from(("Hello", params)).unwrap();
433
434    assert_eq!(tg.get_str("my"), Some("word"));
435  }
436}
437
438// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :