Index: .efiles ================================================================== --- .efiles +++ .efiles @@ -2,13 +2,10 @@ README.md www/index.md www/changelog.md src/err.rs src/lib.rs -src/types.rs -src/types/telegram.rs -src/types/params.rs -src/types/kvlines.rs -src/types/validators.rs +src/params.rs +src/telegram.rs +src/kvlines.rs +src/validators.rs src/codec.rs -src/codec/utils.rs -tests/*.rs Index: Cargo.toml ================================================================== --- Cargo.toml +++ Cargo.toml @@ -1,9 +1,10 @@ [package] name = "blather" -version = "0.11.0" -edition = "2021" +version = "0.12.0" +edition = "2024" +rust-version = "1.85" license = "0BSD" # https://crates.io/category_slugs categories = [ "network-programming" ] keywords = [ "line-based", "protocol", "tokio", "codec" ] repository = "https://repos.qrnch.tech/pub/blather" @@ -19,26 +20,34 @@ # https://doc.rust-lang.org/cargo/reference/manifest.html#the-badges-section [badges] maintenance = { status = "actively-developed" } +[features] +bin = ["dep:base85"] +bincode = ["dep:bincode", "bin"] + [dependencies] -bytes = { version = "1.7.2" } -futures = { version = "0.3.30" } -tokio = { version = "1.40.0" } -tokio-stream = { version = "0.1.16" } -tokio-util = { version= "0.7.12", features = ["codec"] } +base85 = { version = "2.0.0", optional = true } +bincode = { version = "2.0.1", optional = true } +bytes = { version = "1.10.1" } +futures = { version = "0.3.31" } +tokio = { version = "1.45.0" } +tokio-util = { version= "0.7.15", features = ["codec"] } [dev-dependencies] -tokio = { version = "1.40.0", features = ["macros", "net"] } +orphanage = { version = "0.2.4" } +tokio = { version = "1.45.0", features = ["io-util", "macros", "net"] } +tokio-stream = { version = "0.1.17" } tokio-test = { version = "0.4.4" } [package.metadata.docs.rs] -rustdoc-args = ["--generate-link-to-definition"] +all-features = true +rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] [lints.clippy] -all = { level = "deny", priority = -1 } +all = { level = "warn", 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 : Index: rustfmt.toml ================================================================== --- rustfmt.toml +++ rustfmt.toml @@ -1,8 +1,8 @@ blank_lines_upper_bound = 2 comment_width = 79 -edition = "2018" +edition = "2024" #force_multiline_blocks = true format_strings = true max_width = 79 match_block_trailing_comma = false tab_spaces = 2 Index: src/codec.rs ================================================================== --- src/codec.rs +++ src/codec.rs @@ -1,20 +1,19 @@ //! A [`tokio_util::codec`] Codec that is used to encode and decode the //! blather protocol. -pub mod utils; - use std::{ fmt, {cmp, collections::HashMap, mem} }; use bytes::{BufMut, Bytes, BytesMut}; -use tokio::io; - -use tokio_util::codec::{Decoder, Encoder}; +use { + tokio::{io, sync::mpsc::UnboundedSender}, + tokio_util::codec::{Decoder, Encoder} +}; use crate::{ err::Error, {KVLines, Params, Telegram} }; @@ -21,11 +20,11 @@ /// Current state of decoder. /// /// Controls what, if anything, will be returned to the application. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] enum CodecState { /// Read and decode a [`Telegram`] buffer from the network. Telegram, /// Read and decode a [`Params`] buffer from the network. @@ -39,10 +38,13 @@ Chunks, /// Read a specified amount of raw bytes, and return the entire immutable /// buffer when it has arrived. Bytes, + + /// Transmit received bytes buffers to a channel. + BytesCh, /// Ignore a specified amount of raw bytes. Skip } @@ -63,10 +65,14 @@ /// the `u64` parameter is 0 it means this is the final chunk. Chunk(Bytes, u64), /// A complete raw immutable buffer has been received. Bytes(Bytes), + + /// Sentinel value used to signal that transfer of buffers to a channel is + /// done. + BytesChDone, /// The requested number of bytes have been ignored. SkipDone } @@ -78,11 +84,12 @@ max_line_length: usize, tg: Telegram, params: Params, kvlines: KVLines, state: CodecState, - remain: u64 + remain: u64, + bytes_tx: Option> } #[allow(clippy::missing_fields_in_debug)] impl fmt::Debug for Codec { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -93,11 +100,10 @@ impl Default for Codec { fn default() -> Self { Self::new() } } - /// A Codec used to encode and decode the blather protocol. /// /// # Notes /// Normally the Codec object is hidden inside a @@ -130,11 +136,12 @@ max_line_length: usize::MAX, tg: Telegram::new_uninit(), params: Params::new(), kvlines: KVLines::new(), state: CodecState::Telegram, - remain: 0 + remain: 0, + bytes_tx: None } } /// Create a new `Codec` with a specific maximum line length. The default /// state will be to expect a [`Telegram`]. @@ -150,11 +157,10 @@ #[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. 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] @@ -161,11 +167,10 @@ .iter() .position(|b| *b == b'\n'); (read_to, newline_offset) } - /// This is called when `decode_telegram_lines` has encountered an eol, /// determined that the string is longer than zero characters, and thus /// passed the line to this function to process it. /// @@ -282,11 +287,10 @@ return Ok(None); } } } - /// Read buffer line-by-line, split each line at the first space character /// and store the left part as a key and the right part as a value in a /// Params structure. fn decode_params_lines( &mut self, @@ -351,19 +355,19 @@ } 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); + + self.kvlines.append(k, v)?; } } else { // Need more data return Ok(None); } } } - /// Set the decoder to treat the next `size` bytes as raw bytes to be /// received in chunks as `Bytes`. /// /// # Decoder behavior @@ -379,20 +383,19 @@ /// /// # 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())); + return Err(Error::InvalidSize("zero size".to_string())); } //println!("Expecting bin {}", size); self.state = CodecState::Chunks; self.remain = size; Ok(()) } - /// Expect a immutable buffer of a certain size to be received. /// /// The returned buffer will be stored in process memory. /// @@ -407,11 +410,11 @@ /// # 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())); + return Err(Error::InvalidSize("zero size".to_string())); } self.state = CodecState::Bytes; // unwrap() should be safe, unless running on a platform where // size_of::() > size_of::() and the buffer is larger than @@ -418,10 +421,28 @@ // usize::MAX. self.remain = size.try_into().unwrap(); Ok(()) } + /// Expect specified amount of data which will be received as `Bytes` buffers + /// and sent over a channel using a [`UnboundedSender`] end-point. + /// + /// # Errors + /// [`Error::InvalidSize`] means the `size` parameter was set to `0`. + pub fn expect_bytes_channel( + &mut self, + size: u64, + tx: UnboundedSender + ) -> Result<(), Error> { + if size == 0 { + return Err(Error::InvalidSize("must not be zero".to_string())); + } + self.state = CodecState::BytesCh; + self.bytes_tx = Some(tx); + self.remain = size; + Ok(()) + } /// Tell the Decoder to expect lines of key/value pairs. /// /// # Decoder behavior /// On successful completion the the decoder will next return an @@ -428,11 +449,11 @@ /// [`Input::Params(params)`](Input::Params) once a complete `Params` buffer /// has been received. /// /// Once the entire buffer has been received by the `Decoder` it will revert /// to expect an [`Input::Telegram`]. - pub fn expect_params(&mut self) { + pub const fn expect_params(&mut self) { self.state = CodecState::Params; } /// Tell the Decoder to expect lines ordered key/value pairs. /// @@ -441,11 +462,11 @@ /// [`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`]. - pub fn expect_kvlines(&mut self) { + pub const fn expect_kvlines(&mut self) { self.state = CodecState::KVLines; } /// Skip a requested number of bytes. /// @@ -456,11 +477,11 @@ /// /// # 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())); + return Err(Error::InvalidSize("zero size".to_string())); } self.state = CodecState::Skip; self.remain = size; Ok(()) } @@ -568,10 +589,51 @@ // Revert to expecting Telegram lines self.state = CodecState::Telegram; Ok(Some(Input::Bytes(buf.split_to(remain).freeze()))) } + } + CodecState::BytesCh => { + // Calculate how much data to take off read buffer, capping at + // `remain`. + let read_to = cmp::min(self.remain, buf.len() as u64); + self.remain -= read_to; + + // 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. + // + // .unwrap() is safe here, because read_to is guaranteed to + // be within the bounds of an usize due to the `cmp::min()` above. + let len = usize::try_from(read_to).unwrap(); + + let buf = buf.split_to(len).freeze(); + if let Some(ref tx) = self.bytes_tx { + // Ignore errors -- we'll assume this means the application dropped + // the reader end-point to signal it is no longer interested in + // receiving the data. + let _ = tx.send(buf); + } + + // If we have all the expected data, then report that we're done with a + // senitnel value. Otherwise report back to caller that more data is + // needed. + if self.remain == 0 { + // When no more data is expected for this binary part, revert to + // expecting a Telegram + self.state = CodecState::Telegram; + + // Send an empty buffer to signal eof, then drop the end-point + if let Some(tx) = self.bytes_tx.take() { + let _ = tx.send(Bytes::new()); + } + + // Return sentinel value just to signal that we're done + Ok(Some(Input::BytesChDone)) + } else { + Ok(None) + } } CodecState::Skip => { if buf.is_empty() { return Ok(None); // Need more data } @@ -702,6 +764,243 @@ buf.put(data); Ok(()) } } + +#[cfg(test)] +mod tests { + use { + futures::sink::SinkExt, tokio::sync::mpsc::unbounded_channel, + tokio_stream::StreamExt, tokio_test::io::Builder, + tokio_util::codec::Framed + }; + + use bytes::BytesMut; + + use super::{Bytes, Codec, Input, Telegram}; + + #[tokio::test] + async fn valid_no_params() { + let mut mock = Builder::new(); + + mock.read(b"hello\n\n"); + + let mut frm = Framed::new(mock.build(), Codec::new()); + + while let Some(o) = frm.next().await { + let o = o.unwrap(); + if let Input::Telegram(tg) = o { + 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"); + } + } + } + + #[tokio::test] + async fn valid_with_params() { + let mut mock = Builder::new(); + + mock.read(b"hello\nmurky_waters off\nwrong_impression cows\n\n"); + + let mut frm = Framed::new(mock.build(), Codec::new()); + + while let Some(o) = frm.next().await { + let o = o.unwrap(); + + match o { + Input::Telegram(tg) => { + 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"); + } + _ => { + panic!("Not a Telegram"); + } + } + } + } + + #[tokio::test] + #[should_panic( + expected = "Protocol(\"Bad format; Invalid topic character\")" + )] + async fn bad_topic() { + let mut mock = Builder::new(); + + // space isn't allowed in topic + mock.read(b"hel lo\n\n"); + + let mut frm = Framed::new(mock.build(), Codec::new()); + let e = frm.next().await.unwrap(); + e.unwrap(); + } + + #[tokio::test] + async fn multiple() { + let mut mock = Builder::new(); + + mock.read(b"hello\nfoo bar\n\nworld\nholy cows\n\nfinal\nthe thing\n\n"); + + let mut frm = Framed::new(mock.build(), Codec::new()); + + let o = frm.next().await.unwrap().unwrap(); + let Input::Telegram(tg) = o else { + panic!("Unexpectely not Input::Telegram"); + }; + assert_eq!(tg.get_topic(), "hello"); + assert_eq!(tg.get_str("foo"), Some("bar")); + + let o = frm.next().await.unwrap().unwrap(); + let Input::Telegram(tg) = o else { + panic!("Unexpectely not Input::Telegram"); + }; + assert_eq!(tg.get_topic(), "world"); + assert_eq!(tg.get_str("holy"), Some("cows")); + + let o = frm.next().await.unwrap().unwrap(); + let Input::Telegram(tg) = o else { + panic!("Unexpectely not Input::Telegram"); + }; + assert_eq!(tg.get_topic(), "final"); + assert_eq!(tg.get_str("the"), Some("thing")); + } + + #[tokio::test] + async fn tg_followed_by_buf() { + let mut mock = Builder::new(); + + mock.read(b"hello\nlen 4\n\n1234"); + + let mut frm = Framed::new(mock.build(), Codec::new()); + + let Some(o) = frm.next().await else { + panic!("No frame"); + }; + let o = o.unwrap(); + + if let Input::Telegram(tg) = o { + assert_eq!(tg.get_topic(), "hello"); + assert_eq!(tg.get_fromstr::("len").unwrap().unwrap(), 4); + frm.codec_mut().expect_bytes(4).unwrap(); + } else { + panic!("Not a Telegram"); + } + + while let Some(o) = frm.next().await { + let o = o.unwrap(); + if let Input::Bytes(_bm) = o { + } else { + panic!("Not a Buf"); + } + } + } + + #[tokio::test] + async fn tg_buf_tg() { + let mut mock = Builder::new(); + + mock.read(b"hello\nlen 4\n\n1234world\nfoo bar\n\n"); + + let mut frm = Framed::new(mock.build(), Codec::new()); + + // Expect Telegram, which sets up for getting Bytes + let o = frm.next().await.unwrap().unwrap(); + let Input::Telegram(tg) = o else { + panic!("Unexpectedly not Input::Telegram(_)"); + }; + assert_eq!(tg.get_topic(), "hello"); + let len = tg.get_fromstr::("len").unwrap().unwrap(); + assert_eq!(len, 4); + frm.codec_mut().expect_bytes(len).unwrap(); + + // Expect Bytes + let o = frm.next().await.unwrap().unwrap(); + let Input::Bytes(buf) = o else { + panic!("Unexpectedly not Input::Bytes(_)"); + }; + assert_eq!(buf, "1234"); + + // Expect Telegram + let o = frm.next().await.unwrap().unwrap(); + let Input::Telegram(tg) = o else { + panic!("Unexpectedly not Input::Telegram(_)"); + }; + assert_eq!(tg.get_topic(), "world"); + assert_eq!(tg.get_str("foo"), Some("bar")); + } + + + #[tokio::test] + async fn expect_bytes_ch() { + let (client, server) = tokio::io::duplex(64); + + let mut frmin = Framed::new(server, Codec::new()); + let mut frmout = Framed::new(client, Codec::new()); + + // Spawn server task + let jh = tokio::task::spawn(async move { + let o = frmin.next().await.unwrap().unwrap(); + let Input::Telegram(tg) = o else { + panic!("Unexpectedly not Input::Telegram(_)"); + }; + assert_eq!(tg.as_ref(), "ReqToSend"); + let len = tg.get_fromstr::("Len").unwrap().unwrap(); + + // Create a channel for receiving stream of Bytes + let (tx, mut rx) = unbounded_channel(); + + // Expect requested number of bytes + frmin.codec_mut().expect_bytes_channel(len, tx).unwrap(); + + // Spawn task for receiving expected data through channel + let jh = tokio::task::spawn(async move { + let mut inbuf = BytesMut::new(); + inbuf.reserve(16); + + // Receive the expected amount of bytes over channel + loop { + let buf = rx.recv().await.unwrap(); + if buf.is_empty() { + break; + } + // ToDo: Verify buffer contents + inbuf.extend_from_slice(&buf); + } + + let buf = inbuf.freeze(); + assert_eq!(buf, "0123456789abcdef"); + }); + + // Wait for codec to report that it is done feeding data to the channel + let o = frmin.next().await.unwrap().unwrap(); + let Input::BytesChDone = o else { + panic!("Unexpectedly not Input::BytesChDone"); + }; + + jh.await.unwrap(); + }); + + let len = 16; + + let mut tg = Telegram::new("ReqToSend"); + tg.add_param("Len", len).unwrap(); + frmout.send(&tg).await.unwrap(); + + // Send `len` amount of binary data + frmout.send(Bytes::from("0123")).await.unwrap(); + frmout.send(Bytes::from("4567")).await.unwrap(); + frmout.send(Bytes::from("89ab")).await.unwrap(); + frmout.send(Bytes::from("cdef")).await.unwrap(); + + jh.await.unwrap(); + } +} + // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : DELETED src/codec/utils.rs Index: src/codec/utils.rs ================================================================== --- src/codec/utils.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Helpers used to perform common framed stream operation. - -use tokio::io::AsyncRead; - -use tokio_util::codec::Framed; - -use tokio_stream::StreamExt; - -use crate::{ - codec::{self, Codec}, - err::Error, - Telegram -}; - - -/// Validation callback result. -pub enum Outcome { - /// The Telegram should be returned to the application. - Accept, - - /// The Telefgram should be ignored. - Ignore, - - /// Tell the caller that a fatal error occurred. - Fail(Error) -} - -/// Receive a [`Telegram`] from a `Frame`'d stream. -/// -/// 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)`. -#[allow(clippy::missing_errors_doc)] -pub async fn expect_telegram( - frmio: &mut Framed, - validate: F -) -> Result, Error> -where - 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); - }; - let frm = match frm { - Ok(frm) => frm, - Err(e) => { - break Err(Error::Protocol(e.to_string())); - } - }; - let codec::Input::Telegram(tg) = frm else { - // If a non-Telegram is received, then abort and close connection - break Err(Error::Protocol( - "Did not receive an expected Telegram".into() - )); - }; - match validate(&tg) { - Outcome::Accept => break Ok(Some(tg)), - Outcome::Ignore => continue, - Outcome::Fail(e) => break Err(e) - } - } -} - -// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/err.rs ================================================================== --- src/err.rs +++ src/err.rs @@ -2,49 +2,76 @@ use std::fmt; use tokio::io; + +/// Specifies whether a parameter key or value is invalid. +#[derive(Debug)] +pub enum ParamError { + /// The key is invalid. + Key(String), + + /// The value is invalid. + Value(String) +} + /// Error that `blather` can emit. #[derive(Debug)] pub enum Error { /// The input format of a buffer was incorrect. BadFormat(String), - /// Something occurred which was unexpected in the current state. - BadState(String), - /// The specified size is invalid, or invalid in a specific context. InvalidSize(String), /// A `std::io` or `tokio::io` error has occurred. IO(std::io::Error), - /// The requiested key was not found. - KeyNotFound(String), + /// Invalid `Params` field. + Param(ParamError), /// Unable to serialize a buffer. SerializeError(String), /// A "protcol error" implies that the Framed decoder detected an error /// while parsing incoming data. + /// + /// This can either mean that an unexpected format was detected while + /// parsing the wire protocol, or that the `Codec` was found to be in an + /// unepxcted state. Protocol(String) } + +impl Error { + /// Factory for `Self::BadFormat`. + pub fn bad_format(s: impl Into) -> Self { + Self::BadFormat(s.into()) + } +} impl std::error::Error for Error {} impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { 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::Param(pe) => { + if f.alternate() { + match pe { + ParamError::Key(s) => write!(f, "Invalid key; {s}"), + ParamError::Value(s) => write!(f, "Invalid value; {s}") + } + } else { + match pe { + ParamError::Key(s) | ParamError::Value(s) => write!(f, "{s}") + } + } + } + Self::Protocol(s) => write!(f, "Protocol; {s}"), Self::SerializeError(s) => write!(f, "Unable to serialize; {s}") } } } ADDED src/kvlines.rs Index: src/kvlines.rs ================================================================== --- /dev/null +++ src/kvlines.rs @@ -0,0 +1,159 @@ +//! A key/value pair list with stable ordering and non-unique keys. + +use std::fmt; + +use bytes::{BufMut, BytesMut}; + +use crate::{ + validators::{validate_param_key, validate_param_value}, + Error +}; + +/// Representation of a key/value pair in `KVLines`. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct KeyValue { + key: String, + value: String +} + +/// Ordered list of key/value pairs, with no uniqueness constraint for the +/// keys. +#[derive(Debug, Clone, Default)] +pub struct KVLines { + lines: Vec +} + +impl KVLines { + /// Create a new empty parameters object. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Reset all the lines. + pub fn clear(&mut self) { + self.lines.clear(); + } + + /// Get a reference to the inner vector of [`KeyValue`]'s. + #[must_use] + pub const fn get_inner(&self) -> &Vec { + &self.lines + } + + /// Append a key/value entry to the end of the list. + /// + /// # Errors + /// If either key or value contain invalid characters [`Error::Param`] is + /// returned, with `ParamError::Key` or `ParamError::Value` indicating + /// whether it's the key or the value that is invalid. + #[allow(clippy::needless_pass_by_value)] + pub fn append( + &mut self, + key: impl ToString, + value: impl ToString + ) -> Result<(), Error> { + let key = key.to_string(); + let value = value.to_string(); + + validate_param_key(&key)?; + validate_param_value(&value)?; + + self.lines.push(KeyValue { key, value }); + + Ok(()) + } + + /// 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' + } + size + 1 // terminating '\n' + } + + + /// Serialize object into a `Vec` buffer suitable for transmission. + #[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(); + for a in k { + buf.push(*a); + } + buf.push(b' '); + for a in v { + buf.push(*a); + } + buf.push(b'\n'); + } + + buf.push(b'\n'); + + buf + } + + /// Write the Params to a buffer. + pub fn encoder_write(&self, buf: &mut BytesMut) { + // Calculate the required buffer size + let size = self.calc_buf_size(); + + // Reserve space + buf.reserve(size); + + // Write data to output buffer + for n in &self.lines { + buf.put(n.key.as_bytes()); + buf.put_u8(b' '); + buf.put(n.value.as_bytes()); + buf.put_u8(b'\n'); + } + buf.put_u8(b'\n'); + } + + /// 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 { + Self { lines } + } +} + + +impl TryFrom> for KVLines { + type Error = Error; + + fn try_from(lines: Vec<(String, String)>) -> Result { + let mut out = Self { lines: Vec::new() }; + for (key, value) in lines { + out.append(key, value)?; + } + Ok(out) + } +} + +impl fmt::Display for KVLines { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut kvlist = Vec::new(); + for n in &self.lines { + kvlist.push(format!("{}={}", n.key, n.value)); + } + write!(f, "{{{}}}", kvlist.join(",")) + } +} + +// 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 @@ -1,7 +1,7 @@ //! A protocol and a communication library for a mostly line-based key/value -//! pair protocol. +//! protocol. //! //! # Communication buffers //! _blather_ defines a few buffers which it uses to send and receive //! information over its communication module. //! @@ -19,11 +19,12 @@ //! tg.add_param("Job", "Secret Agent"); //! tg.add_param("Age", "42"); //! //! assert_eq!(tg.get_topic(), "AddUser"); //! assert_eq!(tg.get_str("Name").unwrap(), "Frank Foobar"); -//! assert_eq!(tg.get_param::("Age").unwrap(), 42); +//! assert_eq!(tg.get_fromstr::("Nonexistent").unwrap(), None); +//! assert_eq!(tg.get_fromstr::("Age").unwrap(), Some(42)); //! ``` //! //! ## Params //! These are simple key/value pairs, which can be seen as `HashMap`'s with //! some restrictions on key names. @@ -36,11 +37,11 @@ //! params.add_param("Name", "Frank Foobar"); //! params.add_param("Job", "Secret Agent"); //! params.add_param("Age", "42"); //! //! assert_eq!(params.get_str("Name").unwrap(), "Frank Foobar"); -//! assert_eq!(params.get_param::("Age").unwrap(), 42); +//! assert_eq!(params.get_fromstr::("Age").unwrap(), Some(42)); //! ``` //! //! A set of "parameters", represented by the Params struct, is a set of //! key/value pairs. They look similar to `Telegrams` because the `Telegram`'s //! implement their key/value paris using a `Params` buffer. @@ -51,15 +52,21 @@ //! implementing its own [`Codec`]. It can be used to send and //! receive the various communication buffers supported by the crate. #![deny(missing_docs)] #![deny(rustdoc::missing_crate_level_docs)] +#![cfg_attr(docsrs, feature(doc_cfg))] pub mod codec; mod err; -pub mod types; +mod kvlines; +mod params; +mod telegram; +mod validators; pub use codec::Codec; -pub use err::Error; -pub use types::{KVLines, KeyValue, Params, Telegram}; +pub use err::{Error, ParamError}; +pub use kvlines::{KVLines, KeyValue}; +pub use params::Params; +pub use telegram::Telegram; // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : ADDED src/params.rs Index: src/params.rs ================================================================== --- /dev/null +++ src/params.rs @@ -0,0 +1,1003 @@ +//! The `Params` buffer is a set of unorderded key/value pairs, with unique +//! keys. +//! +//! It's similar to a `HashMap`, but has constraints on key names values, due +//! to having a defined serialization format. + +use std::{collections::HashMap, fmt, str::FromStr}; + +use bytes::{BufMut, BytesMut}; + +use super::validators::{validate_param_key, validate_param_value}; + +use crate::err::{Error, ParamError}; + +/// Key/value parameters storage with helper methods to make adding and getting +/// common value types slightly more ergonomic and using a plain `HashMap`. +/// +/// Uses `String`s for both keys and values internally. +// ToDo: Rename `Params` to `Fields`, and call a key/value pair a `field`. +#[repr(transparent)] +#[derive(Debug, Clone, Default)] +pub struct Params(HashMap); + +impl Params { + /// Create a new empty parameters object. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Reset all the key/values in `Params` object. + pub fn clear(&mut self) { + self.0.clear(); + } + + /// Return the number of key/value pairs in the parameter buffer. + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if the `Params` collection does not contain any key/value + /// pairs. + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Return reference to inner `HashMap`. + #[must_use] + pub const fn inner(&self) -> &HashMap { + &self.0 + } + + /// Add a parameter. + /// + /// ``` + /// use blather::{Params, Error, ParamError}; + /// + /// let mut params = Params::new(); + /// + /// // Add a parameter + /// params.add("hello".into(), "world".into()).unwrap(); + /// + /// // Attempt to add a parameter with an invalid key + /// let res = params.add("hell o".into(), "world".into()); + /// let Err(Error::Param(ParamError::Key(msg))) = res else { + /// panic!("Unexpectedly not ParamError::Key(_)"); + /// }; + /// assert_eq!(msg, "invalid character"); + /// ``` + /// + /// # Errors + /// [`Error::Param`] means the input parameters are invalid. It will + /// contains a [`ParamError`] indicating whether the key or the value is + /// invalid. + /// + /// # See also + /// For more ergonomics with automatic conversion to strings, see + /// [`Params::add_param`]. + pub fn add(&mut self, key: String, value: String) -> Result<(), Error> { + validate_param_key(&key)?; + validate_param_value(&value)?; + + self.0.insert(key, value); + + Ok(()) + } + + /// Add a parameter to the parameter. + /// + /// Both the `key` and `value` are anything convertable using the `ToString` + /// trait. + /// + /// # Examples + /// ``` + /// use blather::Params; + /// + /// let mut params = Params::new(); + /// params.add_param("integer", 42).unwrap(); + /// params.add_param("string", "hello").unwrap(); + /// ``` + /// + /// # Errors + /// [`Error::Param`] means the input parameters are invalid. It will + /// contains a [`ParamError`] indicating whether the key or the value is + /// invalid. + /// + /// # See also + /// If the value is a string, prefer to use [`Params::add_str()`]. + #[inline] + #[allow(clippy::needless_pass_by_value)] + pub fn add_param( + &mut self, + key: impl ToString, + value: impl ToString + ) -> Result<(), Error> { + let key = key.to_string(); + let value = value.to_string(); + + validate_param_key(&key)?; + validate_param_value(&value)?; + + self.0.insert(key, value); + + Ok(()) + } + + /// Add a string parameter to the parameter. + /// + /// This function can, under some circumstances, be a little more effecient + /// than [`Params::add_param()`], but offers less flexibility. + /// + /// # Errors + /// If either the key or the value are invalid, [`Error::BadFormat`] means + /// the input parameters are invalid. + #[inline] + #[allow(clippy::needless_pass_by_value)] + pub fn add_str( + &mut self, + key: impl ToString, + value: impl Into + ) -> Result<(), Error> { + let key = key.to_string(); + let value = value.into(); + + validate_param_key(&key)?; + validate_param_value(&value)?; + + self.0.insert(key, value); + + Ok(()) + } + + /// Add a boolean parameter. + /// + /// # Examples + /// ``` + /// 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!(matches!(params.get_bool("should_be_true"), Ok(Some(true)))); + /// assert!(matches!(params.get_bool("should_be_false"), Ok(Some(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. + /// + /// # Errors + /// [`Error::Param`] with [`ParamError::Key`] indicates that the key format + /// is invalid. + #[inline] + #[allow(clippy::needless_pass_by_value)] + pub fn add_bool( + &mut self, + key: impl ToString, + value: bool + ) -> Result<(), Error> { + let key = key.to_string(); + let v = if value { "true" } else { "false" }; + + validate_param_key(&key)?; + + self.add_str(key, v) + } + + /// Returns `true` if the parameter with `key` exists. Returns `false` + /// otherwise. + /// + /// ``` + /// use blather::Params; + /// + /// let mut params = Params::new(); + /// + /// assert!(!params.contains("nonexistent")); + /// + /// params.add_str("Exists", "Very much").unwrap(); + /// assert!(params.contains("Exists")); + /// ``` + #[must_use] + pub fn contains(&self, key: &str) -> bool { + self.0.contains_key(key) + } + + /// Call `FromStr` implementation to extract a value given a key. + /// + /// # Examples + /// ``` + /// use blather::{Params, Error, ParamError}; + /// + /// let mut params = Params::new(); + /// + /// params.add_param("arthur", 42); + /// let fourtytwo = params.get_fromstr::("arthur").unwrap(); + /// assert_eq!(fourtytwo, Some(42)); + /// + /// let nonexist = params.get_fromstr::("ford"); + /// let Ok(None) = nonexist else { + /// panic!("Unexpectedly not `Ok(None)`"); + /// }; + /// + /// params.add_param("notint", "hello"); + /// let res = params.get_fromstr::("notint"); + /// let Err(Error::Param(ParamError::Value(msg))) = res else { + /// panic!("Unexpectedly not Error::Param(ParamError::Value(_))"); + /// }; + /// assert_eq!(msg, "invalid digit found in string"); + /// ``` + /// + /// # Errors + /// Returns [`Error::Param`] containing [`ParamError::Value`] if the value + /// can not be parsed. + pub fn get_fromstr(&self, key: &str) -> Result, Error> + where + T: FromStr, + E: std::fmt::Display + { + self.get_str(key).map_or_else( + || Ok(None), + |val| { + T::from_str(val).map_or_else( + |e| Err(Error::Param(ParamError::Value(e.to_string()))), + |v| Ok(Some(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.0.get_key_value(key); + if let Some((_k, v)) = kv { + return Some(v); + } + None + } + + /// 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, Error> { + let Some(v) = self.get_str(key) else { + return Ok(None); + }; + + let v = v.to_ascii_lowercase(); + match v.as_ref() { + "y" | "yes" | "t" | "true" | "1" | "on" => Ok(Some(true)), + "n" | "no" | "f" | "false" | "0" | "off" => Ok(Some(false)), + _ => Err(Error::Param(ParamError::Value("not boolean".into()))) + } + } + + /// Consume the `Params` buffer and return its internal `HashMap`. + #[must_use] + pub fn into_inner(self) -> HashMap { + self.0 + } + + /// 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.0 { + size += key.len() + 1; // including ' ' + size += value.len() + 1; // including '\n' + } + size + 1 // terminating '\n' + } + + /// Serialize `Params` buffer into a vector of bytes for transmission. + #[must_use] + pub fn serialize(&self) -> Vec { + let mut buf = Vec::new(); + + for (key, value) in &self.0 { + let k = key.as_bytes(); + let v = value.as_bytes(); + for a in k { + buf.push(*a); + } + buf.push(b' '); + for a in v { + buf.push(*a); + } + buf.push(b'\n'); + } + + buf.push(b'\n'); + + buf + } + + /// Write the Params to a buffer. + pub fn encoder_write(&self, buf: &mut BytesMut) { + // Calculate the required buffer size + let size = self.calc_buf_size(); + + // Reserve space + buf.reserve(size); + + // Write data to output buffer + for (key, value) in &self.0 { + buf.put(key.as_bytes()); + buf.put_u8(b' '); + buf.put(value.as_bytes()); + buf.put_u8(b'\n'); + } + buf.put_u8(b'\n'); + } +} + + +impl Params { + /// Encode a binary buffer into a parameter value. + /// + /// The value is stored as a base85 (RFC1924 variant) string. + /// + /// # Errors + /// [`Error::Param`] is returned if the key is invalid, or if the base85 + /// encoder inserts newlines. + #[cfg(feature = "bin")] + #[cfg_attr(docsrs, doc(cfg(feature = "bin")))] + pub fn encode_buf( + &mut self, + key: impl Into, + buf: &[u8] + ) -> Result<(), Error> { + self.add(key.into(), base85::encode(buf))?; + Ok(()) + } + + /// Decode a binary buffer from a parameter value. + /// + /// The value is assumed to be base85 (RFC1924 variant) string. + /// + /// # Errors + /// [`Error::Param`] with [`ParamError::Value`] is returned if the value is + /// not RFC1924-compliant base85. + #[cfg(feature = "bin")] + #[cfg_attr(docsrs, doc(cfg(feature = "bin")))] + pub fn decode_buf(&self, key: &str) -> Result>, Error> { + let Some(b85) = self.get_str(key) else { + return Ok(None); + }; + let buf = base85::decode(b85) + .map_err(|_| Error::Param(ParamError::Value("invalid base85".into())))?; + Ok(Some(buf)) + } + + /// Serialize a type (using bincode), transform into base85 and store as a + /// parameter. + /// + /// # Errors + /// [`Error::Param`] with [`ParamError::Key`] means the key name is + /// invalid. [`Error::SerializeError`] means `data` could not be serialized. + #[cfg(feature = "bincode")] + #[cfg_attr(docsrs, doc(cfg(feature = "bincode")))] + pub fn encode( + &mut self, + key: impl Into, + data: E + ) -> Result<(), Error> + where + E: bincode::Encode + { + let conf = bincode::config::standard(); + + let buf = bincode::encode_to_vec(data, conf) + .map_err(|e| Error::SerializeError(e.to_string()))?; + + self.encode_buf(key, &buf)?; + + Ok(()) + } + + /// Decode a parameter base85 value, then deserialize it into requested type. + /// + /// # Errors + /// [`Error::Param`] with [`ParamError::Value`] means the parameter value + /// could not be deserialized. + #[cfg(feature = "bincode")] + #[cfg_attr(docsrs, doc(cfg(feature = "bincode")))] + pub fn decode(&self, key: &str) -> Result, Error> + where + D: bincode::Decode<()> + { + let Some(buf) = self.decode_buf(key)? else { + return Ok(None); + }; + + let conf = bincode::config::standard(); + + let (data, _) = bincode::decode_from_slice(&buf, conf) + .map_err(|e| Error::Param(ParamError::Value(e.to_string())))?; + + Ok(Some(data)) + } +} + +#[cfg(test)] +#[cfg(feature = "bin")] +mod buf_tests { + use super::Params; + + #[test] + fn enc_dec() { + let buf = orphanage::buf::random(2048); + + let mut params = Params::new(); + params.encode_buf("Bytes", &buf).unwrap(); + + let buf2 = params.decode_buf("Bytes").unwrap().unwrap(); + assert_eq!(buf, buf2); + } +} + + +impl Params { + /// Add an anonymous vector of records. + /// + /// Only one list of elements should be added to a `Params` buffer. If a + /// single `Params` needs to contain multiple lists, use + /// [`Params::encode_named_list()`] instead. + /// + /// ``` + /// use blather::{Params, Error}; + /// + /// struct Agent { + /// name: String, + /// age: u8 + /// } + /// + /// let mut params = Params::default(); + /// + /// let mut agents = vec![ + /// Agent { + /// name: "frank".into(), + /// age: 42 + /// }, + /// Agent { + /// name: "anon".into(), + /// age: 32 + /// } + /// ]; + /// + /// params.encode_anon_list(agents.into_iter(), |rec, v| { + /// v.push(("Name".into(), rec.name)); + /// v.push(("Age".into(), rec.age.to_string())); + /// Ok::<(), Error>(()) + /// }); + /// + /// assert_eq!(params.get_fromstr::("Count").unwrap(), Some(2)); + /// + /// assert_eq!(params.get_str("Name.0"), Some("frank")); + /// assert_eq!(params.get_fromstr::("Age.0").unwrap(), Some(42)); + /// + /// assert_eq!(params.get_str("Name.1"), Some("anon")); + /// assert_eq!(params.get_fromstr::("Age.1").unwrap(), Some(32)); + /// ``` + /// + /// # Errors + /// Returns an application-defined error `E`, with the constraint that it + /// must be able to convert a `blather::Error` into `E`. + pub fn encode_anon_list(&mut self, it: I, f: F) -> Result<(), E> + where + I: IntoIterator, + F: Fn(T, &mut Vec<(String, String)>) -> Result<(), E>, + E: From + { + let mut count = 0; + + // Used for capacity + let mut recs = 0; + + for (idx, rec) in it.into_iter().enumerate() { + let mut rec_kv = Vec::with_capacity(recs); + + // Call closure to generate records + f(rec, &mut rec_kv)?; + + // Predict how many records to preallocate + recs = std::cmp::max(recs, rec_kv.len()); + + // Move indexed record fields into Params + for (k, v) in rec_kv { + let key = format!("{k}.{idx}"); + self.add(key, v)?; + } + + // Count record + count += 1; + } + + self.add_param("Count", count)?; + + Ok(()) + } + + /// Decode an anonymous list previous generated using + /// [`Params::encode_anon_list()`]. + /// + /// ``` + /// use blather::{Params, Error}; + /// + /// struct Agent { + /// name: String, + /// age: u8 + /// } + /// + /// let mut params = Params::new(); + /// params.add_str("Name.0", "frank").unwrap(); + /// params.add_param("Age.0", 42).unwrap(); + /// params.add_str("Name.1", "anon").unwrap(); + /// params.add_param("Age.1", 32).unwrap(); + /// params.add_param("Count", 2).unwrap(); + /// + /// let v = params + /// .decode_anon_list::<_, _, Error>(&["Name", "Age"], |recmap| { + /// let name = (*recmap.get("Name").unwrap()).to_string(); + /// let age = recmap.get("Age").unwrap(); + /// let age = age.parse::().unwrap(); + /// Ok(Agent { name, age }) + /// }) + /// .unwrap(); + /// + /// assert_eq!(v[0].name, "frank"); + /// assert_eq!(v[0].age, 42); + /// assert_eq!(v[1].name, "anon"); + /// assert_eq!(v[1].age, 32); + /// assert_eq!(v.len(), 2); + /// ``` + /// + /// # Errors + /// Returns an application-defined error `E`, with the constraint that it + /// must be able to convert a `blather::Error` into `E`. + pub fn decode_anon_list( + &self, + keys: &[&str], + f: F + ) -> Result, E> + where + F: Fn(&HashMap<&str, &str>) -> Result, + E: From + { + let mut ret = Vec::new(); + let mut recmap: HashMap<&str, &str> = HashMap::new(); + + // Determine how many records are in the list + let count = self.get_fromstr::("Count")?.ok_or_else(|| { + Error::Param(ParamError::Key("Missing 'Count'".into())) + })?; + + for idx in 0..count { + recmap.clear(); + + for (k, ki) in keys.iter().map(|k| (k, format!("{k}.{idx}",))) { + let Some(v) = self.0.get(&ki) else { + continue; + }; + recmap.insert(k, v); + } + + let vecrec = f(&recmap)?; + ret.push(vecrec); + } + + Ok(ret) + } + + /// Add a named vector of records. + /// + /// This function mostly works like [`Params::encode_anon_list()`], but it + /// puts all its parameters into a namespace, specified in `name`. This + /// allows multiple lists to be added to the same `Params`. + /// + /// # Errors + /// Returns an application-defined error `E`, with the constraint that it + /// must be able to convert a `blather::Error` into `E`. + pub fn encode_named_list( + &mut self, + name: &str, + it: I, + f: F + ) -> Result<(), E> + where + I: IntoIterator, + F: Fn(T, &mut Vec<(String, String)>) -> Result<(), E>, + E: From + { + let mut count = 0; + + // Used for capacity + let mut recs = 0; + + for (idx, rec) in it.into_iter().enumerate() { + let mut rec_kv = Vec::with_capacity(recs); + + // Call closure to generate records + f(rec, &mut rec_kv)?; + + // Predict how many records to preallocate + recs = std::cmp::max(recs, rec_kv.len()); + + // Move indexed record fields into Params + for (k, v) in rec_kv { + let key = format!("{name}.{k}.{idx}"); + self.add(key, v)?; + } + + // Count record + count += 1; + } + + let key = format!("{name}.Count"); + self.add_param(key, count)?; + + Ok(()) + } + + /// Decode an anonymous list previous generated using + /// [`Params::encode_anon_list()`]. + /// + /// # Errors + /// Returns an application-defined error `E`, with the constraint that it + /// must be able to convert a `blather::Error` into `E`. + pub fn decode_named_list( + &self, + name: &str, + keys: &[&str], + f: F + ) -> Result, E> + where + F: Fn(&HashMap<&str, &str>) -> Result, + E: From + { + let mut ret = Vec::new(); + let mut recmap: HashMap<&str, &str> = HashMap::new(); + + // Determine how many records are in the list + let key = format!("{name}.Count"); + let count = self.get_fromstr::(&key)?.ok_or_else(|| { + Error::Param(ParamError::Key("Missing 'Count'".into())) + })?; + + for idx in 0..count { + recmap.clear(); + + for (k, ki) in keys.iter().map(|k| (k, format!("{name}.{k}.{idx}",))) { + let Some(v) = self.0.get(&ki) else { + continue; + }; + recmap.insert(k, v); + } + + let vecrec = f(&recmap)?; + ret.push(vecrec); + } + + Ok(ret) + } +} + + +// ToDo: Use TryFrom, and validate all the fields +impl TryFrom> for Params { + type Error = Error; + + fn try_from(hm: HashMap) -> Result { + for (k, v) in &hm { + validate_param_key(k)?; + validate_param_value(v)?; + } + Ok(Self(hm)) + } +} + +// ToDo: Just forward to inner type, and make the application use {:#?} +// instead. +impl fmt::Display for Params { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut kvlist = Vec::new(); + for (key, value) in &self.0 { + kvlist.push(format!("{key}={value}")); + } + write!(f, "{{{}}}", kvlist.join(",")) + } +} + + +#[cfg(test)] +mod tests { + use super::Params; + + #[test] + fn string() { + let mut msg = Params::new(); + + msg.add_str("Foo", "bar").unwrap(); + assert_eq!(msg.get_str("Foo").unwrap(), "bar"); + + assert_eq!(msg.get_str("Moo"), None); + } + + #[test] + fn exists() { + let mut params = Params::new(); + + params.add_str("foo", "bar").unwrap(); + assert!(params.contains("foo")); + + assert!(!params.contains("nonexistent")); + } + + #[test] + fn empty_value() { + let mut params = Params::new(); + + params.add_str("foo", "").unwrap(); + + assert_eq!(params.get_str("foo"), Some("")); + } + + #[test] + fn integer() { + let mut msg = Params::new(); + + msg.add_str("Num", "64").unwrap(); + assert_eq!(msg.get_fromstr::("Num").unwrap().unwrap(), 64); + } + + #[test] + fn size() { + let mut msg = Params::new(); + + msg.add_param("Num", 7_usize).unwrap(); + assert_eq!(msg.get_fromstr::("Num").unwrap().unwrap(), 7); + } + + #[test] + fn intoparams() { + let mut msg = Params::new(); + + msg.add_str("Foo", "bar").unwrap(); + assert_eq!(msg.get_str("Foo").unwrap(), "bar"); + assert_eq!(msg.get_str("Moo"), None); + + let hm = msg.into_inner(); + let kv = hm.get_key_value("Foo"); + if let Some((_k, v)) = kv { + assert_eq!(v, "bar"); + } + } + + #[test] + fn display() { + let mut params = Params::new(); + + params.add_str("foo", "bar").unwrap(); + let s = format!("{params}"); + assert_eq!(s, "{foo=bar}"); + } + + #[test] + fn ser_size() { + let mut params = Params::new(); + + params.add_str("foo", "bar").unwrap(); + params.add_str("moo", "cow").unwrap(); + + let sz = params.calc_buf_size(); + + assert_eq!(sz, 8 + 8 + 1); + } + + #[test] + #[should_panic(expected = "Param(Key(\"invalid character\"))")] + fn key_invalid_char() { + let mut param = Params::new(); + param.add_str("hell o", "world").unwrap(); + } + + #[test] + #[should_panic(expected = "Param(Key(\"empty\"))")] + fn empty_key() { + let mut param = Params::new(); + param.add_str("", "world").unwrap(); + } + + #[test] + #[should_panic(expected = "Param(Value(\"contains newline\"))")] + fn value_newline() { + let mut param = Params::new(); + param.add_str("greeting", "hello\nworld").unwrap(); + } + + #[test] + fn boolvals() { + let mut params = Params::new(); + + params.add_bool("abool", true).unwrap(); + params.add_bool("abooltoo", false).unwrap(); + + let Ok(Some(true)) = params.get_bool("abool") else { + panic!("Unexpectedly not Ok(true)"); + }; + let Ok(Some(false)) = params.get_bool("abooltoo") else { + panic!("Unexpectedly not Ok(false)"); + }; + } + + #[test] + #[should_panic(expected = "Param(Value(\"not boolean\"))")] + fn bad_bool() { + let mut params = Params::new(); + + params.add_str("fool", "uncertain").unwrap(); + + params.get_bool("fool").unwrap(); + } +} + + +#[cfg(test)] +mod recvec_tests { + use super::{Error, Params}; + + struct Agent { + name: String, + age: u8 + } + + impl Agent { + fn new(name: impl Into, age: u8) -> Self { + Self { + name: name.into(), + age + } + } + } + + #[test] + fn encode_anon() { + let mut params = Params::new(); + + let agents = vec![Agent::new("frank", 42), Agent::new("anon", 32)]; + + params + .encode_anon_list::<_, _, _, Error>(agents, |agent, v| { + v.push(("Name".to_string(), agent.name)); + v.push(("Age".to_string(), agent.age.to_string())); + Ok(()) + }) + .unwrap(); + + assert_eq!(params.get_fromstr::("Count").unwrap().unwrap(), 2); + + assert_eq!(params.get_str("Name.0"), Some("frank")); + assert_eq!(params.get_str("Age.0"), Some("42")); + assert_eq!(params.get_str("Name.1"), Some("anon")); + assert_eq!(params.get_str("Age.1"), Some("32")); + } + + #[test] + fn decode_anon() { + let mut params = Params::new(); + params.add_param("Name.0", "frank").unwrap(); + params.add_param("Age.0", "42").unwrap(); + params.add_param("Name.1", "anon").unwrap(); + params.add_param("Age.1", "32").unwrap(); + params.add_param("Count", "2").unwrap(); + + let v = params + .decode_anon_list::<_, _, Error>(&["Name", "Age"], |recmap| { + let name = (*recmap.get("Name").unwrap()).to_string(); + let age = recmap.get("Age").unwrap(); + let age = age.parse::().unwrap(); + Ok(Agent { name, age }) + }) + .unwrap(); + + assert_eq!(v.len(), 2); + assert_eq!(&v[0].name, "frank"); + assert_eq!(v[0].age, 42); + assert_eq!(&v[1].name, "anon"); + assert_eq!(v[1].age, 32); + } + + #[test] + #[should_panic(expected = "Param(Key(\"Missing 'Count'\"))")] + fn decode_no_count() { + let params = Params::new(); + let _v = params + .decode_anon_list::<_, _, Error>(&["Name", "Age"], |recmap| { + let name = (*recmap.get("Name").unwrap()).to_string(); + let age = recmap.get("Age").unwrap(); + let age = age.parse::().unwrap(); + Ok(Agent { name, age }) + }) + .unwrap(); + } + + #[test] + #[should_panic(expected = "Param(Value(\"invalid digit found in string\"))")] + fn decode_bad_count() { + let mut params = Params::new(); + params.add_param("Count", "moo").unwrap(); + let _v = params + .decode_anon_list::<_, _, Error>(&["Name", "Age"], |recmap| { + let name = (*recmap.get("Name").unwrap()).to_string(); + let age = recmap.get("Age").unwrap(); + let age = age.parse::().unwrap(); + Ok(Agent { name, age }) + }) + .unwrap(); + } + + #[test] + fn encode_named() { + let mut params = Params::new(); + + let agents = vec![Agent::new("frank", 42), Agent::new("anon", 32)]; + + params + .encode_named_list::<_, _, _, Error>("Agents", agents, |agent, v| { + v.push(("Name".to_string(), agent.name)); + v.push(("Age".to_string(), agent.age.to_string())); + Ok(()) + }) + .unwrap(); + + assert_eq!( + params + .get_fromstr::("Agents.Count") + .unwrap() + .unwrap(), + 2 + ); + + assert_eq!(params.get_str("Agents.Name.0"), Some("frank")); + assert_eq!(params.get_str("Agents.Age.0"), Some("42")); + assert_eq!(params.get_str("Agents.Name.1"), Some("anon")); + assert_eq!(params.get_str("Agents.Age.1"), Some("32")); + } + + #[test] + fn decode_named() { + let mut params = Params::new(); + params.add_param("Agents.Name.0", "frank").unwrap(); + params.add_param("Agents.Age.0", "42").unwrap(); + params.add_param("Agents.Name.1", "anon").unwrap(); + params.add_param("Agents.Age.1", "32").unwrap(); + params.add_param("Agents.Count", "2").unwrap(); + + let v = params + .decode_named_list::<_, _, Error>("Agents", &["Name", "Age"], |recmap| { + let name = (*recmap.get("Name").unwrap()).to_string(); + let age = recmap.get("Age").unwrap(); + let age = age.parse::().unwrap(); + Ok(Agent { name, age }) + }) + .unwrap(); + + assert_eq!(v.len(), 2); + assert_eq!(&v[0].name, "frank"); + assert_eq!(v[0].age, 42); + assert_eq!(&v[1].name, "anon"); + assert_eq!(v[1].age, 32); + } +} + +// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : ADDED src/telegram.rs Index: src/telegram.rs ================================================================== --- /dev/null +++ src/telegram.rs @@ -0,0 +1,438 @@ +//! Telegrams are objects that contain a _topic_ and a set of zero or more +//! parameters. They can be serialized into a line-based format for +//! transmission over a network link. + +use std::{ + collections::HashMap, + fmt, + ops::{Deref, DerefMut} +}; + +use bytes::{BufMut, BytesMut}; + +use crate::err::Error; + +use super::{params::Params, validators::validate_topic}; + +/// 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)] +pub struct Telegram { + topic: String, + params: Params +} + +impl Deref for Telegram { + type Target = Params; + + fn deref(&self) -> &Self::Target { + &self.params + } +} + +impl DerefMut for Telegram { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.params + } +} + +impl AsRef for Telegram { + fn as_ref(&self) -> &str { + &self.topic + } +} + +impl Telegram { + /// 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() + } + } + + /// Fallibly create a new `Telegram` object. + /// + /// # Errors + /// [`Error::BadFormat`] means the topic is invalid. + pub fn try_new(topic: impl Into) -> Result { + let topic = topic.into(); + validate_topic(&topic)?; + Ok(Self { + topic, + params: Params::default() + }) + } + + /// Create a new `Telegram` object with pre-built [`Params`]. + /// + /// # Panics + /// The `topic` must be valid. + pub fn with_params(topic: impl Into, params: Params) -> Self { + let topic = topic.into(); + validate_topic(&topic).unwrap(); + Self { topic, params } + } + + /// Fallibly create a new `Telegram`, with a pre-built [`Params`]. + /// + /// # Errors + /// [`Error::BadFormat`] means the topic is invalid. + #[allow(clippy::needless_pass_by_value)] + pub fn try_with_params( + topic: impl ToString, + params: Params + ) -> Result { + let topic = topic.to_string(); + validate_topic(&topic)?; + Ok(Self { topic, params }) + } + + /// 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("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. + #[must_use] + pub const fn params(&self) -> &Params { + &self.params + } + + /// Get a mutable reference to the inner [`Params`] object. + /// + /// ``` + /// use blather::Telegram; + /// + /// let mut tg = Telegram::new("Topic"); + /// tg.add_param("cat", "meow"); + /// assert_eq!(tg.num_params(), 1); + /// tg.params_mut().clear(); + /// assert_eq!(tg.num_params(), 0); + /// ``` + pub const fn params_mut(&mut self) -> &mut Params { + &mut self.params + } + + /// Get a reference the the parameter's internal `HashMap`. + /// + /// Note: The inner representation of the Params object may change in the + /// future. + #[must_use] + pub const fn get_params_inner(&self) -> &HashMap { + self.params.inner() + } + + /// Set topic for telegram. + /// + /// Overwrites current topic is one has already been set. + /// + /// # Examples + /// ``` + /// use blather::{Telegram, Error}; + /// + /// let mut tg = Telegram::new("SomeTopic"); + /// assert!(matches!(tg.set_topic("Hello"), Ok(()))); + /// + /// let e = Error::BadFormat("Invalid topic character".to_string()); + /// 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 = 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("SomeTopic"); + /// assert_eq!(tg.get_topic(), "SomeTopic"); + /// ``` + #[must_use] + pub fn get_topic(&self) -> &str { + self.topic.as_ref() + } + + /// 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; + 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 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); + } + buf.push(b' '); + for a in v { + buf.push(*a); + } + buf.push(b'\n'); + } + + buf.push(b'\n'); + + Ok(buf) + } + + /// 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_empty() { + return Err(Error::SerializeError("Missing Telegram topic".to_string())); + } + + // Calculate the required buffer size + let size = self.calc_buf_size(); + + // Reserve space + buf.reserve(size); + + // Write data to output buffer + 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' '); + buf.put(value.as_bytes()); + buf.put_u8(b'\n'); + } + 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 { + Self { + topic, + params: Params::default() + } + } +} + +impl TryFrom<(&str, Params)> for Telegram { + type Error = Error; + + fn try_from(t: (&str, Params)) -> Result { + validate_topic(t.0)?; + Ok(Self { + topic: t.0.to_string(), + params: t.1 + }) + } +} + +impl TryFrom<(String, Params)> for Telegram { + type Error = Error; + + fn try_from(t: (String, Params)) -> Result { + validate_topic(&t.0)?; + Ok(Self { + topic: t.0, + params: t.1 + }) + } +} + +impl fmt::Display for Telegram { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}:{}", self.topic, self.params) + } +} + + +#[cfg(test)] +mod tests { + use super::{Error, Params, Telegram}; + + #[test] + fn simple() { + 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("SomeTopic"); + + tg.add_str("foo", "bar").unwrap(); + assert!(tg.contains("foo")); + + assert!(!tg.contains("nonexistent")); + } + + #[test] + fn integer() { + let mut tg = Telegram::new("SomeTopic"); + + assert_eq!(tg.get_topic(), "SomeTopic"); + + tg.add_str("Num", "64").unwrap(); + assert_eq!(tg.get_fromstr::("Num").unwrap().unwrap(), 64); + } + + #[test] + fn size() { + let mut tg = Telegram::new("SomeTopic"); + + tg.add_param("Num", 7_usize).unwrap(); + assert_eq!(tg.get_fromstr::("Num").unwrap().unwrap(), 7); + } + + #[test] + fn intoparams() { + let mut tg = Telegram::new("SomeTopic"); + + tg.add_str("Foo", "bar").unwrap(); + assert_eq!(tg.get_str("Foo").unwrap(), "bar"); + assert_eq!(tg.get_str("Moo"), None); + + let params = tg.into_params(); + let val = params.get_str("Foo"); + assert_eq!(val.unwrap(), "bar"); + } + + #[test] + fn display() { + let mut tg = Telegram::new("hello"); + + tg.add_param("foo", "bar").unwrap(); + let s = format!("{tg}"); + assert_eq!(s, "hello:{foo=bar}"); + } + + #[test] + fn ser_size() { + let mut tg = Telegram::new("hello"); + + tg.add_str("foo", "bar").unwrap(); + tg.add_str("moo", "cow").unwrap(); + + let sz = tg.calc_buf_size(); + + assert_eq!(sz, 6 + 8 + 8 + 1); + } + + #[test] + fn bad_topic_leading() { + 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("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() { + let mut params = Params::new(); + params.add_str("my", "word").unwrap(); + let tg = Telegram::try_from(("Hello", params)).unwrap(); + + assert_eq!(tg.get_str("my"), Some("word")); + } +} + +// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : DELETED src/types.rs Index: src/types.rs ================================================================== --- src/types.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Collection of data types which can be sent/received using the internal -//! [`Codec`](crate::codec::Codec) - -pub mod kvlines; -pub mod params; -pub mod telegram; - -mod validators; - -pub use kvlines::{KVLines, KeyValue}; -pub use params::Params; -pub use telegram::Telegram; - -// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : DELETED src/types/kvlines.rs Index: src/types/kvlines.rs ================================================================== --- src/types/kvlines.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! A key/value pair list with stable ordering and non-unique keys. - -use std::fmt; - -use bytes::{BufMut, BytesMut}; - -/// Representation of a key/value pair in `KVLines`. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct KeyValue { - key: String, - value: String -} - -/// Ordered list of key/value pairs, with no uniqueness constraint for the -/// keys. -#[derive(Debug, Clone, Default)] -pub struct KVLines { - lines: Vec -} - -impl KVLines { - /// Create a new empty parameters object. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Reset all the lines. - pub fn clear(&mut self) { - self.lines.clear(); - } - - /// Get a reference to the inner vector of [`KeyValue`]'s. - #[must_use] - pub const fn get_inner(&self) -> &Vec { - &self.lines - } - - /// Append a key/value entry to the end of the list. - #[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' - } - size + 1 // terminating '\n' - } - - - /// Serialize object into a `Vec` buffer suitable for transmission. - #[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(); - for a in k { - buf.push(*a); - } - buf.push(b' '); - for a in v { - buf.push(*a); - } - buf.push(b'\n'); - } - - buf.push(b'\n'); - - buf - } - - /// Write the Params to a buffer. - pub fn encoder_write(&self, buf: &mut BytesMut) { - // Calculate the required buffer size - let size = self.calc_buf_size(); - - // Reserve space - buf.reserve(size); - - // Write data to output buffer - for n in &self.lines { - buf.put(n.key.as_bytes()); - buf.put_u8(b' '); - buf.put(n.value.as_bytes()); - buf.put_u8(b'\n'); - } - buf.put_u8(b'\n'); - } - - /// 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 { - Self { lines } - } -} - - -impl From> for KVLines { - fn from(lines: Vec<(String, String)>) -> Self { - let mut out = Self { lines: Vec::new() }; - for (key, value) in lines { - out.append(key, value); - } - out - } -} - -impl fmt::Display for KVLines { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut kvlist = Vec::new(); - for n in &self.lines { - kvlist.push(format!("{}={}", n.key, n.value)); - } - write!(f, "{{{}}}", kvlist.join(",")) - } -} - -// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : DELETED src/types/params.rs Index: src/types/params.rs ================================================================== --- src/types/params.rs +++ /dev/null @@ -1,567 +0,0 @@ -//! The `Params` buffer is a set of unorderded key/value pairs, with unique -//! keys. It's similar to a `HashMap`, but has constraints on key names and -//! offers conventions for value layouts, such as comma-separated values for -//! lists. - -use std::{ - collections::{HashMap, HashSet}, - fmt, - str::FromStr -}; - -use bytes::{BufMut, BytesMut}; - -use super::validators::validate_param_key; - -use crate::err::Error; - -/// Key/value parameters storage with helper methods to make adding and getting -/// common value types slightly more ergonomic and using a plain `HashMap`. -/// -/// Uses `String`s for both keys and values internally. -#[derive(Debug, Clone, Default)] -pub struct Params { - hm: HashMap -} - -impl Params { - /// Create a new empty parameters object. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - - /// Reset all the key/values in `Params` object. - pub fn clear(&mut self) { - 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`. - #[must_use] - pub const fn get_inner(&self) -> &HashMap { - &self.hm - } - - - /// Add a parameter to the parameter. - /// - /// The `key` and `value` parameters are generic over the trait `ToString`, - /// allowing a polymorphic behavior. - /// - /// # Examples - /// ``` - /// use blather::Params; - /// - /// let mut params = Params::new(); - /// params.add_param("integer", 42).unwrap(); - /// params.add_param("string", "hello").unwrap(); - /// ``` - /// - /// # Errors - /// [`Error::BadFormat`] means the input parameters are invalid. - #[allow(clippy::needless_pass_by_value)] - pub fn add_param( - &mut self, - key: impl ToString, - value: impl ToString - ) -> Result<(), Error> { - let key = key.to_string(); - - validate_param_key(&key)?; - - self.hm.insert(key, value.to_string()); - Ok(()) - } - - - /// Add a string parameter to the parameter. - /// - /// # 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) - } - - - /// Add parameter where the value is generated from an iterator over - /// strings, where entries are comma-separated. - /// - /// # Examples - /// ``` - /// use std::collections::HashSet; - /// use blather::Params; - /// - /// let mut params = Params::new(); - /// - /// params.add_strit("Cat", &["meow", "paws", "tail"]).unwrap(); - /// assert_eq!(params.get_str("Cat"), Some("meow,paws,tail")); - /// - /// let v = vec!["meow", "paws", "tail"]; - /// params.add_strit("CatToo", v.into_iter()).unwrap(); - /// assert_eq!(params.get_str("CatToo"), Some("meow,paws,tail")); - /// - /// 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 { - sv.push(o.as_ref().to_string()); - } - self.add_param(key, sv.join(","))?; - - Ok(()) - } - - - /// Add a boolean parameter. - /// - /// # Examples - /// ``` - /// 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!(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. - /// - /// # Errors - /// [`Error::BadFormat`] means the input parameters are invalid. - pub fn add_bool( - &mut self, - key: impl ToString, - value: bool - ) -> Result<(), Error> { - 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) - } - - - /// Get a parameter and convert it to a requested type, fail if key isn't - /// found. - /// - /// # Examples - /// ``` - /// use blather::{Params, Error}; - /// - /// 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"); - /// 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 { - 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. - /// - /// # Examples - /// ``` - /// use blather::Params; - /// - /// let mut params = Params::new(); - /// let val = params.get_param_def::("nonexist", 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 { - 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); - } - None - } - - - /// Get string representation of a value for a requested key. Returns a - /// default value if key does not exist in parameter buffer. - /// - /// # Examples - /// ``` - /// use blather::Params; - /// - /// let params = Params::new(); - /// 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 { - def - } - } - - - /// Get a parameter and convert it to an integer type. - /// - /// # Examples - /// ``` - /// use blather::Params; - /// - /// let mut params = Params::new(); - /// params.add_param("Num", 7); - /// assert_eq!(params.get_int::("Num").unwrap(), 7); - /// ``` - /// - /// # 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 { - 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. - /// - /// # Examples - /// ``` - /// use blather::Params; - /// - /// let mut params = Params::new(); - /// params.add_param("num", 11); - /// assert_eq!(params.get_int_def::("num", 5).unwrap(), 11); - /// assert_eq!(params.get_int_def::("nonexistent", 17).unwrap(), 17); - /// ``` - /// - /// # 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 { - 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" => { - return Ok(true); - } - "n" | "no" | "f" | "false" | "0" => { - return Ok(false); - } - _ => { - return Err(Error::BadFormat( - "Unrecognized boolean value".to_string() - )); - } - } - } - - 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 - /// ``` - /// use blather::Params; - /// - /// 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(','); - for s in split { - if !s.is_empty() { - ret.push(s.to_string()); - } - } - } - - 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. - /// - /// # Examples - /// ``` - /// use blather::Params; - /// - /// let mut params = Params::new(); - /// params.add_param("set", "elena,chloe"); - /// let set = params.get_hashset("set").unwrap(); - /// 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(','); - for s in split { - if !s.is_empty() { - ret.insert(s.to_string()); - } - } - } - - Ok(ret) - } - - - /// 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' - } - size + 1 // terminating '\n' - } - - - /// Serialize `Params` buffer into a vector of bytes for transmission. - #[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(); - for a in k { - buf.push(*a); - } - buf.push(b' '); - for a in v { - buf.push(*a); - } - buf.push(b'\n'); - } - - buf.push(b'\n'); - - buf - } - - - /// Write the Params to a buffer. - pub fn encoder_write(&self, buf: &mut BytesMut) { - // Calculate the required buffer size - let size = self.calc_buf_size(); - - // Reserve space - buf.reserve(size); - - // Write data to output buffer - for (key, value) in &self.hm { - buf.put(key.as_bytes()); - buf.put_u8(b' '); - buf.put(value.as_bytes()); - buf.put_u8(b'\n'); - } - buf.put_u8(b'\n'); - } - - /// 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 { - 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}")); - } - write!(f, "{{{}}}", kvlist.join(",")) - } -} - -// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : DELETED src/types/telegram.rs Index: src/types/telegram.rs ================================================================== --- src/types/telegram.rs +++ /dev/null @@ -1,507 +0,0 @@ -//! Telegrams are objects that contain a _topic_ and a set of zero or more -//! parameters. They can be serialized into a line-based format for -//! transmission over a network link. - -use std::{ - collections::{HashMap, HashSet}, - fmt, - str::FromStr -}; - -use bytes::{BufMut, BytesMut}; - -use crate::err::Error; - -use super::{params::Params, validators::validate_topic}; - -/// 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)] -pub struct Telegram { - topic: String, - params: Params -} - -impl Telegram { - /// 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("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. - #[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("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`. - /// - /// Note: The inner representation of the Params object may change in the - /// future. - #[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("SomeTopic"); - /// assert!(matches!(tg.set_topic("Hello"), Ok(()))); - /// - /// let e = Error::BadFormat("Invalid topic character".to_string()); - /// 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 = 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("SomeTopic"); - /// assert_eq!(tg.get_topic(), "SomeTopic"); - /// ``` - #[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. - /// - /// # Examples - /// ``` - /// use blather::Telegram; - /// - /// 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). - /// - /// # Errors - /// [`Error::BadFormat`] means the input parameters are invalid. - pub fn add_param( - &mut self, - 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("SomeTopic"); - /// tg.add_strit("Cat", &["meow", "paws", "tail"]).unwrap(); - /// 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("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("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 - /// 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; - 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 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); - } - buf.push(b' '); - for a in v { - buf.push(*a); - } - buf.push(b'\n'); - } - - buf.push(b'\n'); - - Ok(buf) - } - - /// 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_empty() { - return Err(Error::SerializeError("Missing Telegram topic".to_string())); - } - - // Calculate the required buffer size - let size = self.calc_buf_size(); - - // Reserve space - buf.reserve(size); - - // Write data to output buffer - 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' '); - buf.put(value.as_bytes()); - buf.put_u8(b'\n'); - } - 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 { - 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 = Self::new(t.0.to_string()); - tg.params = t.1; - Ok(tg) - } -} - -impl fmt::Display for Telegram { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}:{}", self.topic, self.params) - } -} - -// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : DELETED src/types/validators.rs Index: src/types/validators.rs ================================================================== --- src/types/validators.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::err::Error; - -fn is_topic_leading_char(c: char) -> bool { - c.is_alphabetic() -} - -fn is_topic_char(c: char) -> bool { - c.is_alphanumeric() || c == '_' || c == '-' -} - -/// Make sure that topic string is valid. -pub fn validate_topic(topic: &str) -> Result<(), Error> { - let mut chars = topic.chars(); - match chars.next() { - Some(c) => { - if !is_topic_leading_char(c) { - return Err(Error::BadFormat( - "Invalid leading topic character".to_string() - )); - } - } - None => return Err(Error::BadFormat("Empty or broken topic".to_string())) - } - - if chars.any(|c| !is_topic_char(c)) { - return Err(Error::BadFormat("Invalid topic character".to_string())); - } - Ok(()) -} - - -fn is_key_char(c: char) -> bool { - c.is_alphanumeric() || c.is_ascii_punctuation() -} - -/// Make sure that a parameter key is valid. -pub fn validate_param_key(key: &str) -> Result<(), Error> { - let mut chars = key.chars(); - match chars.next() { - Some(c) => { - if !is_key_char(c) { - return Err(Error::BadFormat("Invalid key character".to_string())); - } - } - None => return Err(Error::BadFormat("Empty or broken key".to_string())) - } - - if chars.any(|c| !is_key_char(c)) { - return Err(Error::BadFormat("Invalid key character".to_string())); - } - Ok(()) -} - - -#[cfg(test)] -mod tests { - use super::validate_topic; - use super::Error; - - #[test] - fn ok_topic_1() { - assert!(validate_topic("Foobar").is_ok()); - } - - #[test] - fn empty_topic() { - 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() { - 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() { - 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 : ADDED src/validators.rs Index: src/validators.rs ================================================================== --- /dev/null +++ src/validators.rs @@ -0,0 +1,103 @@ +use crate::err::{Error, ParamError}; + +fn is_topic_leading_char(c: char) -> bool { + c.is_alphabetic() +} + +fn is_topic_char(c: char) -> bool { + c.is_alphanumeric() || c == '_' || c == '-' +} + +/// Make sure that topic string is valid. +pub fn validate_topic(topic: &str) -> Result<(), Error> { + let mut chars = topic.chars(); + match chars.next() { + Some(c) => { + if !is_topic_leading_char(c) { + return Err(Error::BadFormat( + "Invalid leading topic character".to_string() + )); + } + } + None => return Err(Error::BadFormat("Empty or broken topic".to_string())) + } + + if chars.any(|c| !is_topic_char(c)) { + return Err(Error::BadFormat("Invalid topic character".to_string())); + } + Ok(()) +} + + +fn is_key_char(c: char) -> bool { + c.is_alphanumeric() || c.is_ascii_punctuation() +} + +/// Make sure that a parameter key is valid. +pub fn validate_param_key(key: &str) -> Result<(), Error> { + if key.is_empty() { + return Err(Error::Param(ParamError::Key("empty".into()))); + } + + let mut chars = key.chars(); + if chars.any(|c| !is_key_char(c)) { + return Err(Error::Param(ParamError::Key("invalid character".into()))); + } + Ok(()) +} + +/// Make sure that a parameter value is valid. +pub fn validate_param_value(val: &str) -> Result<(), Error> { + (!val.contains('\n')).then_some(()).ok_or_else(|| { + Error::Param(ParamError::Value("contains newline".into())) + })?; + Ok(()) +} + + +#[cfg(test)] +mod tests { + use super::{validate_param_value, validate_topic, Error}; + + #[test] + fn ok_topic_1() { + assert!(validate_topic("Foobar").is_ok()); + } + + #[test] + fn empty_topic() { + 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() { + 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() { + let Err(Error::BadFormat(msg)) = validate_topic(" foobar") else { + panic!("Unexpectedly not Error::BadFormat"); + }; + assert_eq!(msg, "Invalid leading topic character"); + } + + #[test] + fn okval() { + validate_param_value("hello world").unwrap(); + } + + #[test] + #[should_panic(expected = "Param(Value(\"contains newline\"))")] + fn val_newline() { + validate_param_value("hello\nworld").unwrap(); + } +} + +// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : DELETED tests/conn_telegram.rs Index: tests/conn_telegram.rs ================================================================== --- tests/conn_telegram.rs +++ /dev/null @@ -1,86 +0,0 @@ -use tokio_stream::StreamExt; - -use tokio_test::io::Builder; - -use tokio_util::codec::Framed; - -use blather::{codec, Codec, Error}; - -#[tokio::test] -async fn valid_no_params() { - let mut mock = Builder::new(); - - mock.read(b"hello\n\n"); - - 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(), "hello"); - let params = tg.into_params(); - let map = params.into_inner(); - assert_eq!(map.len(), 0); - } else { - panic!("Not a Telegram"); - } - } -} - - -#[tokio::test] -async fn valid_with_params() { - let mut mock = Builder::new(); - - mock.read(b"hello\nmurky_waters off\nwrong_impression cows\n\n"); - - let mut frm = Framed::new(mock.build(), Codec::new()); - - while let Some(o) = frm.next().await { - let o = o.unwrap(); - - match o { - codec::Input::Telegram(tg) => { - 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"); - } - _ => { - panic!("Not a Telegram"); - } - } - } -} - - -#[tokio::test] -async fn bad_topic() { - let mut mock = Builder::new(); - - // space isn't allowed in topic - mock.read(b"hel lo\n\n"); - - let mut frm = Framed::new(mock.build(), Codec::new()); - if let Some(e) = frm.next().await { - if let Err(e) = e { - match e { - Error::Protocol(s) => { - assert_eq!(s, "Bad format; Invalid topic character"); - } - _ => { - panic!("Wrong error"); - } - } - } else { - panic!("Unexpected success"); - } - } else { - panic!("Didn't get expected frame"); - } -} - - -// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : DELETED tests/params.rs Index: tests/params.rs ================================================================== --- tests/params.rs +++ /dev/null @@ -1,128 +0,0 @@ -use blather::{Error, Params}; - - -#[test] -fn string() { - let mut msg = Params::new(); - - msg.add_str("Foo", "bar").unwrap(); - assert_eq!(msg.get_str("Foo").unwrap(), "bar"); - - assert_eq!(msg.get_str("Moo"), None); -} - - -#[test] -fn exists() { - let mut params = Params::new(); - - params.add_str("foo", "bar").unwrap(); - assert!(params.have("foo")); - - assert!(!params.have("nonexistent")); -} - - -#[test] -fn integer() { - let mut msg = Params::new(); - - msg.add_str("Num", "64").unwrap(); - assert_eq!(msg.get_int::("Num").unwrap(), 64); -} - - -#[test] -fn size() { - let mut msg = Params::new(); - - msg.add_param("Num", 7_usize).unwrap(); - assert_eq!(msg.get_int::("Num").unwrap(), 7); -} - - -#[test] -fn intoparams() { - let mut msg = Params::new(); - - msg.add_str("Foo", "bar").unwrap(); - assert_eq!(msg.get_str("Foo").unwrap(), "bar"); - assert_eq!(msg.get_str("Moo"), None); - - let hm = msg.into_inner(); - let kv = hm.get_key_value("Foo"); - if let Some((_k, v)) = kv { - assert_eq!(v, "bar"); - } -} - - -#[test] -fn display() { - let mut params = Params::new(); - - params.add_str("foo", "bar").unwrap(); - let s = format!("{params}"); - assert_eq!(s, "{foo=bar}"); -} - - -#[test] -fn ser_size() { - let mut params = Params::new(); - - params.add_str("foo", "bar").unwrap(); - params.add_str("moo", "cow").unwrap(); - - let sz = params.calc_buf_size(); - - assert_eq!(sz, 8 + 8 + 1); -} - - -#[test] -fn def_int() { - let params = Params::new(); - - let num = params.get_int_def::("nonexistent", 42).unwrap(); - - assert_eq!(num, 42); -} - - -#[test] -fn broken_key() { - let mut param = Params::new(); - 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(); - 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() { - let mut params = Params::new(); - - params.add_bool("abool", true).unwrap(); - params.add_bool("abooltoo", false).unwrap(); - - 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 : DELETED tests/params_strvec.rs Index: tests/params_strvec.rs ================================================================== --- tests/params_strvec.rs +++ /dev/null @@ -1,110 +0,0 @@ -use std::collections::HashSet; - -use blather::Params; - -#[test] -fn strvec_empty() { - let mut params = Params::new(); - params.add_str("hello", "").unwrap(); - - let sv = params.get_strvec("hello").unwrap(); - assert_eq!(sv.len(), 0); -} - - -#[test] -fn strvec_single() { - let mut params = Params::new(); - params.add_str("hello", "foo").unwrap(); - - let sv = params.get_strvec("hello").unwrap(); - assert_eq!(sv.len(), 1); - assert_eq!(sv[0], "foo"); -} - - -#[test] -fn strvec_two() { - let mut params = Params::new(); - params.add_str("hello", "foo,bar").unwrap(); - - let sv = params.get_strvec("hello").unwrap(); - assert_eq!(sv.len(), 2); - assert_eq!(sv[0], "foo"); - assert_eq!(sv[1], "bar"); -} - - -#[test] -fn strvec_single_add() { - let mut params = Params::new(); - - let sv = vec!["foo"]; - params.add_strit("hello", &sv).unwrap(); - - - //let v = params.get_str("hello").unwrap(); - assert_eq!(params.get_str("hello"), Some("foo")); - - - let sv = params.get_strvec("hello").unwrap(); - assert_eq!(sv.len(), 1); - assert_eq!(sv[0], "foo"); -} - - -#[test] -fn strvec_two_add() { - let mut params = Params::new(); - - let sv = vec!["foo", "bar"]; - params.add_strit("hello", &sv).unwrap(); - - assert_eq!(params.get_str("hello"), Some("foo,bar")); - - let sv = params.get_strvec("hello").unwrap(); - assert_eq!(sv.len(), 2); - assert_eq!(sv[0], "foo"); - assert_eq!(sv[1], "bar"); -} - - -#[test] -fn slice_two_add() { - let mut params = Params::new(); - - let slice = &["foo", "bar"]; - params.add_strit("hello", slice).unwrap(); - - assert_eq!(params.get_str("hello"), Some("foo,bar")); - - let sv = params.get_strvec("hello").unwrap(); - assert_eq!(sv.len(), 2); - assert_eq!(sv[0], "foo"); - assert_eq!(sv[1], "bar"); -} - - -#[test] -fn add_hashset() { - let mut params = Params::new(); - - let mut hs = HashSet::new(); - hs.insert("foo"); - hs.insert("bar"); - - params.add_strit("hello", hs).unwrap(); - - let sv = params.get_strvec("hello").unwrap(); - assert_eq!(sv.len(), 2); - - if sv[0] == "foo" { - assert_eq!(sv[1], "bar"); - } else { - assert_eq!(sv[0], "bar"); - assert_eq!(sv[1], "foo"); - } -} - - -// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : DELETED tests/telegram.rs Index: tests/telegram.rs ================================================================== --- tests/telegram.rs +++ /dev/null @@ -1,125 +0,0 @@ -use blather::{Error, Params, Telegram}; - - -#[test] -fn simple() { - 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("SomeTopic"); - - tg.add_str("foo", "bar").unwrap(); - assert!(tg.have_param("foo")); - - assert!(!tg.have_param("nonexistent")); -} - - -#[test] -fn integer() { - let mut tg = Telegram::new("SomeTopic"); - - assert_eq!(tg.get_topic(), "SomeTopic"); - - tg.add_str("Num", "64").unwrap(); - assert_eq!(tg.get_int::("Num").unwrap(), 64); -} - - -#[test] -fn size() { - let mut tg = Telegram::new("SomeTopic"); - - tg.add_param("Num", 7_usize).unwrap(); - assert_eq!(tg.get_int::("Num").unwrap(), 7); -} - - -#[test] -fn intoparams() { - let mut tg = Telegram::new("SomeTopic"); - - tg.add_str("Foo", "bar").unwrap(); - assert_eq!(tg.get_str("Foo").unwrap(), "bar"); - assert_eq!(tg.get_str("Moo"), None); - - let params = tg.into_params(); - let val = params.get_str("Foo"); - assert_eq!(val.unwrap(), "bar"); -} - - -#[test] -fn display() { - let mut tg = Telegram::new("hello"); - - tg.add_param("foo", "bar").unwrap(); - let s = format!("{tg}"); - assert_eq!(s, "hello:{foo=bar}"); -} - - -#[test] -fn ser_size() { - let mut tg = Telegram::new("hello"); - - tg.add_str("foo", "bar").unwrap(); - tg.add_str("moo", "cow").unwrap(); - - let sz = tg.calc_buf_size(); - - assert_eq!(sz, 6 + 8 + 8 + 1); -} - -#[test] -fn def_int() { - 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(); - - assert_eq!(num, 42); -} - - -#[test] -fn bad_topic_leading() { - 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("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() { - let mut params = Params::new(); - params.add_str("my", "word").unwrap(); - let tg = Telegram::try_from(("Hello", params)).unwrap(); - - assert_eq!(tg.get_str("my"), Some("word")); -} - -// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : DELETED tests/tg_to_expect.rs Index: tests/tg_to_expect.rs ================================================================== --- tests/tg_to_expect.rs +++ /dev/null @@ -1,39 +0,0 @@ -use tokio_stream::StreamExt; - -use tokio_test::io::Builder; - -use tokio_util::codec::Framed; - -use blather::{codec, Codec}; - -#[tokio::test] -async fn tg_followed_by_buf() { - let mut mock = Builder::new(); - - mock.read(b"hello\nlen 4\n\n1234"); - - let mut frm = Framed::new(mock.build(), Codec::new()); - - let Some(o) = frm.next().await else { - panic!("No frame"); - }; - let o = o.unwrap(); - - if let codec::Input::Telegram(tg) = o { - 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"); - } - - while let Some(o) = frm.next().await { - let o = o.unwrap(); - if let codec::Input::Bytes(_bm) = o { - } else { - panic!("Not a Buf"); - } - } -} - -// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: www/changelog.md ================================================================== --- www/changelog.md +++ www/changelog.md @@ -2,17 +2,70 @@ ⚠️ indicates a breaking change. ## [Unreleased] -[Details](/vdiff?from=blather-0.11.0&to=trunk) +[Details](/vdiff?from=blather-0.12.0&to=trunk) + +### Added + +### Changed + +### Removed + +--- + +## [0.12.0] - 2025-05-13 + +[Details](/vdiff?from=blather-0.11.0&to=blather-0.12.0) ### Added +- Proper value validation. +- `Params::get_fromstr()` takes the place of `get_param()`, and is more + explicit about what it does -- call the `FromStr` trait to parse the + parameter's value. +- Added the ability to the codec `Decoder` to send incoming `Bytes` through a + channel. This can be activated using `Codec::expect_bytes_channel()`. +- `Error::Param` and `ParamError` are used to signal errors when + getting/setting parameters from/to `Params`. +- `Params::add()` was added as a no-conversion means to add `String`s to a + `Params`. +- The `bool` parser in `Params` now also recognizes `on` and `off`. +- `Params::encode_anon_list()`/`Params::decode_anon_list()` + can be used to encode a `Vec` in a `Params` buffer. +- `Params::encode_named_list()`/`Params_decode_named_list()` serve the same + role as their `anon` counter-parts, but the `named` variant supports using a + namespace, allowing multiple lists in the same `Params` buffer. +- Support for encoding/decoding binary data into `Params` values. Requires the + feature `bin`. +- Support for encoding/decoding types into/from Params values using the + `bincode` crate. Requires the feature `bincode`. + ### Changed +- ⚠️ Major redesign + - `Telegram` no longer reimplement methods as thin wrappers around `Params`. + Instead it implements `Deref` and `DerefMut` so the `Params`' functionality + can be accessed through the `Telegram`. +- Removed the `types` module; moved all its submodules to the top module. +- Added missing error-handling in various places. +- ⚠️ Methods that previously could return `Error::KeyNotFound` now wrap their + return value in `Option` instead. +- ⚠️ Renamed `Params::have()` to `Params::contains()`. +- ⚠️ `From> for Paramas` changed to `TryFrom`, and now + performs key/value validation. + ### Removed + +- ⚠️ All the `_def` methods have been removed. Use iterator adapters instead. +- ⚠️ Removed the `codec/utils` module, including its `expect_telegram()` + function. +- ⚠️ `Error::BadState` was removed. It was unused. +- ⚠️ `Error::KeyNotFound` was removed. Instead, methods use `Option` to signal + that a key was not found. + --- ## [0.11.0] - 2024-09-22 Index: www/index.md ================================================================== --- www/index.md +++ www/index.md @@ -1,10 +1,20 @@ # blather A talkative, somwhat reminiscent of HTTP, line-based protocol, implemented as a tokio-util Codec. + +## Feature labels in documentation + +The crate's documentation uses automatically generated feature labels, which +currently requires nightly featuers. To build the documentation locally use: + +``` +$ RUSTFLAGS="--cfg docsrs" RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +``` + ## Change log The details of changes can always be found in the timeline, but for a high-level view of changes between released versions there's a manually