Index: .efiles ================================================================== --- .efiles +++ .efiles @@ -1,8 +1,11 @@ Cargo.toml -src/lib.rs +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 Index: Cargo.toml ================================================================== --- Cargo.toml +++ Cargo.toml @@ -1,9 +1,8 @@ [package] name = "blather" -version = "0.9.0" -authors = ["Jan Danielsson "] +version = "0.10.0" edition = "2021" license = "0BSD" keywords = [ "line-based", "protocol", "tokio", "codec" ] repository = "https://repos.qrnch.tech/pub/blather" description = "A talkative line-based protocol" @@ -10,21 +9,24 @@ exclude = [ ".fossil-settings", ".efiles", ".fslckout", "rustfmt.toml", - "tests", "www" ] [dependencies] -bytes = { version = "1.1.0" } -futures = { version = "0.3.21" } -tokio = { version = "1.17.0" } -tokio-util = { version= "0.7.0", features = ["codec"] } +bytes = { version = "1.5.0" } +futures = { version = "0.3.30" } +# Tempoarily add "net", because there's a rustdoc bug in tokio 1.36.0. +tokio = { version = "1.36.0", features = ["net"] } +tokio-util = { version= "0.7.10", features = ["codec"] } [dev-dependencies] -tokio = { version = "1.17.0", features = ["macros", "net"] } -tokio-stream = { version = "0.1.8" } -tokio-test = { version = "0.4.2" } +tokio = { version = "1.36.0", features = ["macros", "net"] } +tokio-stream = { version = "0.1.14" } +tokio-test = { version = "0.4.3" } + +[package.metadata.docs.rs] +rustdoc-args = ["--generate-link-to-definition"] # vim: set ft=toml et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : ADDED README.md Index: README.md ================================================================== --- /dev/null +++ README.md @@ -0,0 +1,5 @@ +# blather + +A talkative, somwhat reminiscent to HTTP, line-based protocol, implemented as +a tokio-util Codec. + Index: src/codec.rs ================================================================== --- src/codec.rs +++ src/codec.rs @@ -1,23 +1,23 @@ //! A [`tokio_util::codec`] Codec that is used to encode and decode the //! blather protocol. -use std::fmt; -use std::fs::File; -use std::io::Write; -use std::path::PathBuf; -use std::{cmp, collections::HashMap, mem}; +use std::{ + fmt, + {cmp, collections::HashMap, mem} +}; use bytes::{BufMut, Bytes, BytesMut}; use tokio::io; -use tokio_util::codec::Decoder; -use tokio_util::codec::Encoder; +use tokio_util::codec::{Decoder, Encoder}; -use crate::err::Error; -use crate::{KVLines, Params, Telegram}; +use crate::{ + err::Error, + {KVLines, Params, Telegram} +}; /// Current state of decoder. /// /// Controls what, if anything, will be returned to the application. @@ -38,22 +38,10 @@ /// Read a specified amount of raw bytes, and return the entire immutable /// buffer when it has arrived. Bytes, - /// Read a specified amount of raw bytes, and return the entire mutable - /// buffer when it has arrived. - BytesMut, - - /// Read a specified amount of raw bytes and store them in chunks as they - /// arrive in a file. - File, - - /// Read a specified amount of raw bytes and write them in chunks as they - /// arrive to a writer object. - Writer, - /// Ignore a specified amount of raw bytes. Skip } /// Data returned to the application when the Codec's Decode iterator is @@ -67,27 +55,17 @@ /// A complete [`Params`] has been received. Params(Params), /// A chunk of raw data has arrived. The second argument is the amount of - /// data remains, which has been adjusted for the current [`BytesMut`]. If - /// the `usize` parameter is 0 it means this is the final chunk. - Chunk(BytesMut, usize), + /// data remains, which has been adjusted for the current [`Bytes`]. If + /// 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), - /// A complete raw mutable buffer has been received. - BytesMut(BytesMut), - - /// A complete buffer has been received and stored to the file specified in - /// `PathBuf`. - File(PathBuf), - - /// A complete buffer has been written to the writer. - WriteDone, - /// The requested number of bytes have been ignored. SkipDone } @@ -98,14 +76,11 @@ max_line_length: usize, tg: Telegram, params: Params, kvlines: KVLines, state: CodecState, - bin_remain: usize, - pathname: Option, - writer: Option>, - buf: BytesMut + remain: u64 } impl fmt::Debug for Codec { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Codec").field("state", &self.state).finish() @@ -136,11 +111,11 @@ /// let mut conn = Framed::new(socket, Codec::new()); /// /// // .. do stuff .. /// /// let len = 8192; -/// conn.codec_mut().expect_bytesmut(len); +/// conn.codec_mut().expect_bytes(len); /// } /// ``` impl Codec { /// Create a new `Codec`. It will default to having not practical limit to /// the maximum line length and it will expect a [`Telegram`] buffer to @@ -151,14 +126,11 @@ max_line_length: usize::MAX, tg: Telegram::new(), params: Params::new(), kvlines: KVLines::new(), state: CodecState::Telegram, - bin_remain: 0, - pathname: None, - writer: None, - buf: BytesMut::new() + remain: 0 } } /// Create a new `Codec` with a specific maximum line length. The default /// state will be to expect a [`Telegram`]. @@ -194,11 +166,14 @@ /// The first line received is a telegram topic. This is a required line. /// Following lines are parameter lines, which are a single space character /// separated key/value pairs. fn decode_telegram_line(&mut self, line: &str) -> Result<(), Error> { if self.tg.get_topic().is_none() { - self.tg.set_topic(line)?; + self + .tg + .set_topic(line) + .map_err(|e| Error::Protocol(e.to_string()))?; } else { let idx = line.find(' '); if let Some(idx) = idx { let (k, v) = line.split_at(idx); let v = &v[1..v.len()]; @@ -241,21 +216,21 @@ } */ /// Get index of the next end of line in `buf`. fn get_eol_idx(&mut self, buf: &BytesMut) -> Result, Error> { - let (read_to, newline_offset) = self.find_newline(&buf); + let (read_to, newline_offset) = self.find_newline(buf); match newline_offset { Some(offset) => { // Found an eol let newline_index = offset + self.next_line_index; self.next_line_index = 0; Ok(Some(newline_index + 1)) } - None if buf.len() > self.max_line_length => Err(Error::BadFormat( - "Exceeded maximum line length.".to_string() - )), + None if buf.len() > self.max_line_length => { + Err(Error::Protocol("Exceeded maximum line length.".to_string())) + } None => { // Didn't find a line or reach the length limit, so the next // call will resume searching at the current offset. self.next_line_index = read_to; @@ -289,11 +264,11 @@ // mem::take() can replace a member of a struct. // (This requires Default to be implemented for the object being // taken). return Ok(Some(mem::take(&mut self.tg))); } else { - self.decode_telegram_line(&line)?; + self.decode_telegram_line(line)?; } } else { // Returning Ok(None) instructs the FramedRead that more data is // needed. return Ok(None); @@ -393,14 +368,20 @@ /// chunk, which means that the application can detect the end of the /// buffer by checking if the remaining value is zero. /// /// Once the entire buffer has been received by the `Decoder` it will revert /// to expect an [`Input::Telegram`]. - pub fn expect_chunks(&mut self, size: usize) { + 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())); + } + //println!("Expecting bin {}", size); self.state = CodecState::Chunks; - self.bin_remain = size; + self.remain = size; + + Ok(()) } /// Expect a immutable buffer of a certain size to be received. /// @@ -416,93 +397,18 @@ pub fn expect_bytes(&mut self, size: usize) -> Result<(), Error> { if size == 0 { return Err(Error::InvalidSize("The size must not be zero".to_string())); } self.state = CodecState::Bytes; - self.bin_remain = size; - self.buf = BytesMut::with_capacity(size); - Ok(()) - } - - - /// Expect a mutable buffer of a certain size to be received. - /// - /// The returned buffer will be stored in process memory. - /// - /// # Decoder behavior - /// Once a complete buffer has been successfully reaceived the `Decoder` will - /// return an [`Input::BytesMut(b)`](Input::BytesMut) where `b` is a - /// [`bytes::BytesMut`] containing the entire buffer. - /// - /// Once the entire buffer has been received by the `Decoder` it will revert - /// to expect an [`Input::Telegram`]. - pub fn expect_bytesmut(&mut self, size: usize) -> Result<(), Error> { - if size == 0 { - return Err(Error::InvalidSize("The size must not be zero".to_string())); - } - self.state = CodecState::BytesMut; - self.bin_remain = size; - self.buf = BytesMut::with_capacity(size); - Ok(()) - } - - - /// Expects a certain amount of bytes of data to arrive from the peer, and - /// that data should be stored to a file. - /// - /// # Decoder behavior - /// On successful completion the Decoder will return an - /// [`Input::File(pathname)`](Input::File) once the entire file length has - /// successfully been received, where the pathname is a PathBuf which - /// matches the pathname parameter passed to this function. - /// - /// Once the entire buffer has been received by the `Decoder` it will revert - /// to expect an [`Input::Telegram`]. - pub fn expect_file>( - &mut self, - pathname: P, - size: usize - ) -> Result<(), Error> { - if size == 0 { - return Err(Error::InvalidSize("The size must not be zero".to_string())); - } - self.state = CodecState::File; - let pathname = pathname.into(); - self.writer = Some(Box::new(File::create(&pathname)?)); - self.pathname = Some(pathname); - - self.bin_remain = size; - - Ok(()) - } - - /// Called from an application to request that data should be written to a - /// supplied writer. - /// - /// The writer's ownership will be transferred to the `Decoder` and will - /// automatically be dropped once the entire buffer has been written. - /// - /// # Decoder behavior - /// On successful completion the Decoder will return an Input::WriteDone to - /// signal that the entire buffer has been received and written to the - /// `Writer`. - /// - /// Once the entire buffer has been received by the `Decoder` it will revert - /// to expect an [`Input::Telegram`]. - pub fn expect_writer( - &mut self, - writer: W, - size: usize - ) -> Result<(), Error> { - if size == 0 { - return Err(Error::InvalidSize("The size must not be zero".to_string())); - } - self.state = CodecState::Writer; - self.writer = Some(Box::new(writer)); - self.bin_remain = size; - Ok(()) - } + + // unwrap() should be safe, unless running on a platform where + // size_of::() > size_of::() and the buffer is larger than + // usize::MAX. + self.remain = size.try_into().unwrap(); + Ok(()) + } + /// Tell the Decoder to expect lines of key/value pairs. /// /// # Decoder behavior /// On successful completion the the decoder will next return an @@ -532,19 +438,16 @@ /// /// # Decoder behavior /// On successful completion the decoder will have ignored the specified /// number of byes, reverts back to waiting for a [`Input::Telegram`] and /// returns [`Input::SkipDone`]. - /// - /// Once the entire buffer has been skipped by the `Decoder` it will revert - /// to expect an [`Input::Telegram`]. - pub fn skip(&mut self, size: usize) -> Result<(), Error> { + pub fn skip(&mut self, size: u64) -> Result<(), Error> { if size == 0 { return Err(Error::InvalidSize("The size must not be zero".to_string())); } self.state = CodecState::Skip; - self.bin_remain = size; + self.remain = size; Ok(()) } } fn utf8(buf: &[u8]) -> Result<&str, io::Error> { @@ -619,133 +522,55 @@ if buf.is_empty() { // Need more data return Ok(None); } - let read_to = cmp::min(self.bin_remain, buf.len()); - self.bin_remain -= read_to; + let read_to = cmp::min(self.remain, buf.len() as u64); + self.remain -= read_to; - if self.bin_remain == 0 { + if self.remain == 0 { // When no more data is expected for this binary part, revert to // expecting Telegram lines self.state = CodecState::Telegram; } // 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. - Ok(Some(Input::Chunk(buf.split_to(read_to), self.bin_remain))) + // + // The `as usize` cast is safe to do, because read_to is guaranteed to + // be within the bounds of an usize due to the `cmp::min()` above. + Ok(Some(Input::Chunk( + buf.split_to(read_to as usize).freeze(), + self.remain + ))) } CodecState::Bytes => { - if buf.is_empty() { - // Need more data - return Ok(None); - } - let read_to = cmp::min(self.bin_remain, buf.len()); - - // Transfer data from input to output buffer - self.buf.put(buf.split_to(read_to)); - - self.bin_remain -= read_to; - if self.bin_remain != 0 { - // Need more data - return Ok(None); - } - - // When no more data is expected for this binary part, revert to - // expecting Telegram lines - self.state = CodecState::Telegram; - - // 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. - let bytesmut = mem::take(&mut self.buf); - - Ok(Some(Input::Bytes(Bytes::from(bytesmut)))) - } - CodecState::BytesMut => { - if buf.is_empty() { - // Need more data - return Ok(None); - } - let read_to = cmp::min(self.bin_remain, buf.len()); - - // Transfer data from input to output buffer - self.buf.put(buf.split_to(read_to)); - - self.bin_remain -= read_to; - if self.bin_remain != 0 { - // Need more data - return Ok(None); - } - - // When no more data is expected for this binary part, revert to - // expecting Telegram lines - self.state = CodecState::Telegram; - - // 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. - Ok(Some(Input::BytesMut(mem::take(&mut self.buf)))) - } - CodecState::File | CodecState::Writer => { - if buf.is_empty() { - return Ok(None); // Need more data - } - - // Read as much data as available or requested and write it to our - // output. - let read_to = cmp::min(self.bin_remain, buf.len()); - if let Some(ref mut f) = self.writer { - f.write_all(&buf.split_to(read_to))?; - } - - self.bin_remain -= read_to; - if self.bin_remain != 0 { - return Ok(None); // Need more data - } - - // At this point the entire expected buffer has been received - - // Close file - self.writer = None; - - // 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. - let ret = if self.state == CodecState::File { - let pathname = if let Some(ref fname) = self.pathname { - fname.clone() - } else { - return Err(Error::BadState("Missing pathname".to_string())); - }; - - // Reset the pathname - self.pathname = None; - - Input::File(pathname) - } else { - Input::WriteDone - }; - - // Revert to the default of expecting a telegram. - self.state = CodecState::Telegram; - - Ok(Some(ret)) - } // CodecState::{File|Writer} + // This is guaranteed to work, because expect_bytes() takes in an + // usize. + let remain: usize = self.remain.try_into().unwrap(); + if buf.len() < remain { + Ok(None) + } else { + // Revert to expecting Telegram lines + self.state = CodecState::Telegram; + + Ok(Some(Input::Bytes(buf.split_to(remain).freeze()))) + } + } CodecState::Skip => { if buf.is_empty() { return Ok(None); // Need more data } // Read as much data as available or requested and write it to our // output. - let read_to = cmp::min(self.bin_remain, buf.len()); - let _ = buf.split_to(read_to); + let read_to = cmp::min(self.remain, buf.len() as u64); + let _ = buf.split_to(read_to as usize); - self.bin_remain -= read_to; - if self.bin_remain != 0 { + self.remain -= read_to; + if self.remain != 0 { return Ok(None); // Need more data } // Revert to the default of expecting a telegram. self.state = CodecState::Telegram; Index: src/err.rs ================================================================== --- src/err.rs +++ src/err.rs @@ -5,42 +5,47 @@ use tokio::io; /// Error that `blather` can emit. #[derive(Debug, PartialEq)] pub enum Error { - /// The requiested key was not found. - KeyNotFound(String), - /// The input format of a buffer was incorrect. BadFormat(String), - /// Unable to serialize a buffer. - SerializeError(String), - - /// A `std::io` or `tokio::io` error has occurred. - IO(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) + InvalidSize(String), + + /// A `std::io` or `tokio::io` error has occurred. + IO(String), + + /// The requiested key was not found. + KeyNotFound(String), + + /// Unable to serialize a buffer. + SerializeError(String), + + /// A "protcol error" implies that the Framed decoder detected an error + /// while parsing incoming data. + Protocol(String) } impl std::error::Error for Error {} impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match &*self { - Error::KeyNotFound(s) => write!(f, "Parameter '{}' not found", s), + match self { Error::BadFormat(s) => write!(f, "Bad format; {}", s), - Error::SerializeError(s) => write!(f, "Unable to serialize; {}", s), - Error::IO(s) => write!(f, "I/O error; {}", s), Error::BadState(s) => { write!(f, "Encountred an unexpected/bad state: {}", s) } - Error::InvalidSize(s) => write!(f, "Invalid size; {}", s) + Error::InvalidSize(s) => write!(f, "Invalid size; {}", s), + Error::IO(s) => write!(f, "I/O error; {}", s), + Error::KeyNotFound(s) => write!(f, "Parameter '{}' not found", s), + Error::Protocol(s) => write!(f, "Protocol error; {}", s), + Error::SerializeError(s) => write!(f, "Unable to serialize; {}", s) } } } impl From for Error { Index: src/lib.rs ================================================================== --- src/lib.rs +++ src/lib.rs @@ -46,16 +46,15 @@ //! implement their key/value paris using a `Params` buffer. //! //! # Communication //! blather handles transmission using tokio-util's //! [`Framed`](tokio_util::codec::Framed) framework, by -//! implementing its own [`Codec`](codec::Codec). It can be used to send and +//! 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)] -#![deny(rustdoc::missing_doc_code_examples)] pub mod codec; mod err; pub mod types; Index: src/types/kvlines.rs ================================================================== --- src/types/kvlines.rs +++ src/types/kvlines.rs @@ -1,8 +1,7 @@ //! A key/value pair list with stable ordering and non-unique keys. -use std::convert::From; use std::fmt; use bytes::{BufMut, BytesMut}; use crate::err::Error; Index: src/types/params.rs ================================================================== --- src/types/params.rs +++ src/types/params.rs @@ -1,14 +1,15 @@ //! 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}; -use std::convert::From; -use std::fmt; -use std::str::FromStr; +use std::{ + collections::{HashMap, HashSet}, + fmt, + str::FromStr +}; use bytes::{BufMut, BytesMut}; use super::validators::validate_param_key; @@ -41,10 +42,17 @@ /// Return the number of key/value pairs in the parameter buffer. pub fn len(&self) -> usize { self.hm.len() } + + /// Returns `true` if the `Params` collection does not contain any key/value + /// pairs. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Return reference to inner HashMap. pub fn get_inner(&self) -> &HashMap { &self.hm } @@ -56,15 +64,14 @@ /// allowing a polymorphic behavior. /// /// # Examples /// ``` /// use blather::Params; - /// fn main() { - /// let mut params = Params::new(); - /// params.add_param("integer", 42).unwrap(); - /// params.add_param("string", "hello").unwrap(); - /// } + /// + /// let mut params = Params::new(); + /// params.add_param("integer", 42).unwrap(); + /// params.add_param("string", "hello").unwrap(); /// ``` pub fn add_param( &mut self, key: T, value: U @@ -94,25 +101,24 @@ /// /// # Examples /// ``` /// use std::collections::HashSet; /// use blather::Params; - /// fn main() { - /// 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(); - /// } + /// + /// 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(); /// ``` pub fn add_strit(&mut self, key: &str, c: I) -> Result<(), Error> where I: IntoIterator, S: AsRef @@ -130,17 +136,16 @@ /// Add a boolean parameter. /// /// # Examples /// ``` /// use blather::Params; - /// fn main() { - /// let mut params = Params::new(); - /// params.add_bool("should_be_true", true).unwrap(); - /// params.add_bool("should_be_false", false).unwrap(); - /// assert_eq!(params.get_bool("should_be_true"), Ok(true)); - /// assert_eq!(params.get_bool("should_be_false"), Ok(false)); - /// } + /// + /// let mut params = Params::new(); + /// params.add_bool("should_be_true", true).unwrap(); + /// params.add_bool("should_be_false", false).unwrap(); + /// assert_eq!(params.get_bool("should_be_true"), Ok(true)); + /// assert_eq!(params.get_bool("should_be_false"), Ok(false)); /// ``` /// /// # Notes /// - Applications should not make assumptions about the specific string /// value added by this function. Do not treat boolean values as strings; @@ -169,18 +174,17 @@ /// found. /// /// # Examples /// ``` /// use blather::{Params, Error}; - /// fn main() { - /// let mut params = Params::new(); - /// params.add_param("arthur", 42); - /// let fourtytwo = params.get_param::("arthur").unwrap(); - /// assert_eq!(fourtytwo, 42); - /// let nonexist = params.get_param::("ford"); - /// assert_eq!(nonexist, Err(Error::KeyNotFound("ford".to_string()))); - /// } + /// + /// let mut params = Params::new(); + /// params.add_param("arthur", 42); + /// let fourtytwo = params.get_param::("arthur").unwrap(); + /// assert_eq!(fourtytwo, 42); + /// let nonexist = params.get_param::("ford"); + /// assert_eq!(nonexist, Err(Error::KeyNotFound("ford".to_string()))); /// ``` pub fn get_param(&self, key: &str) -> Result { if let Some(val) = self.get_str(key) { if let Ok(v) = T::from_str(val) { return Ok(v); @@ -198,15 +202,14 @@ /// value if key isn't found. /// /// # Examples /// ``` /// use blather::Params; - /// fn main() { - /// let mut params = Params::new(); - /// let val = params.get_param_def::("nonexist", 11); - /// assert_eq!(val, Ok(11)); - /// } + /// + /// let mut params = Params::new(); + /// let val = params.get_param_def::("nonexist", 11); + /// assert_eq!(val, Ok(11)); /// ``` pub fn get_param_def( &self, key: &str, def: T @@ -240,15 +243,14 @@ /// default value if key does not exist in parameter buffer. /// /// # Examples /// ``` /// use blather::Params; - /// fn main() { - /// let params = Params::new(); - /// let e = params.get_str_def("nonexist", "elena"); - /// assert_eq!(e, "elena"); - /// } + /// + /// 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. pub fn get_str_def<'a>(&'a self, key: &str, def: &'a str) -> &'a str { let kv = self.hm.get_key_value(key); @@ -263,15 +265,14 @@ /// Get a parameter and convert it to an integer type. /// /// # Examples /// ``` /// use blather::Params; - /// fn main() { - /// let mut params = Params::new(); - /// params.add_param("Num", 7); - /// assert_eq!(params.get_int::("Num").unwrap(), 7); - /// } + /// + /// 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 @@ -296,16 +297,15 @@ /// does not exist then return a default value supplied by the caller. /// /// # Examples /// ``` /// use blather::Params; - /// fn main() { - /// 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); - /// } + /// + /// 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. @@ -363,24 +363,23 @@ /// it. Only non-empty entries are returned. /// /// # Examples /// ``` /// use blather::Params; - /// fn main() { - /// 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"]); - /// } + /// + /// 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"]); /// ``` 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.len() != 0 { + if !s.is_empty() { ret.push(s.to_string()); } } } @@ -392,27 +391,26 @@ /// return them in a HashSet. Only non-empty entries are returned. /// /// # Examples /// ``` /// use blather::Params; - /// fn main() { - /// 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); - /// } + /// + /// 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); /// ``` 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.len() != 0 { + if !s.is_empty() { ret.insert(s.to_string()); } } } Index: src/types/telegram.rs ================================================================== --- src/types/telegram.rs +++ src/types/telegram.rs @@ -1,19 +1,20 @@ //! 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}; -use std::fmt; -use std::str::FromStr; +use std::{ + collections::{HashMap, HashSet}, + fmt, + str::FromStr +}; use bytes::{BufMut, BytesMut}; use crate::err::Error; -use super::params::Params; -use super::validators::validate_topic; +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`] @@ -40,14 +41,13 @@ /// Create a new telegram object with a topic. /// /// ``` /// use blather::Telegram; - /// fn main() { - /// let mut tg = Telegram::new_topic("Hello").unwrap(); - /// assert_eq!(tg.get_topic(), Some("Hello")); - /// } + /// + /// let mut tg = Telegram::new_topic("Hello").unwrap(); + /// assert_eq!(tg.get_topic(), Some("Hello")); /// ``` pub fn new_topic(topic: &str) -> Result { validate_topic(topic)?; Ok(Telegram { topic: Some(topic.to_string()), @@ -58,17 +58,16 @@ /// Clear topic and internal parameters buffer. /// /// ``` /// use blather::Telegram; - /// fn main() { - /// let mut tg = Telegram::new(); - /// tg.add_param("cat", "meow"); - /// assert_eq!(tg.num_params(), 1); - /// tg.clear(); - /// assert_eq!(tg.num_params(), 0); - /// } + /// + /// let mut tg = Telegram::new(); + /// tg.add_param("cat", "meow"); + /// assert_eq!(tg.num_params(), 1); + /// tg.clear(); + /// assert_eq!(tg.num_params(), 0); /// ``` pub fn clear(&mut self) { self.topic = None; self.params.clear(); } @@ -77,16 +76,15 @@ /// Return the number of key/value parameters in the Telegram object. /// /// # Examples /// ``` /// use blather::Telegram; - /// fn main() { - /// let mut tg = Telegram::new(); - /// assert_eq!(tg.num_params(), 0); - /// tg.add_param("cat", "meow"); - /// assert_eq!(tg.num_params(), 1); - /// } + /// + /// let mut tg = Telegram::new(); + /// 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). pub fn num_params(&self) -> usize { @@ -98,21 +96,20 @@ pub fn get_params(&self) -> &Params { &self.params } - /// Get a mutable reference to the inner [`Params`](crate::Params) object. + /// Get a mutable reference to the inner [`Params`] object. /// /// ``` /// use blather::Telegram; - /// fn main() { - /// let mut tg = Telegram::new(); - /// tg.add_param("cat", "meow"); - /// assert_eq!(tg.num_params(), 1); - /// tg.get_params_mut().clear(); - /// assert_eq!(tg.num_params(), 0); - /// } + /// + /// let mut tg = Telegram::new(); + /// 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 } @@ -120,11 +117,11 @@ /// Get a reference the the parameter's internal HashMap. /// /// Note: The inner representation of the Params object may change in the /// future. pub fn get_params_inner(&self) -> &HashMap { - &self.params.get_inner() + self.params.get_inner() } /// Set topic for telegram. /// @@ -131,17 +128,16 @@ /// Overwrites current topic is one has already been set. /// /// # Examples /// ``` /// use blather::{Telegram, Error}; - /// fn main() { - /// let mut tg = Telegram::new(); - /// assert_eq!(tg.set_topic("Hello"), Ok(())); + /// + /// let mut tg = Telegram::new(); + /// assert_eq!(tg.set_topic("Hello"), Ok(())); /// - /// let e = Error::BadFormat("Invalid topic character".to_string()); - /// assert_eq!(tg.set_topic("Hell o"), Err(e)); - /// } + /// let e = Error::BadFormat("Invalid topic character".to_string()); + /// assert_eq!(tg.set_topic("Hell o"), Err(e)); /// ``` pub fn set_topic(&mut self, topic: &str) -> Result<(), Error> { validate_topic(topic)?; self.topic = Some(topic.to_string()); Ok(()) @@ -151,17 +147,16 @@ /// Get a reference to the topic string, or None if topic is not been set. /// /// # Examples /// ``` /// use blather::{Telegram, Error}; - /// fn main() { - /// let tg = Telegram::new_topic("shoe0nhead").unwrap(); - /// assert_eq!(tg.get_topic(), Some("shoe0nhead")); + /// + /// let tg = Telegram::new_topic("shoe0nhead").unwrap(); + /// assert_eq!(tg.get_topic(), Some("shoe0nhead")); /// - /// let tg = Telegram::new(); - /// assert_eq!(tg.get_topic(), None); - /// } + /// let tg = Telegram::new(); + /// assert_eq!(tg.get_topic(), None); /// ``` pub fn get_topic(&self) -> Option<&str> { if let Some(t) = &self.topic { Some(t) } else { @@ -176,15 +171,14 @@ /// allowing a polymorphic behavior. /// /// # Examples /// ``` /// use blather::Telegram; - /// fn main() { - /// let mut tg = Telegram::new(); - /// tg.add_param("integer", 42).unwrap(); - /// tg.add_param("string", "hello").unwrap(); - /// } + /// + /// let mut tg = Telegram::new(); + /// 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). @@ -211,15 +205,14 @@ /// Add parameter where the value is generated from an iterator over a /// string container, where entries will be comma-separated. /// /// ``` /// use blather::Telegram; - /// fn main() { - /// let mut tg = Telegram::new(); - /// tg.add_strit("Cat", &["meow", "paws", "tail"]).unwrap(); - /// assert_eq!(tg.get_str("Cat"), Some("meow,paws,tail")); - /// } + /// + /// let mut tg = Telegram::new(); + /// tg.add_strit("Cat", &["meow", "paws", "tail"]).unwrap(); + /// assert_eq!(tg.get_str("Cat"), Some("meow,paws,tail")); /// ``` /// /// # Notes /// - This is a thin wrapper for /// [`Params::add_strit()`](crate::Params::add_strit). @@ -303,15 +296,14 @@ /// Get an integer representation of a parameter. /// /// ``` /// use blather::Telegram; - /// fn main() { - /// let mut tg = Telegram::new(); - /// tg.add_param("Num", 7); - /// assert_eq!(tg.get_int::("Num").unwrap(), 7); - /// } + /// + /// let mut tg = Telegram::new(); + /// 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. @@ -326,16 +318,15 @@ /// If the key does not exist then return a default value supplied by the /// caller. /// /// ``` /// use blather::Telegram; - /// fn main() { - /// let mut tg = Telegram::new(); - /// 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); - /// } + /// + /// let mut tg = Telegram::new(); + /// 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); /// ``` pub fn get_int_def( &self, key: &str, def: T @@ -507,13 +498,13 @@ impl fmt::Display for Telegram { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let topic: &str = match &self.topic { Some(s) => s.as_ref(), - None => &"" + None => "" }; write!(f, "{}:{}", topic, self.params) } } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: tests/conn_telegram.rs ================================================================== --- tests/conn_telegram.rs +++ tests/conn_telegram.rs @@ -65,12 +65,12 @@ let mut frm = Framed::new(mock.build(), Codec::new()); if let Some(e) = frm.next().await { if let Err(e) = e { match e { - Error::BadFormat(s) => { - assert_eq!(s, "Invalid topic character"); + Error::Protocol(s) => { + assert_eq!(s, "Bad format; Invalid topic character"); } _ => { panic!("Wrong error"); } } Index: tests/params.rs ================================================================== --- tests/params.rs +++ tests/params.rs @@ -15,13 +15,13 @@ #[test] fn exists() { let mut params = Params::new(); params.add_str("foo", "bar").unwrap(); - assert_eq!(params.have("foo"), true); + assert!(params.have("foo")); - assert_eq!(params.have("nonexistent"), false); + assert!(!params.have("nonexistent")); } #[test] fn integer() { @@ -34,11 +34,11 @@ #[test] fn size() { let mut msg = Params::new(); - msg.add_param("Num", 7 as usize).unwrap(); + msg.add_param("Num", 7_usize).unwrap(); assert_eq!(msg.get_int::("Num").unwrap(), 7); } #[test] Index: tests/params_strvec.rs ================================================================== --- tests/params_strvec.rs +++ tests/params_strvec.rs @@ -37,12 +37,11 @@ #[test] fn strvec_single_add() { let mut params = Params::new(); - let mut sv = Vec::new(); - sv.push("foo"); + 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")); @@ -56,13 +55,11 @@ #[test] fn strvec_two_add() { let mut params = Params::new(); - let mut sv = Vec::new(); - sv.push("foo"); - sv.push("bar"); + 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(); Index: tests/telegram.rs ================================================================== --- tests/telegram.rs +++ tests/telegram.rs @@ -17,13 +17,13 @@ #[test] fn exist() { let mut tg = Telegram::new(); tg.add_str("foo", "bar").unwrap(); - assert_eq!(tg.have_param("foo"), true); + assert!(tg.have_param("foo")); - assert_eq!(tg.have_param("nonexistent"), false); + assert!(!tg.have_param("nonexistent")); } #[test] fn integer() { @@ -39,11 +39,11 @@ #[test] fn size() { let mut msg = Telegram::new(); - msg.add_param("Num", 7 as usize).unwrap(); + msg.add_param("Num", 7_usize).unwrap(); assert_eq!(msg.get_int::("Num").unwrap(), 7); } #[test] Index: tests/tg_to_expect.rs ================================================================== --- tests/tg_to_expect.rs +++ tests/tg_to_expect.rs @@ -12,25 +12,26 @@ mock.read(b"hello\nlen 4\n\n1234"); let mut frm = Framed::new(mock.build(), Codec::new()); - while let Some(o) = frm.next().await { - let o = o.unwrap(); - if let codec::Input::Telegram(tg) = o { - assert_eq!(tg.get_topic(), Some("hello")); - assert_eq!(tg.get_int::("len").unwrap(), 4); - frm.codec_mut().expect_bytesmut(4).unwrap(); - break; - } else { - panic!("Not a Telegram"); - } + 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(), Some("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::BytesMut(_bm) = o { + if let codec::Input::Bytes(_bm) = o { } else { panic!("Not a Buf"); } } } ADDED www/changelog.md Index: www/changelog.md ================================================================== --- /dev/null +++ www/changelog.md @@ -0,0 +1,43 @@ +# Change Log + +⚠️ indicates a breaking change. + +## [Unreleased] + +[Details](/vdiff?from=blather-0.10.0&to=trunk) + +### Added + +### Changed + +### Removed + +--- + +## [0.10.0] - 2024-02-23 + +[Details](/vdiff?from=blather-0.9.0&to=blather-0.10.0) + +### Changed + +- Dependency maintenance. +- ⚠️ Changed `expect_chunks()` to accept length as `u64` rather than `usize` to + support blobs on 32-bit platforms. +- ⚠️ Make `skip()` take in `u64` rather than `usize` to be able to skip very + large blobs. +- ⚠️ Use `Bytes` rather than `BytesMut` for chunk buffer. +- ⚠️ The decoder will wrap any errors relating to parsing incoming data in + `Error::Protocol()` to signal that the incoming protocol fomrat was invalid. +- ⚠️ Make `expect_chunks()` fallible and check for zero-length. + +### Removed + +- ⚠️ Removed three codec states: + - `BytesMut` has turned out not to be particularly useful. + - `File` was always a bad idea. + - `Writer` can have valid use-cases, but risk being misused as a substitute + for `File`. + +Use `Bytes` instead of `BytesMut`. Use `Chunks` instead of `File` or +`Writer`, and let the application write the chunks as appropriate. + ADDED www/index.md Index: www/index.md ================================================================== --- /dev/null +++ www/index.md @@ -0,0 +1,12 @@ +# blather + +A talkative, somwhat reminiscent to HTTP, line-based protocol, implemented as +a tokio-util Codec. + + +## 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 +maintained [Change Log](./changelog.md). +