Index: Cargo.toml ================================================================== --- Cargo.toml +++ Cargo.toml @@ -1,31 +1,44 @@ [package] name = "blather" -version = "0.10.1" +version = "0.11.0" edition = "2021" license = "0BSD" +# https://crates.io/category_slugs +categories = [ "network-programming" ] keywords = [ "line-based", "protocol", "tokio", "codec" ] repository = "https://repos.qrnch.tech/pub/blather" description = "A talkative line-based protocol" exclude = [ ".fossil-settings", ".efiles", ".fslckout", + "bacon.toml", "rustfmt.toml", "www" ] +# https://doc.rust-lang.org/cargo/reference/manifest.html#the-badges-section +[badges] +maintenance = { status = "actively-developed" } + [dependencies] -bytes = { version = "1.7.1" } +bytes = { version = "1.7.2" } futures = { version = "0.3.30" } -tokio = { version = "1.39.2" } -tokio-stream = { version = "0.1.15" } -tokio-util = { version= "0.7.11", features = ["codec"] } +tokio = { version = "1.40.0" } +tokio-stream = { version = "0.1.16" } +tokio-util = { version= "0.7.12", features = ["codec"] } [dev-dependencies] -tokio = { version = "1.39.2", features = ["macros", "net"] } +tokio = { version = "1.40.0", features = ["macros", "net"] } tokio-test = { version = "0.4.4" } [package.metadata.docs.rs] rustdoc-args = ["--generate-link-to-definition"] +[lints.clippy] +all = { level = "deny", priority = -1 } +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } + # vim: set ft=toml et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : ADDED bacon.toml Index: bacon.toml ================================================================== --- /dev/null +++ bacon.toml @@ -0,0 +1,101 @@ +# This is a configuration file for the bacon tool +# +# Bacon repository: https://github.com/Canop/bacon +# Complete help on configuration: https://dystroy.org/bacon/config/ +# You can also check bacon's own bacon.toml file +# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml + +default_job = "clippy-all" + +[jobs.check] +command = ["cargo", "check", "--color", "always"] +need_stdout = false + +[jobs.check-all] +command = ["cargo", "check", "--all-targets", "--color", "always"] +need_stdout = false + +# Run clippy on the default target +[jobs.clippy] +command = [ + "cargo", "clippy", + "--color", "always", +] +need_stdout = false + +# Run clippy on all targets +# To disable some lints, you may change the job this way: +# [jobs.clippy-all] +# command = [ +# "cargo", "clippy", +# "--all-targets", +# "--color", "always", +# "--", +# "-A", "clippy::bool_to_int_with_if", +# "-A", "clippy::collapsible_if", +# "-A", "clippy::derive_partial_eq_without_eq", +# ] +# need_stdout = false +[jobs.clippy-all] +command = [ + "cargo", "clippy", + "--all-targets", + "--color", "always", +] +need_stdout = false + +# This job lets you run +# - all tests: bacon test +# - a specific test: bacon test -- config::test_default_files +# - the tests of a package: bacon test -- -- -p config +[jobs.test] +command = [ + "cargo", "test", "--color", "always", + "--", "--color", "always", # see https://github.com/Canop/bacon/issues/124 +] +need_stdout = true + +[jobs.doc] +command = ["cargo", "doc", "--color", "always", "--no-deps"] +need_stdout = false + +# If the doc compiles, then it opens in your browser and bacon switches +# to the previous job +[jobs.doc-open] +command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +# You can run your application and have the result displayed in bacon, +# *if* it makes sense for this crate. +# Don't forget the `--color always` part or the errors won't be +# properly parsed. +# If your program never stops (eg a server), you may set `background` +# to false to have the cargo run output immediately displayed instead +# of waiting for program's end. +[jobs.run] +command = [ + "cargo", "run", + "--color", "always", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = true + +# This parameterized job runs the example of your choice, as soon +# as the code compiles. +# Call it as +# bacon ex -- my-example +[jobs.ex] +command = ["cargo", "run", "--color", "always", "--example"] +need_stdout = true +allow_warnings = true + +# You may define here keybindings that would be specific to +# a project, for example a shortcut to launch a specific job. +# Shortcuts to internal functions (scrolling, toggling, etc.) +# should go in your personal global prefs.toml file instead. +[keybindings] +# alt-m = "job:my-job" +c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target Index: src/codec.rs ================================================================== --- src/codec.rs +++ src/codec.rs @@ -81,19 +81,20 @@ kvlines: KVLines, state: CodecState, remain: u64 } +#[allow(clippy::missing_fields_in_debug)] impl fmt::Debug for Codec { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Codec").field("state", &self.state).finish() } } impl Default for Codec { fn default() -> Self { - Codec::new() + Self::new() } } /// A Codec used to encode and decode the blather protocol. @@ -120,39 +121,42 @@ /// ``` impl Codec { /// Create a new `Codec`. It will default to having not practical limit to /// the maximum line length and it will expect a [`Telegram`] buffer to /// arrive as the first frame. - pub fn new() -> Codec { - Codec { + #[must_use] + pub fn new() -> Self { + Self { next_line_index: 0, max_line_length: usize::MAX, - tg: Telegram::new(), + tg: Telegram::new_uninit(), params: Params::new(), kvlines: KVLines::new(), state: CodecState::Telegram, remain: 0 } } /// Create a new `Codec` with a specific maximum line length. The default /// state will be to expect a [`Telegram`]. + #[must_use] pub fn new_with_max_length(max_line_length: usize) -> Self { - Codec { + Self { max_line_length, - ..Codec::new() + ..Self::new() } } /// Get the current maximum line length. - pub fn max_line_length(&self) -> usize { + #[must_use] + pub const fn max_line_length(&self) -> usize { self.max_line_length } /// Determine how far into the buffer we'll search for a newline. If - /// there's no max_length set, we'll read to the end of the buffer. + /// there's no `max_length` set, we'll read to the end of the buffer. fn find_newline(&self, buf: &BytesMut) -> (usize, Option) { let read_to = cmp::min(self.max_line_length.saturating_add(1), buf.len()); let newline_offset = buf[self.next_line_index..read_to] .iter() .position(|b| *b == b'\n'); @@ -167,11 +171,11 @@ /// /// The first line received is a telegram topic. This is a required line. /// Following lines are parameter lines, which are a single space character /// separated key/value pairs. fn decode_telegram_line(&mut self, line: &str) -> Result<(), Error> { - if self.tg.get_topic().is_none() { + if self.tg.get_topic().is_empty() { self .tg .set_topic(line) .map_err(|e| Error::Protocol(e.to_string()))?; } else { @@ -264,14 +268,16 @@ // Empty line marks end of Telegram if line.is_empty() { // mem::take() can replace a member of a struct. // (This requires Default to be implemented for the object being // taken). - return Ok(Some(mem::take(&mut self.tg))); - } else { - self.decode_telegram_line(line)?; + let newtg = Telegram::new_uninit(); + + //return Ok(Some(mem::take(&mut self.tg))); + return Ok(Some(mem::replace(&mut self.tg, newtg))); } + self.decode_telegram_line(line)?; } else { // Returning Ok(None) instructs the FramedRead that more data is // needed. return Ok(None); } @@ -301,17 +307,16 @@ // mem::take() can replace a member of a struct. // (This requires Default to be implemented for the object being // taken). return Ok(Some(mem::take(&mut self.params))); - } else { - let idx = line.find(' '); - if let Some(idx) = idx { - let (k, v) = line.split_at(idx); - let v = &v[1..v.len()]; - self.params.add_param(k, v)?; - } + } + let idx = line.find(' '); + if let Some(idx) = idx { + let (k, v) = line.split_at(idx); + let v = &v[1..v.len()]; + self.params.add_param(k, v)?; } } else { // Need more data return Ok(None); } @@ -341,17 +346,16 @@ // mem::take() can replace a member of a struct. // (This requires Default to be implemented for the object being // taken). return Ok(Some(mem::take(&mut self.kvlines))); - } else { - let idx = line.find(' '); - if let Some(idx) = idx { - let (k, v) = line.split_at(idx); - let v = &v[1..v.len()]; - self.kvlines.append(k, v); - } + } + let idx = line.find(' '); + if let Some(idx) = idx { + let (k, v) = line.split_at(idx); + let v = &v[1..v.len()]; + self.kvlines.append(k, v); } } else { // Need more data return Ok(None); } @@ -358,11 +362,11 @@ } } /// Set the decoder to treat the next `size` bytes as raw bytes to be - /// received in chunks as BytesMut. + /// received in chunks as `Bytes`. /// /// # Decoder behavior /// The decoder will return an [`Input::Chunk(buf, remain)`](Input::Chunk) to /// the application each time a new chunk has been received. In addition to /// the actual chunk the number of bytes remaining will be returned. The @@ -370,10 +374,13 @@ /// chunk, which means that the application can detect the end of the /// buffer by checking if the remaining value is zero. /// /// Once the entire buffer has been received by the `Decoder` it will revert /// to expect an [`Input::Telegram`]. + /// + /// # Errors + /// [`Error::InvalidSize`] means the `size` parameter was set to `0`. pub fn expect_chunks(&mut self, size: u64) -> Result<(), Error> { if size == 0 { return Err(Error::InvalidSize("The size must not be zero".to_string())); } @@ -394,10 +401,14 @@ /// return an [`Input::Bytes(b)`](Input::Bytes) where `b` is a /// [`bytes::Bytes`] containing the entire buffer. /// /// Once the entire buffer has been received by the `Decoder` it will revert /// to expect an [`Input::Telegram`]. + /// + /// # Errors + /// [`Error::InvalidSize`] means the `size` parameter was set to `0`. + #[allow(clippy::missing_panics_doc)] pub fn expect_bytes(&mut self, size: usize) -> Result<(), Error> { if size == 0 { return Err(Error::InvalidSize("The size must not be zero".to_string())); } self.state = CodecState::Bytes; @@ -424,11 +435,11 @@ } /// Tell the Decoder to expect lines ordered key/value pairs. /// /// # Decoder behavior - /// On successful completion the Framed StreamExt next() will return an + /// On successful completion the Framed `StreamExt::next()` will return an /// [`Input::KVLines(kvlines)`](Input::KVLines) once a complete `KVLines` /// buffer has been received. /// /// Once the entire buffer has been received by the `Decoder` it will revert /// to expect an [`Input::Telegram`]. @@ -440,10 +451,13 @@ /// /// # Decoder behavior /// On successful completion the decoder will have ignored the specified /// number of byes, reverts back to waiting for a [`Input::Telegram`] and /// returns [`Input::SkipDone`]. + /// + /// # Errors + /// [`Error::InvalidSize`] means the `size` parameter was set to `0`. pub fn skip(&mut self, size: u64) -> Result<(), Error> { if size == 0 { return Err(Error::InvalidSize("The size must not be zero".to_string())); } self.state = CodecState::Skip; @@ -460,11 +474,11 @@ ) }) } fn without_carriage_return(s: &[u8]) -> &[u8] { - if let Some(&b'\r') = s.last() { + if s.last() == Some(&b'\r') { &s[..s.len() - 1] } else { s } } @@ -537,16 +551,14 @@ // Return a buffer and the amount of data remaining, this buffer // included. The application can check if remain is 0 to determine // if it has received all the expected binary data. // - // The `as usize` cast is safe to do, because read_to is guaranteed to + // .unwrap() is safe here, because read_to is guaranteed to // be within the bounds of an usize due to the `cmp::min()` above. - Ok(Some(Input::Chunk( - buf.split_to(read_to as usize).freeze(), - self.remain - ))) + let len = usize::try_from(read_to).unwrap(); + Ok(Some(Input::Chunk(buf.split_to(len).freeze(), self.remain))) } CodecState::Bytes => { // This is guaranteed to work, because expect_bytes() takes in an // usize. let remain: usize = self.remain.try_into().unwrap(); @@ -565,11 +577,14 @@ } // Read as much data as available or requested and write it to our // output. let read_to = cmp::min(self.remain, buf.len() as u64); - let _ = buf.split_to(read_to as usize); + + // unwrap() is okay here, due to the cmd::min() above + let len = usize::try_from(read_to).unwrap(); + let _ = buf.split_to(len); self.remain -= read_to; if self.remain != 0 { return Ok(None); // Need more data } @@ -604,11 +619,11 @@ fn encode( &mut self, params: &Params, buf: &mut BytesMut ) -> Result<(), Error> { - params.encoder_write(buf)?; + params.encoder_write(buf); Ok(()) } } @@ -620,11 +635,11 @@ data: &HashMap, buf: &mut BytesMut ) -> Result<(), Error> { // Calculate the amount of space required let mut sz = 0; - for (k, v) in data.iter() { + for (k, v) in data { // key space + whitespace + value space + eol sz += k.len() + 1 + v.len() + 1; } // Terminating empty line @@ -631,11 +646,11 @@ sz += 1; //println!("Writing {} bin data", data.len()); buf.reserve(sz); - for (k, v) in data.iter() { + for (k, v) in data { buf.put(k.as_bytes()); buf.put_u8(b' '); buf.put(v.as_bytes()); buf.put_u8(b'\n'); } @@ -652,11 +667,11 @@ fn encode( &mut self, kvlines: &KVLines, buf: &mut BytesMut ) -> Result<(), Error> { - kvlines.encoder_write(buf)?; + kvlines.encoder_write(buf); Ok(()) } } Index: src/codec/utils.rs ================================================================== --- src/codec/utils.rs +++ src/codec/utils.rs @@ -29,17 +29,18 @@ /// /// The callback in `validate` can be used to inspect the received `Telegram` /// and choose whether to return/ignore it or return a fatal error. /// /// If the connection is closed this function will return `Ok(None)`. -pub async fn expect_telegram( - frmio: &mut Framed, +#[allow(clippy::missing_errors_doc)] +pub async fn expect_telegram( + frmio: &mut Framed, validate: F ) -> Result, Error> where - T: AsyncRead + Unpin, - F: Fn(&Telegram) -> Outcome + C: AsyncRead + Unpin + Send, + F: Fn(&Telegram) -> Outcome + Send { loop { let Some(frm) = frmio.next().await else { // Got None, which (probably) means the connection has ended. break Ok(None); Index: src/err.rs ================================================================== --- src/err.rs +++ src/err.rs @@ -3,11 +3,11 @@ use std::fmt; use tokio::io; /// Error that `blather` can emit. -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub enum Error { /// The input format of a buffer was incorrect. BadFormat(String), /// Something occurred which was unexpected in the current state. @@ -15,11 +15,11 @@ /// The specified size is invalid, or invalid in a specific context. InvalidSize(String), /// A `std::io` or `tokio::io` error has occurred. - IO(String), + IO(std::io::Error), /// The requiested key was not found. KeyNotFound(String), /// Unable to serialize a buffer. @@ -33,25 +33,25 @@ impl std::error::Error for Error {} impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Error::BadFormat(s) => write!(f, "Bad format; {}", s), - Error::BadState(s) => { - write!(f, "Encountred an unexpected/bad state: {}", s) - } - Error::InvalidSize(s) => write!(f, "Invalid size; {}", s), - Error::IO(s) => write!(f, "I/O error; {}", s), - Error::KeyNotFound(s) => write!(f, "Parameter '{}' not found", s), - Error::Protocol(s) => write!(f, "Protocol error; {}", s), - Error::SerializeError(s) => write!(f, "Unable to serialize; {}", s) + Self::BadFormat(s) => write!(f, "Bad format; {s}"), + Self::BadState(s) => { + write!(f, "Encountred an unexpected/bad state: {s}") + } + Self::InvalidSize(s) => write!(f, "Invalid size; {s}"), + Self::IO(e) => write!(f, "I/O error; {e}"), + Self::KeyNotFound(s) => write!(f, "Parameter '{s}' not found"), + Self::Protocol(s) => write!(f, "Protocol error; {s}"), + Self::SerializeError(s) => write!(f, "Unable to serialize; {s}") } } } impl From for Error { fn from(err: io::Error) -> Self { - Error::IO(err.to_string()) + Self::IO(err) } } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/lib.rs ================================================================== --- src/lib.rs +++ src/lib.rs @@ -11,17 +11,17 @@ //! be unique. //! //! ``` //! use blather::Telegram; //! -//! let mut tg = Telegram::new_topic("AddUser").unwrap(); +//! let mut tg = Telegram::new("AddUser"); //! //! tg.add_param("Name", "Frank Foobar"); //! tg.add_param("Job", "Secret Agent"); //! tg.add_param("Age", "42"); //! -//! assert_eq!(tg.get_topic(), Some("AddUser")); +//! assert_eq!(tg.get_topic(), "AddUser"); //! assert_eq!(tg.get_str("Name").unwrap(), "Frank Foobar"); //! assert_eq!(tg.get_param::("Age").unwrap(), 42); //! ``` //! //! ## Params Index: src/types/kvlines.rs ================================================================== --- src/types/kvlines.rs +++ src/types/kvlines.rs @@ -2,12 +2,10 @@ use std::fmt; use bytes::{BufMut, BytesMut}; -use crate::err::Error; - /// Representation of a key/value pair in `KVLines`. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct KeyValue { key: String, value: String @@ -20,37 +18,39 @@ lines: Vec } impl KVLines { /// Create a new empty parameters object. + #[must_use] pub fn new() -> Self { - KVLines { - ..Default::default() - } + Self::default() } /// Reset all the lines. pub fn clear(&mut self) { self.lines.clear(); } /// Get a reference to the inner vector of [`KeyValue`]'s. - pub fn get_inner(&self) -> &Vec { + #[must_use] + pub const fn get_inner(&self) -> &Vec { &self.lines } /// Append a key/value entry to the end of the list. - pub fn append(&mut self, key: T, value: U) { + #[allow(clippy::needless_pass_by_value)] + pub fn append(&mut self, key: impl ToString, value: impl ToString) { self.lines.push(KeyValue { key: key.to_string(), value: value.to_string() }); } /// Calculate the size of the buffer in serialized form. /// Each entry will be a newline terminated utf-8 line. /// Last line will be a single newline character. + #[must_use] pub fn calc_buf_size(&self) -> usize { let mut size = 0; for n in &self.lines { size += n.key.len() + 1; // including ' ' size += n.value.len() + 1; // including '\n' @@ -58,11 +58,12 @@ size + 1 // terminating '\n' } /// Serialize object into a `Vec` buffer suitable for transmission. - pub fn serialize(&self) -> Result, Error> { + #[must_use] + pub fn serialize(&self) -> Vec { let mut buf = Vec::new(); for n in &self.lines { let k = n.key.as_bytes(); let v = n.value.as_bytes(); @@ -76,15 +77,15 @@ buf.push(b'\n'); } buf.push(b'\n'); - Ok(buf) + buf } /// Write the Params to a buffer. - pub fn encoder_write(&self, buf: &mut BytesMut) -> Result<(), Error> { + pub fn encoder_write(&self, buf: &mut BytesMut) { // Calculate the required buffer size let size = self.calc_buf_size(); // Reserve space buf.reserve(size); @@ -95,31 +96,30 @@ buf.put_u8(b' '); buf.put(n.value.as_bytes()); buf.put_u8(b'\n'); } buf.put_u8(b'\n'); - - Ok(()) } /// Consume the Params buffer and return the internal key/value list as a /// `Vec` + #[must_use] pub fn into_inner(self) -> Vec { self.lines } } impl From> for KVLines { fn from(lines: Vec) -> Self { - KVLines { lines } + Self { lines } } } impl From> for KVLines { fn from(lines: Vec<(String, String)>) -> Self { - let mut out = KVLines { lines: Vec::new() }; + let mut out = Self { lines: Vec::new() }; for (key, value) in lines { out.append(key, value); } out } Index: src/types/params.rs ================================================================== --- src/types/params.rs +++ src/types/params.rs @@ -24,14 +24,13 @@ hm: HashMap } impl Params { /// Create a new empty parameters object. + #[must_use] pub fn new() -> Self { - Params { - ..Default::default() - } + Self::default() } /// Reset all the key/values in `Params` object. pub fn clear(&mut self) { @@ -38,24 +37,27 @@ self.hm.clear(); } /// Return the number of key/value pairs in the parameter buffer. + #[must_use] pub fn len(&self) -> usize { self.hm.len() } /// Returns `true` if the `Params` collection does not contain any key/value /// pairs. + #[must_use] pub fn is_empty(&self) -> bool { self.len() == 0 } - /// Return reference to inner HashMap. - pub fn get_inner(&self) -> &HashMap { + /// Return reference to inner `HashMap`. + #[must_use] + pub const fn get_inner(&self) -> &HashMap { &self.hm } /// Add a parameter to the parameter. @@ -69,14 +71,18 @@ /// /// let mut params = Params::new(); /// params.add_param("integer", 42).unwrap(); /// params.add_param("string", "hello").unwrap(); /// ``` - pub fn add_param( + /// + /// # Errors + /// [`Error::BadFormat`] means the input parameters are invalid. + #[allow(clippy::needless_pass_by_value)] + pub fn add_param( &mut self, - key: T, - value: U + key: impl ToString, + value: impl ToString ) -> Result<(), Error> { let key = key.to_string(); validate_param_key(&key)?; @@ -89,10 +95,13 @@ /// /// # Notes /// - This method exists for parity with a C++ interface and is a less /// flexible version of [`add_param()`](Self::add_param), which application /// should use instead. + /// + /// # Errors + /// [`Error::BadFormat`] means the input parameters are invalid. pub fn add_str(&mut self, key: &str, value: &str) -> Result<(), Error> { self.add_param(key, value) } @@ -116,17 +125,20 @@ /// let mut hs = HashSet::new(); /// hs.insert("Elena"); /// hs.insert("Drake"); /// params.add_strit("Uncharted", hs.into_iter()).unwrap(); /// ``` + /// + /// # Errors + /// [`Error::BadFormat`] means the input parameters are invalid. pub fn add_strit(&mut self, key: &str, c: I) -> Result<(), Error> where I: IntoIterator, S: AsRef { let mut sv = Vec::new(); - for o in c.into_iter() { + for o in c { sv.push(o.as_ref().to_string()); } self.add_param(key, sv.join(","))?; Ok(()) @@ -140,33 +152,34 @@ /// use blather::Params; /// /// let mut params = Params::new(); /// params.add_bool("should_be_true", true).unwrap(); /// params.add_bool("should_be_false", false).unwrap(); - /// assert_eq!(params.get_bool("should_be_true"), Ok(true)); - /// assert_eq!(params.get_bool("should_be_false"), Ok(false)); + /// assert!(matches!(params.get_bool("should_be_true"), Ok(true))); + /// assert!(matches!(params.get_bool("should_be_false"), Ok(false))); /// ``` /// /// # Notes /// - Applications should not make assumptions about the specific string /// value added by this function. Do not treat boolean values as strings; /// use the [`get_bool()`](Self::get_bool) method instead. - pub fn add_bool( + /// + /// # Errors + /// [`Error::BadFormat`] means the input parameters are invalid. + pub fn add_bool( &mut self, - key: K, + key: impl ToString, value: bool ) -> Result<(), Error> { - let v = match value { - true => "True", - false => "False" - }; + let v = if value { "True" } else { "False" }; self.add_param(key, v) } /// Returns `true` if the parameter with `key` exists. Returns `false` /// otherwise. + #[must_use] pub fn have(&self, key: &str) -> bool { self.hm.contains_key(key) } @@ -180,23 +193,31 @@ /// let mut params = Params::new(); /// params.add_param("arthur", 42); /// let fourtytwo = params.get_param::("arthur").unwrap(); /// assert_eq!(fourtytwo, 42); /// let nonexist = params.get_param::("ford"); - /// assert_eq!(nonexist, Err(Error::KeyNotFound("ford".to_string()))); + /// let expect = Error::KeyNotFound(String::from("ford")); + /// assert!(matches!(nonexist, Err(expect))); /// ``` + /// + /// # Errors + /// [`Error::KeyNotFound`] means the requested parameter does not exist. + /// [`Error::BadFormat`] means the input parameters are invalid. pub fn get_param(&self, key: &str) -> Result { - if let Some(val) = self.get_str(key) { - if let Ok(v) = T::from_str(val) { - return Ok(v); - } - return Err(Error::BadFormat(format!( - "Unable to parse value from parameter '{}'", - key - ))); - } - Err(Error::KeyNotFound(key.to_string())) + self.get_str(key).map_or_else( + || Err(Error::KeyNotFound(key.to_string())), + |val| { + T::from_str(val).map_or_else( + |_| { + Err(Error::BadFormat(format!( + "Unable to parse value from parameter '{key}'" + ))) + }, + |v| Ok(v) + ) + } + ) } /// Get a parameter and convert it to a requested type, return a default /// value if key isn't found. @@ -205,33 +226,41 @@ /// ``` /// use blather::Params; /// /// let mut params = Params::new(); /// let val = params.get_param_def::("nonexist", 11); - /// assert_eq!(val, Ok(11)); + /// assert!(matches!(val, Ok(11))); /// ``` + /// + /// # Errors + /// [`Error::KeyNotFound`] means the requested parameter does not exist. + /// [`Error::BadFormat`] means the input parameters are invalid. pub fn get_param_def( &self, key: &str, def: T ) -> Result { - if let Some(val) = self.get_str(key) { - if let Ok(v) = T::from_str(val) { - return Ok(v); - } - return Err(Error::BadFormat(format!( - "Unable to parse value from parameter '{}'", - key - ))); - } - Ok(def) + self.get_str(key).map_or_else( + || Ok(def), + |val| { + T::from_str(val).map_or_else( + |_| { + Err(Error::BadFormat(format!( + "Unable to parse value from parameter '{key}'" + ))) + }, + |v| Ok(v) + ) + } + ) } /// Get string representation of a value for a requested key. /// Returns `None` if the key is not found in the inner storage. Returns /// `Some(&str)` if parameter exists. + #[must_use] pub fn get_str(&self, key: &str) -> Option<&str> { let kv = self.hm.get_key_value(key); if let Some((_k, v)) = kv { return Some(v); } @@ -250,10 +279,11 @@ /// let e = params.get_str_def("nonexist", "elena"); /// assert_eq!(e, "elena"); /// ``` // Lifetimes of self and def don't really go hand-in-hand, but we bound them // together for the sake of the return value's lifetime. + #[must_use] pub fn get_str_def<'a>(&'a self, key: &str, def: &'a str) -> &'a str { let kv = self.hm.get_key_value(key); if let Some((_k, v)) = kv { v } else { @@ -275,23 +305,30 @@ /// /// # Notes /// - This method exists primarily to achive some sort of parity with a /// corresponding C++ library. It is recommended that applications use /// [`Params::get_param()`](Self::get_param) instead. + /// + /// # Errors + /// [`Error::KeyNotFound`] means the requested parameter does not exist. + /// [`Error::BadFormat`] means the input parameters are invalid. // This method should really have some integer trait bound, but it doesn't // seem to exist in the standard library. pub fn get_int(&self, key: &str) -> Result { - if let Some(val) = self.get_str(key) { - if let Ok(v) = T::from_str(val) { - return Ok(v); - } - return Err(Error::BadFormat(format!( - "Unable to parse numeric value from parameter '{}'", - key - ))); - } - Err(Error::KeyNotFound(key.to_string())) + self.get_str(key).map_or_else( + || Err(Error::KeyNotFound(key.to_string())), + |val| { + T::from_str(val).map_or_else( + |_| { + Err(Error::BadFormat(format!( + "Unable to parse numeric value from parameter '{key}'" + ))) + }, + |v| Ok(v) + ) + } + ) } /// Try to get the value of a key and interpret it as an integer. If the key /// does not exist then return a default value supplied by the caller. @@ -307,29 +344,38 @@ /// ``` /// /// # Notes /// - It is recommended that application use /// [`Params::get_param_def()`](Self::get_param_def) instead. + /// + /// # Errors + /// [`Error::BadFormat`] means the value's format is invalid. pub fn get_int_def( &self, key: &str, def: T ) -> Result { - if let Some(val) = self.get_str(key) { - if let Ok(v) = T::from_str(val) { - return Ok(v); - } - return Err(Error::BadFormat(format!( - "Unable to parse numeric value from parameter '{}'", - key - ))); - } - Ok(def) + self.get_str(key).map_or_else( + || Ok(def), + |val| { + T::from_str(val).map_or_else( + |_| { + Err(Error::BadFormat(format!( + "Unable to parse numeric value from parameter '{key}'" + ))) + }, + |v| Ok(v) + ) + } + ) } /// Get a boolean value; return error if key wasn't found. + /// + /// # Errors + /// [`Error::BadFormat`] means the input parameters are invalid. pub fn get_bool(&self, key: &str) -> Result { if let Some(v) = self.get_str(key) { let v = v.to_ascii_lowercase(); match v.as_ref() { "y" | "yes" | "t" | "true" | "1" => { @@ -348,18 +394,33 @@ Err(Error::KeyNotFound(key.to_string())) } /// Get a boolean value; return a default value if key wasn't found. + /// + /// # Errors pub fn get_bool_def(&self, key: &str, def: bool) -> Result { match self.get_bool(key) { Ok(v) => Ok(v), Err(Error::KeyNotFound(_)) => Ok(def), Err(e) => Err(e) } } + /* + /// Get a value and call a closure to map the value to another. + pub fn get_map(&self, key: &str, f: F) -> Result + where + F: FnOnce(&str) -> Result + { + if let Some(val) = self.get_str(key) { + f(val) + } else { + Err(Error::KeyNotFound(key.to_string())) + } + } + */ /// Parse the value of a key as a comma-separated list of strings and return /// it. Only non-empty entries are returned. /// /// # Examples @@ -369,10 +430,13 @@ /// let mut params = Params::new(); /// params.add_param("csv", "elena,chloe,drake"); /// let sv = params.get_strvec("csv").unwrap(); /// assert_eq!(sv, vec!["elena", "chloe", "drake"]); /// ``` + /// + /// # Errors + /// [`Error::KeyNotFound`] means the requested parameter does not exist. pub fn get_strvec(&self, key: &str) -> Result, Error> { let mut ret = Vec::new(); if let Some(v) = self.get_str(key) { let split = v.split(','); @@ -386,11 +450,11 @@ Ok(ret) } /// Parse the value of a key as a comma-separated list of uniqie strings and - /// return them in a HashSet. Only non-empty entries are returned. + /// return them in a `HashSet`. Only non-empty entries are returned. /// /// # Examples /// ``` /// use blather::Params; /// @@ -400,10 +464,13 @@ /// assert_eq!(set.len(), 2); /// assert_eq!(set.contains("elena"), true); /// assert_eq!(set.contains("chloe"), true); /// assert_eq!(set.contains("drake"), false); /// ``` + /// + /// # Errors + /// [`Error::KeyNotFound`] means the requested parameter does not exist. pub fn get_hashset(&self, key: &str) -> Result, Error> { let mut ret = HashSet::new(); if let Some(v) = self.get_str(key) { let split = v.split(','); @@ -419,10 +486,11 @@ /// Calculate the size of the buffer in serialized form. /// Each entry will be a newline terminated utf-8 line. /// Last line will be a single newline character. + #[must_use] pub fn calc_buf_size(&self) -> usize { let mut size = 0; for (key, value) in &self.hm { size += key.len() + 1; // including ' ' size += value.len() + 1; // including '\n' @@ -430,11 +498,12 @@ size + 1 // terminating '\n' } /// Serialize `Params` buffer into a vector of bytes for transmission. - pub fn serialize(&self) -> Result, Error> { + #[must_use] + pub fn serialize(&self) -> Vec { let mut buf = Vec::new(); for (key, value) in &self.hm { let k = key.as_bytes(); let v = value.as_bytes(); @@ -448,16 +517,16 @@ buf.push(b'\n'); } buf.push(b'\n'); - Ok(buf) + buf } /// Write the Params to a buffer. - pub fn encoder_write(&self, buf: &mut BytesMut) -> Result<(), Error> { + pub fn encoder_write(&self, buf: &mut BytesMut) { // Calculate the required buffer size let size = self.calc_buf_size(); // Reserve space buf.reserve(size); @@ -468,32 +537,31 @@ buf.put_u8(b' '); buf.put(value.as_bytes()); buf.put_u8(b'\n'); } buf.put_u8(b'\n'); - - Ok(()) } - /// Consume the Params buffer and return its internal HashMap. + /// Consume the `Params` buffer and return its internal `HashMap`. + #[must_use] pub fn into_inner(self) -> HashMap { self.hm } } impl From> for Params { fn from(hm: HashMap) -> Self { - Params { hm } + Self { hm } } } impl fmt::Display for Params { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut kvlist = Vec::new(); for (key, value) in &self.hm { - kvlist.push(format!("{}={}", key, value)); + kvlist.push(format!("{key}={value}")); } write!(f, "{{{}}}", kvlist.join(",")) } } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/types/telegram.rs ================================================================== --- src/types/telegram.rs +++ src/types/telegram.rs @@ -17,155 +17,127 @@ /// Representation of a Telegram; a buffer which contains a _topic_ and a set /// of key/value parameters. /// /// Internally the key/value parameters are represented by a [`Params`] /// structure. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct Telegram { - topic: Option, + topic: String, params: Params } impl Telegram { - /// Create a new telegram object, with an unset topic. - /// - /// Note that a telegram object without a topic is invalid. - /// [`set_topic()`](Self::set_topic) must be called to set a topic to make - /// the object valid. Use [`new_topic()`](Self::new_topic) to create a new - /// Telegram object with a topic. - pub fn new() -> Self { - Telegram { - ..Default::default() - } - } - - - /// Create a new telegram object with a topic. - /// - /// ``` - /// use blather::Telegram; - /// - /// let mut tg = Telegram::new_topic("Hello").unwrap(); - /// assert_eq!(tg.get_topic(), Some("Hello")); - /// ``` - pub fn new_topic(topic: &str) -> Result { - validate_topic(topic)?; - Ok(Telegram { - topic: Some(topic.to_string()), - ..Default::default() - }) - } - - - /// Clear topic and internal parameters buffer. - /// - /// ``` - /// use blather::Telegram; - /// - /// let mut tg = Telegram::new(); - /// tg.add_param("cat", "meow"); - /// assert_eq!(tg.num_params(), 1); - /// tg.clear(); - /// assert_eq!(tg.num_params(), 0); - /// ``` - pub fn clear(&mut self) { - self.topic = None; - self.params.clear(); - } - + /// Create a new telegram object. + /// + /// # Panics + /// The `topic` must be valid. + #[must_use] + #[allow(clippy::needless_pass_by_value)] + pub fn new(topic: impl ToString) -> Self { + let topic = topic.to_string(); + assert!(validate_topic(&topic).is_ok()); + Self { + topic, + params: Params::default() + } + } + + /// Internal factory function for creating a `Telegram` with an empty topic. + /// + /// This is intended to be used only by the codec. + pub(crate) fn new_uninit() -> Self { + Self { + topic: String::new(), + params: Params::default() + } + } /// Return the number of key/value parameters in the Telegram object. /// /// # Examples /// ``` /// use blather::Telegram; /// - /// let mut tg = Telegram::new(); + /// let mut tg = Telegram::new("SomeTopic"); /// assert_eq!(tg.num_params(), 0); /// tg.add_param("cat", "meow"); /// assert_eq!(tg.num_params(), 1); /// ``` /// /// # Notes /// This is a wrapper around [`Params::len()`](crate::Params::len). + #[must_use] pub fn num_params(&self) -> usize { self.params.len() } - /// Get a reference to the internal parameters object. - pub fn get_params(&self) -> &Params { + #[must_use] + pub const fn get_params(&self) -> &Params { &self.params } - /// Get a mutable reference to the inner [`Params`] object. /// /// ``` /// use blather::Telegram; /// - /// let mut tg = Telegram::new(); + /// let mut tg = Telegram::new("Topic"); /// tg.add_param("cat", "meow"); /// assert_eq!(tg.num_params(), 1); /// tg.get_params_mut().clear(); /// assert_eq!(tg.num_params(), 0); /// ``` pub fn get_params_mut(&mut self) -> &mut Params { &mut self.params } - - /// Get a reference the the parameter's internal HashMap. + /// Get a reference the the parameter's internal `HashMap`. /// /// Note: The inner representation of the Params object may change in the /// future. - pub fn get_params_inner(&self) -> &HashMap { + #[must_use] + pub const fn get_params_inner(&self) -> &HashMap { self.params.get_inner() } - /// Set topic for telegram. /// /// Overwrites current topic is one has already been set. /// /// # Examples /// ``` /// use blather::{Telegram, Error}; /// - /// let mut tg = Telegram::new(); - /// assert_eq!(tg.set_topic("Hello"), Ok(())); + /// let mut tg = Telegram::new("SomeTopic"); + /// assert!(matches!(tg.set_topic("Hello"), Ok(()))); /// /// let e = Error::BadFormat("Invalid topic character".to_string()); - /// assert_eq!(tg.set_topic("Hell o"), Err(e)); + /// assert!(matches!(tg.set_topic("Hell o"), Err(e))); /// ``` + /// + /// # Errors + /// [`Error::BadFormat`] means the input parameters are invalid. pub fn set_topic(&mut self, topic: &str) -> Result<(), Error> { validate_topic(topic)?; - self.topic = Some(topic.to_string()); + self.topic = topic.to_string(); Ok(()) } - /// Get a reference to the topic string, or None if topic is not been set. /// /// # Examples /// ``` /// use blather::{Telegram, Error}; /// - /// let tg = Telegram::new_topic("shoe0nhead").unwrap(); - /// assert_eq!(tg.get_topic(), Some("shoe0nhead")); - /// - /// let tg = Telegram::new(); - /// assert_eq!(tg.get_topic(), None); + /// let tg = Telegram::new("SomeTopic"); + /// assert_eq!(tg.get_topic(), "SomeTopic"); /// ``` - pub fn get_topic(&self) -> Option<&str> { - if let Some(t) = &self.topic { - Some(t) - } else { - None - } - } - + #[must_use] + pub fn get_topic(&self) -> &str { + self.topic.as_ref() + } /// Add a parameter to the telegram. /// /// The `key` and `value` parameters are generic over the trait `ToString`, /// allowing a polymorphic behavior. @@ -172,201 +144,227 @@ /// /// # Examples /// ``` /// use blather::Telegram; /// - /// let mut tg = Telegram::new(); + /// let mut tg = Telegram::new("Hello"); /// tg.add_param("integer", 42).unwrap(); /// tg.add_param("string", "hello").unwrap(); /// ``` /// /// # Notes /// - This is a thin wrapper around /// [`Params::add_param()`](crate::Params::add_param). - pub fn add_param( + /// + /// # Errors + /// [`Error::BadFormat`] means the input parameters are invalid. + pub fn add_param( &mut self, - key: T, - value: U + key: impl ToString, + value: impl ToString ) -> Result<(), Error> { self.params.add_param(key, value) } - /// Add a string parameter to the telegram. /// /// # Notes /// - This function exists primarily for parity with a C++ library; it is /// just a wrapper around [`add_param()`](Self::add_param), which is /// recommended over `add_str()`. + /// + /// # Errors + /// [`Error::BadFormat`] means the input parameters are invalid. pub fn add_str(&mut self, key: &str, value: &str) -> Result<(), Error> { self.add_param(key, value) } - /// Add parameter where the value is generated from an iterator over a /// string container, where entries will be comma-separated. /// /// ``` /// use blather::Telegram; /// - /// let mut tg = Telegram::new(); + /// let mut tg = Telegram::new("SomeTopic"); /// tg.add_strit("Cat", &["meow", "paws", "tail"]).unwrap(); - /// assert_eq!(tg.get_str("Cat"), Some("meow,paws,tail")); + /// assert!(matches!(tg.get_str("Cat"), Some("meow,paws,tail"))); /// ``` /// /// # Notes /// - This is a thin wrapper for /// [`Params::add_strit()`](crate::Params::add_strit). + /// + /// # Errors + /// [`Error::BadFormat`] means the input parameters are invalid. pub fn add_strit(&mut self, key: &str, c: I) -> Result<(), Error> where I: IntoIterator, S: AsRef { self.params.add_strit(key, c) } - /// Add a boolean value to Telegram object. /// /// # Notes /// - This is a thin wrapper around /// [`Params::add_bool()`](crate::Params::add_bool). + /// + /// # Errors + /// [`Error::BadFormat`] means the input parameters are invalid. pub fn add_bool( &mut self, key: K, value: bool ) -> Result<(), Error> { self.params.add_bool(key, value) } - /// Check whether a parameter exists in Telegram object. /// /// Returns `true` is the key exists, and `false` otherwise. + #[must_use] pub fn have_param(&self, key: &str) -> bool { self.params.have(key) } - /// Get a parameter. Fail if the parameter does not exist. /// /// # Notes /// - This is a thin wrapper around /// [`Params::get_param()`](crate::Params::get_param). + /// + /// # Errors + /// [`Error::KeyNotFound`] means the requested parameter does not exist. pub fn get_param(&self, key: &str) -> Result { self.params.get_param(key) } - /// Get a parameter. Return a default value if the parameter does not /// exist. /// /// # Notes /// - This is a thin wrapper around /// [`Params::get_param_def()`](crate::Params::get_param_def). + /// + /// # Errors + /// [`Error::KeyNotFound`] means the requested parameter does not exist. pub fn get_param_def( &self, key: &str, def: T ) -> Result { self.params.get_param_def(key, def) } - /// Get a string representation of a parameter. Return `None` is parameter /// does not exist. /// /// # Notes /// - This is a thin wrapper around /// [`Params::get_str()`](crate::Params::get_str) + /// + /// # Errors + /// [`Error::KeyNotFound`] means the requested parameter does not exist. + #[must_use] pub fn get_str(&self, key: &str) -> Option<&str> { self.params.get_str(key) } - /// Get a string representation of a parameter. Returns a default value is /// the parameter does not exist. /// /// # Notes /// - This is a thin wrapper around /// [`Params::get_str_def()`](crate::Params::get_str_def) + #[must_use] pub fn get_str_def<'a>(&'a self, key: &str, def: &'a str) -> &'a str { self.params.get_str_def(key, def) } - /// Get an integer representation of a parameter. /// /// ``` /// use blather::Telegram; /// - /// let mut tg = Telegram::new(); + /// let mut tg = Telegram::new("SomeTopic"); /// tg.add_param("Num", 7); /// assert_eq!(tg.get_int::("Num").unwrap(), 7); /// ``` /// /// # Notes /// - This function uses the `FromStr` trait on the return-type so it /// technically isn't limited to integers. /// - The method exists to mimic a C++ library. It is recommeded that /// applications use [`Telegram::get_param()`](Self::get_param) instead. + /// + /// # Errors + /// [`Error::KeyNotFound`] means the requested parameter does not exist. pub fn get_int(&self, key: &str) -> Result { self.params.get_int(key) } - /// Try to get the parameter value of a key and interpret it as an integer. /// If the key does not exist then return a default value supplied by the /// caller. /// /// ``` /// use blather::Telegram; /// - /// let mut tg = Telegram::new(); + /// let mut tg = Telegram::new("SomeTopic"); /// tg.add_param("num", 11); /// assert_eq!(tg.get_int_def::("num", 5).unwrap(), 11); /// assert_eq!(tg.get_int_def::("nonexistent", 17).unwrap(), 17); /// ``` + /// + /// # Errors + /// [`Error::BadFormat`] means the value's format is invalid. pub fn get_int_def( &self, key: &str, def: T ) -> Result { self.params.get_int_def(key, def) } - /// Return a boolean value. Return error if parameter does not exist. /// /// If a value exist but can not be parsed as a boolean value the error /// `Error::BadFormat` will be returned. /// /// # Notes /// - This is a thing wrapper around /// [`Params::get_bool()`](crate::Params::get_bool). + /// + /// # Errors + /// [`Error::KeyNotFound`] means the requested parameter does not exist. pub fn get_bool(&self, key: &str) -> Result { self.params.get_bool(key) } - /// Return a boolean value. Return a default value if parameter does not /// exist. /// /// # Notes /// - This is a thing wrapper around /// [`Params::get_bool()`](crate::Params::get_bool). + /// + /// # Errors + /// [`Error::BadFormat`] means the value's format is invalid. pub fn get_bool_def(&self, key: &str, def: bool) -> Result { self.params.get_bool_def(key, def) } - /// Parse the value of a key as a comma-separated list of strings and return /// it as a `Vec`. Only non-empty entries are returned. /// /// # Notes /// - This is a thin wrapper around /// [`Params::get_strvec()`](crate::Params::get_strvec). + /// + /// # Errors + /// [`Error::KeyNotFound`] means the requested parameter does not exist. pub fn get_strvec(&self, key: &str) -> Result, Error> { self.params.get_strvec(key) } /// Parse the value of a key as a comma-separated list of strings and return @@ -373,49 +371,53 @@ /// it as a `HashSet`. Only non-empty entries are returned. /// /// # Notes /// - This is a thin wrapper around /// [`Params::get_hashset()`](crate::Params::get_hashset). + /// + /// # Errors + /// [`Error::KeyNotFound`] means the requested parameter does not exist. + /// [`Error::BadFormat`] means the value's format is invalid. pub fn get_hashset(&self, key: &str) -> Result, Error> { self.params.get_hashset(key) } - /// Calculate the size of a serialized version of this Telegram object. /// If no topic has been set it is simply ignored. In the future this might /// change to something more dramatic, like a panic. Telegrams should always /// contain a topic when transmitted. /// /// Each line is terminated by a newline character. /// The last line consists of a single newline character. + #[must_use] pub fn calc_buf_size(&self) -> usize { // Calculate the required buffer size let mut size = 0; - if let Some(ref h) = self.topic { - size += h.len() + 1; // including '\n' - } + size += self.topic.len() + 1; // including '\n' // Note that the Params method reserves the final terminating newline. size + self.params.calc_buf_size() } - /// Serialize `Telegram` into a vector of bytes for transmission. + /// + /// # Errors + /// [`Error::BadFormat`] the `Telegram` is missing a topic. pub fn serialize(&self) -> Result, Error> { let mut buf = Vec::new(); - if let Some(ref h) = self.topic { - // Copy topic - let b = h.as_bytes(); - for a in b { - buf.push(*a); - } - buf.push(b'\n'); - } else { + if self.topic.is_empty() { return Err(Error::BadFormat("Missing heading".to_string())); } + // Copy topic + let b = self.topic.as_bytes(); + for a in b { + buf.push(*a); + } + buf.push(b'\n'); + for (key, value) in self.get_params_inner() { let k = key.as_bytes(); let v = value.as_bytes(); for a in k { buf.push(*a); @@ -430,14 +432,16 @@ buf.push(b'\n'); Ok(buf) } - - /// Write the Telegram to a BytesMut buffer. + /// Write the Telegram to a `BytesMut` buffer. + /// + /// # Errors + /// [`Error::SerializeError`] the `Telegram` is missing a topic. pub fn encoder_write(&self, buf: &mut BytesMut) -> Result<(), Error> { - if self.topic.is_none() { + if self.topic.is_empty() { return Err(Error::SerializeError("Missing Telegram topic".to_string())); } // Calculate the required buffer size let size = self.calc_buf_size(); @@ -444,13 +448,11 @@ // Reserve space buf.reserve(size); // Write data to output buffer - if let Some(ref b) = self.topic { - buf.put(b.as_bytes()); - } + buf.put(self.topic.as_bytes()); buf.put_u8(b'\n'); for (key, value) in self.get_params_inner() { buf.put(key.as_bytes()); buf.put_u8(b' '); @@ -460,61 +462,46 @@ buf.put_u8(b'\n'); Ok(()) } - /// Consume the Telegram buffer and return the internal parameters object. + #[must_use] pub fn into_params(self) -> Params { self.params } + + /// Unwrap the `Telegram` into a topic and `Params`. + #[must_use] + pub fn unwrap_topic_params(self) -> (String, Params) { + (self.topic, self.params) + } } impl From for Telegram { fn from(topic: String) -> Self { - Telegram { - topic: Some(topic), - ..Default::default() - } - } -} - -impl From for Telegram { - fn from(params: Params) -> Self { - Telegram { - params, - ..Default::default() + Self { + topic, + params: Params::default() } } } impl TryFrom<(&str, Params)> for Telegram { type Error = Error; + /// # Panics + /// The topic must be valid. fn try_from(t: (&str, Params)) -> Result { - let mut tg = Telegram::new_topic(t.0)?; + let mut tg = Self::new(t.0.to_string()); tg.params = t.1; Ok(tg) } } -impl From> for Telegram { - fn from(hm: HashMap) -> Self { - Telegram { - params: Params::from(hm), - ..Default::default() - } - } -} - impl fmt::Display for Telegram { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let topic: &str = match &self.topic { - Some(s) => s.as_ref(), - None => "" - }; - - write!(f, "{}:{}", topic, self.params) + write!(f, "{}:{}", self.topic, self.params) } } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/types/validators.rs ================================================================== --- src/types/validators.rs +++ src/types/validators.rs @@ -62,31 +62,29 @@ assert!(validate_topic("Foobar").is_ok()); } #[test] fn empty_topic() { - assert_eq!( - validate_topic(""), - Err(Error::BadFormat("Empty or broken topic".to_string())) - ); + let Err(Error::BadFormat(msg)) = validate_topic("") else { + panic!("Unexpectedly not Error::BadFormat"); + }; + assert_eq!(msg, "Empty or broken topic"); } #[test] fn broken_topic_1() { - assert_eq!( - validate_topic("foo bar"), - Err(Error::BadFormat("Invalid topic character".to_string())) - ); + let Err(Error::BadFormat(msg)) = validate_topic("foo bar") else { + panic!("Unexpectedly not Error::BadFormat"); + }; + assert_eq!(msg, "Invalid topic character"); } #[test] fn broken_topic_2() { - assert_eq!( - validate_topic(" foobar"), - Err(Error::BadFormat( - "Invalid leading topic character".to_string() - )) - ); + let Err(Error::BadFormat(msg)) = validate_topic(" foobar") else { + panic!("Unexpectedly not Error::BadFormat"); + }; + assert_eq!(msg, "Invalid leading topic character"); } } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: tests/conn_telegram.rs ================================================================== --- tests/conn_telegram.rs +++ tests/conn_telegram.rs @@ -15,11 +15,11 @@ let mut frm = Framed::new(mock.build(), Codec::new()); while let Some(o) = frm.next().await { let o = o.unwrap(); if let codec::Input::Telegram(tg) = o { - assert_eq!(tg.get_topic(), Some("hello")); + assert_eq!(tg.get_topic(), "hello"); let params = tg.into_params(); let map = params.into_inner(); assert_eq!(map.len(), 0); } else { panic!("Not a Telegram"); @@ -39,11 +39,11 @@ while let Some(o) = frm.next().await { let o = o.unwrap(); match o { codec::Input::Telegram(tg) => { - assert_eq!(tg.get_topic(), Some("hello")); + assert_eq!(tg.get_topic(), "hello"); let params = tg.into_params(); let map = params.into_inner(); assert_eq!(map.len(), 2); assert_eq!(map.get("murky_waters").unwrap(), "off"); assert_eq!(map.get("wrong_impression").unwrap(), "cows"); Index: tests/params.rs ================================================================== --- tests/params.rs +++ tests/params.rs @@ -60,11 +60,11 @@ #[test] fn display() { let mut params = Params::new(); params.add_str("foo", "bar").unwrap(); - let s = format!("{}", params); + let s = format!("{params}"); assert_eq!(s, "{foo=bar}"); } #[test] @@ -91,24 +91,24 @@ #[test] fn broken_key() { let mut param = Params::new(); - assert_eq!( - param.add_str("hell o", "world"), - Err(Error::BadFormat("Invalid key character".to_string())) - ); + let Err(Error::BadFormat(msg)) = param.add_str("hell o", "world") else { + panic!("Unexpectedly not Error::BadFormat"); + }; + assert_eq!(msg, "Invalid key character"); } #[test] fn empty_key() { let mut param = Params::new(); - assert_eq!( - param.add_str("", "world"), - Err(Error::BadFormat("Empty or broken key".to_string())) - ); + let Err(Error::BadFormat(msg)) = param.add_str("", "world") else { + panic!("Unexpectedly not Error::BadFormat"); + }; + assert_eq!(msg, "Empty or broken key"); } #[test] fn boolvals() { @@ -115,11 +115,14 @@ let mut params = Params::new(); params.add_bool("abool", true).unwrap(); params.add_bool("abooltoo", false).unwrap(); - assert_eq!(params.get_bool("abool"), Ok(true)); - assert_eq!(params.get_bool("abooltoo"), Ok(false)); + let Ok(true) = params.get_bool("abool") else { + panic!("Unexpectedly not Ok(true)"); + }; + let Ok(false) = params.get_bool("abooltoo") else { + panic!("Unexpectedly not Ok(false)"); + }; } - // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: tests/telegram.rs ================================================================== --- tests/telegram.rs +++ tests/telegram.rs @@ -1,25 +1,23 @@ use blather::{Error, Params, Telegram}; #[test] fn simple() { - let mut msg = Telegram::new(); - - msg.set_topic("SomeTopic").unwrap(); - assert_eq!(msg.get_topic().unwrap(), "SomeTopic"); - - msg.add_str("Foo", "bar").unwrap(); - assert_eq!(msg.get_str("Foo").unwrap(), "bar"); - - assert_eq!(msg.get_str("Moo"), None); + let mut tg = Telegram::new("SomeTopic"); + assert_eq!(tg.get_topic(), "SomeTopic"); + + tg.add_str("Foo", "bar").unwrap(); + assert_eq!(tg.get_str("Foo").unwrap(), "bar"); + + assert_eq!(tg.get_str("Moo"), None); } #[test] fn exist() { - let mut tg = Telegram::new(); + let mut tg = Telegram::new("SomeTopic"); tg.add_str("foo", "bar").unwrap(); assert!(tg.have_param("foo")); assert!(!tg.have_param("nonexistent")); @@ -26,56 +24,55 @@ } #[test] fn integer() { - let mut msg = Telegram::new(); + let mut tg = Telegram::new("SomeTopic"); - msg.set_topic("SomeTopic").unwrap(); - assert_eq!(msg.get_topic().unwrap(), "SomeTopic"); + assert_eq!(tg.get_topic(), "SomeTopic"); - msg.add_str("Num", "64").unwrap(); - assert_eq!(msg.get_int::("Num").unwrap(), 64); + tg.add_str("Num", "64").unwrap(); + assert_eq!(tg.get_int::("Num").unwrap(), 64); } #[test] fn size() { - let mut msg = Telegram::new(); + let mut tg = Telegram::new("SomeTopic"); - msg.add_param("Num", 7_usize).unwrap(); - assert_eq!(msg.get_int::("Num").unwrap(), 7); + tg.add_param("Num", 7_usize).unwrap(); + assert_eq!(tg.get_int::("Num").unwrap(), 7); } #[test] fn intoparams() { - let mut msg = Telegram::new(); + let mut tg = Telegram::new("SomeTopic"); - msg.add_str("Foo", "bar").unwrap(); - assert_eq!(msg.get_str("Foo").unwrap(), "bar"); - assert_eq!(msg.get_str("Moo"), None); + tg.add_str("Foo", "bar").unwrap(); + assert_eq!(tg.get_str("Foo").unwrap(), "bar"); + assert_eq!(tg.get_str("Moo"), None); - let params = msg.into_params(); + let params = tg.into_params(); let val = params.get_str("Foo"); assert_eq!(val.unwrap(), "bar"); } #[test] fn display() { - let mut tg = Telegram::new_topic("hello").unwrap(); + let mut tg = Telegram::new("hello"); tg.add_param("foo", "bar").unwrap(); - let s = format!("{}", tg); + let s = format!("{tg}"); assert_eq!(s, "hello:{foo=bar}"); } #[test] fn ser_size() { - let mut tg = Telegram::new_topic("hello").unwrap(); + let mut tg = Telegram::new("hello"); tg.add_str("foo", "bar").unwrap(); tg.add_str("moo", "cow").unwrap(); let sz = tg.calc_buf_size(); @@ -83,11 +80,11 @@ assert_eq!(sz, 6 + 8 + 8 + 1); } #[test] fn def_int() { - let mut tg = Telegram::new(); + let mut tg = Telegram::new("SomeTopic"); tg.add_str("Num", "11").unwrap(); assert_eq!(tg.get_int_def::("Num", 17).unwrap(), 11); let num = tg.get_int_def::("nonexistent", 42).unwrap(); @@ -96,27 +93,25 @@ } #[test] fn bad_topic_leading() { - let mut tg = Telegram::new(); - assert_eq!( - tg.set_topic(" SomeTopic"), - Err(Error::BadFormat( - "Invalid leading topic character".to_string() - )) - ); + let mut tg = Telegram::new("Hello"); + let Err(Error::BadFormat(msg)) = tg.set_topic(" SomeTopic") else { + panic!("Unexpectedly not Error::BadFormat"); + }; + assert_eq!(msg, "Invalid leading topic character"); } #[test] fn bad_topic() { - let mut tg = Telegram::new(); - assert_eq!( - tg.set_topic("Some Topic"), - Err(Error::BadFormat("Invalid topic character".to_string())) - ); + let mut tg = Telegram::new("Hello"); + let Err(Error::BadFormat(msg)) = tg.set_topic("Some Topic") else { + panic!("Unexpectedly not Error::BadFormat"); + }; + assert_eq!(msg, "Invalid topic character"); } #[test] fn create_from_tuple() { Index: tests/tg_to_expect.rs ================================================================== --- tests/tg_to_expect.rs +++ tests/tg_to_expect.rs @@ -18,11 +18,11 @@ panic!("No frame"); }; let o = o.unwrap(); if let codec::Input::Telegram(tg) = o { - assert_eq!(tg.get_topic(), Some("hello")); + assert_eq!(tg.get_topic(), "hello"); assert_eq!(tg.get_int::("len").unwrap(), 4); frm.codec_mut().expect_bytes(4).unwrap(); } else { panic!("Not a Telegram"); } Index: www/changelog.md ================================================================== --- www/changelog.md +++ www/changelog.md @@ -2,18 +2,35 @@ ⚠️ indicates a breaking change. ## [Unreleased] -[Details](/vdiff?from=blather-0.10.1&to=trunk) +[Details](/vdiff?from=blather-0.11.0&to=trunk) ### Added ### Changed ### Removed +--- + +## [0.11.0] - 2024-09-22 + +[Details](/vdiff?from=blather-0.10.1&to=blather-0.11.0) + +### Changed + +- ⚠️ `Error::IO` carries with it `std::io::Error` rather than a string + representation of the error. + +### Removed + +- ⚠️ `Error` no longer implements `PartialEq`. +- ⚠️ `Telegram::clear()` was removed because there's no "default" topic. +- Remove `From` for `Telegram` (since it needs a topic now). + --- ## [0.10.1] - 2024-08-06 [Details](/vdiff?from=blather-0.10.0&to=blather-0.10.1)