diff --git a/examples/version.rs b/examples/version.rs index 62943a2..857aaab 100644 --- a/examples/version.rs +++ b/examples/version.rs @@ -1,14 +1,10 @@ -use arctictis::{ - Command, Scanner, - bc125at::{GetFirmwareVersion, GetModelInfo}, -}; +use arctictis::{Command, Scanner, bc125at::GetFirmwareVersion}; #[tokio::main] async fn main() -> Result<(), Box> { let mut scanner = Scanner::open()?; println!("{scanner:#?}"); print_response(&mut scanner, GetFirmwareVersion).await?; - print_response(&mut scanner, GetModelInfo).await?; Ok(()) } diff --git a/examples/volume.rs b/examples/volume.rs deleted file mode 100644 index e353f45..0000000 --- a/examples/volume.rs +++ /dev/null @@ -1,36 +0,0 @@ -use arctictis::{ - Command, Scanner, - bc125at::{GetVolumeLevel, SetVolumeLevel, VolumeLevel}, -}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let volume = std::env::args() - .nth(1) - .expect("usage: `volume `") - .parse() - .expect("volume should be a number between 0-15"); - let volume = VolumeLevel::new(volume).expect("invalid volume"); - - let mut scanner = Scanner::open()?; - println!("{scanner:#?}"); - print_response(&mut scanner, GetVolumeLevel).await?; - print_response(&mut scanner, SetVolumeLevel(volume)).await?; - print_response(&mut scanner, GetVolumeLevel).await?; - - Ok(()) -} - -async fn print_response( - scanner: &mut Scanner, - cmd: Cmd, -) -> Result<(), Box> -where - Cmd: Command + 'static, - Cmd::Response: std::fmt::Debug, -{ - let name = String::from_utf8_lossy(Cmd::TEXT); - let r = scanner.command(cmd).await?; - println!("{name} => {r:?}"); - Ok(()) -} diff --git a/src/codec.rs b/src/codec.rs index bea952f..8e5bd69 100644 --- a/src/codec.rs +++ b/src/codec.rs @@ -5,7 +5,7 @@ use tokio_util::{ use crate::{ bytes_split::BytesSplit, - command::{Command, ParamBuffer, Params, Response}, + command::{Command, Params, Response}, }; pub(crate) const RETURN_CODE: u8 = b'\r'; @@ -32,11 +32,14 @@ where fn encode(&mut self, item: Cmd, dst: &mut BytesMut) -> Result<(), Self::Error> { let params = item.params(); - let est_len = Cmd::TEXT.len() + params.count() + params.max_size() + 1; + let est_len = Cmd::TEXT.len() + params.size_hint() + 1; dst.reserve(est_len); dst.extend_from_slice(Cmd::TEXT); - params.serialize_to(ParamBuffer::new(dst)); + for param in params { + dst.put_u8(PARAM_DELIMITER); + dst.extend_from_slice(param); + } dst.put_u8(RETURN_CODE); @@ -73,7 +76,7 @@ impl RawResponse { return Err(ResponseError::WrongNumberOfFields); } - let response = Cmd::Response::deserialize(&self.raw_values)?; + let response = Cmd::Response::deserialize(self.raw_values.iter())?; Ok(response) } @@ -127,197 +130,3 @@ impl Decoder for Codec { Ok(Some(RawResponse { cmd, raw_values })) } } - -#[cfg(test)] -mod tests { - use claims::{assert_matches, assert_none, assert_ok}; - use tokio_util::{ - bytes::{Bytes, BytesMut}, - codec::{AnyDelimiterCodec, AnyDelimiterCodecError, Decoder, Encoder}, - }; - - use crate::{ - Command, DecoderError, OkResponseError, ResponseError, - codec::{Codec, RETURN_CODE, RawResponse}, - command::{OkResponse, command, range_param, range_response}, - }; - - range_param!(RangeParam(0..=15): u8); - range_response!(RangeParam => ParamError::Invalid("invalid")); - - command!(b"CMD": SimpleCommand); - command!(b"CMD": ResponseCommand => RangeParam); - command!(b"CMD": ParamCommand(RangeParam) => OkResponse); - - mod encode { - use super::*; - - #[test] - fn simple_command() { - let buf = encode(SimpleCommand); - - assert_eq!(buf.as_ref(), b"CMD\r"); - } - - #[test] - fn single_param() { - let buf = encode(ParamCommand(RangeParam::new(0).unwrap())); - - assert_eq!(buf.as_ref(), b"CMD,0\r"); - } - - fn encode(cmd: C) -> BytesMut { - let mut buf = BytesMut::new(); - Codec::new().encode(cmd, &mut buf).unwrap(); - buf - } - } - - mod decode { - use super::*; - - #[test] - fn ok_response() { - let item = decode(b"CMD,OK\r").unwrap().unwrap(); - - assert_eq!(item.cmd.as_ref(), b"CMD"); - assert_eq!(item.raw_values, [Bytes::from(b"OK".as_slice())]); - } - - #[test] - fn multi_param_response() { - let item = decode(b"CMD,1,2,3\r").unwrap().unwrap(); - - assert_eq!(item.cmd.as_ref(), b"CMD"); - assert_eq!( - item.raw_values, - [ - Bytes::from(b"1".as_slice()), - Bytes::from(b"2".as_slice()), - Bytes::from(b"3".as_slice()) - ], - ); - } - - #[test] - fn incomplete_response_returns_none() { - let (response, result) = decode_buf(b"CMD,"); - - assert_none!(assert_ok!(result)); - // buffer should not be changed - assert_eq!(response.as_ref(), b"CMD,"); - } - - #[test] - fn clears_buffer_on_complete_response() { - let (response, result) = decode_buf(b"CMD,OK\r"); - - assert_eq!(result.unwrap().unwrap().cmd.as_ref(), b"CMD"); - // buffer should be cleared - assert!(response.is_empty()); - } - - #[test] - fn partially_consumes_buffer_on_complete_response() { - let (response, result) = decode_buf(b"CMD,OK\rCMD2"); - - assert_eq!(result.unwrap().unwrap().cmd.as_ref(), b"CMD"); - // buffer should have single complete command removed - assert_eq!(response.as_ref(), b"CMD2"); - } - - #[test] - fn malformed_response() { - let err = decode(b"\r").unwrap_err(); - - assert_matches!(err, DecoderError::Malformed); - } - - #[test] - fn not_acceptable_response() { - let err = decode(b"CMD,NG\r").unwrap_err(); - - assert_matches!(err, DecoderError::NotAcceptable); - } - - #[test] - fn error_response() { - let err = decode(b"CMD,ERR\r").unwrap_err(); - - assert_matches!(err, DecoderError::ErrorResponse); - } - - #[test] - fn delimiter_error() { - let mut codec = Codec::new(); - // reduce the max length of the internal delimiter decoder - codec.decoder = - AnyDelimiterCodec::new_with_max_length(vec![RETURN_CODE], vec![RETURN_CODE], 1); - let mut response = BytesMut::from(b"CMD,OK\r".as_slice()); - - let err = codec.decode(&mut response).unwrap_err(); - assert_matches!( - err, - DecoderError::DelimiterError(AnyDelimiterCodecError::MaxChunkLengthExceeded) - ); - } - - fn decode(raw_response: &[u8]) -> Result, DecoderError> { - decode_buf(raw_response).1 - } - - fn decode_buf( - raw_response: &[u8], - ) -> (BytesMut, Result, DecoderError>) { - let mut raw_response = BytesMut::from(raw_response); - let result = Codec::new().decode(&mut raw_response); - - (raw_response, result) - } - } - - mod deserialize { - use super::*; - - #[test] - fn ok_response() { - decode(b"CMD,OK\r").deserialize::().unwrap(); - // nothing to assert - } - - #[test] - fn single_param_response() { - let response = decode(b"CMD,1\r").deserialize::().unwrap(); - assert_eq!(response.value(), 1); - } - - #[test] - fn wrong_command() { - let result = decode(b"FOO,OK\r") - .deserialize::() - .unwrap_err(); - assert_matches!(result, ResponseError::WrongCommand); - } - - #[test] - fn wrong_number_of_fields() { - let result = decode(b"CMD,OK,OK\r") - .deserialize::() - .unwrap_err(); - assert_matches!(result, ResponseError::WrongNumberOfFields); - } - - #[test] - fn invalid_fields() { - let result = decode(b"CMD,FOO\r") - .deserialize::() - .unwrap_err(); - assert_matches!(result, ResponseError::InvalidFields(OkResponseError)); - } - - fn decode(raw: &[u8]) -> RawResponse { - let mut raw = BytesMut::from(raw); - Codec::new().decode(&mut raw).unwrap().unwrap() - } - } -} diff --git a/src/command/bc125at/backlight.rs b/src/command/bc125at/backlight.rs deleted file mode 100644 index 51f41fa..0000000 --- a/src/command/bc125at/backlight.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::{ - OkResponse, - command::{command, macros::enum_param_response}, -}; - -enum_param_response!(Backlight { - AlwaysOn => b"AO", - AlwaysOff => b"AF", - Keypress => b"KY", - Squelch => b"SQ", - KeypressOrSquelch => b"KS", -}: BacklightError("invalid backlight setting")); - -command!(b"BLT": GetBacklight => Backlight); -command!(b"BLT": SetBacklight(Backlight) => OkResponse); diff --git a/src/command/bc125at/battery_charge_time.rs b/src/command/bc125at/battery_charge_time.rs deleted file mode 100644 index ada0d07..0000000 --- a/src/command/bc125at/battery_charge_time.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::command::{OkResponse, command, range_param, range_response}; - -range_param!(BatteryChargeTime(1..=16): u8); -range_response!(BatteryChargeTime => BatteryChargeTimeError::InvalidBatteryChargeTime("volume must be between [1..16], got `{0}`")); - -command!(b"BSV": GetBatteryInfo => BatteryChargeTime); -command!(b"BSV": SetBatteryInfo(BatteryChargeTime) => OkResponse); diff --git a/src/command/bc125at/firmware_version.rs b/src/command/bc125at/firmware_version.rs index a4f27d9..408565c 100644 --- a/src/command/bc125at/firmware_version.rs +++ b/src/command/bc125at/firmware_version.rs @@ -1,5 +1,52 @@ -use crate::command::{command, macros::string_response}; +use std::string::FromUtf8Error; -string_response!(FirmwareVersion => FirmwareVersionError); +use crate::{ + Command, + command::{NoParams, Response}, +}; -command!(b"VER": GetFirmwareVersion => FirmwareVersion); +#[derive(Debug, thiserror::Error)] +pub enum FirmwareVersionError { + #[error(transparent)] + Utf8(#[from] FromUtf8Error), + #[error("expected one field")] + Malformed, +} + +#[derive(Debug)] +pub struct FirmwareVersion(pub String); + +impl Response for FirmwareVersion { + type Error = FirmwareVersionError; + + fn deserialize<'i, I: Iterator>( + mut raw_values: I, + ) -> Result { + if let Some(val) = raw_values.next() + && raw_values.next().is_none() + { + let utf8 = String::from_utf8(val.to_vec())?; + Ok(Self(utf8)) + } else { + Err(FirmwareVersionError::Malformed) + } + } + + fn expected_field_count() -> usize { + 1 + } +} + +pub struct GetFirmwareVersion; + +impl Command for GetFirmwareVersion { + const TEXT: &'static [u8] = b"VER"; + + type Params = NoParams; + + type Response = FirmwareVersion; + + fn params(self) -> Self::Params { + NoParams + } +} diff --git a/src/command/bc125at/lcd_contrast.rs b/src/command/bc125at/lcd_contrast.rs deleted file mode 100644 index a1044de..0000000 --- a/src/command/bc125at/lcd_contrast.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::command::{OkResponse, command, range_param, range_response}; - -range_param!(LcdContrast(0..=15): u8); -range_response!(LcdContrast => LcdContrastError::InvalidLcdContrast("LCD contrast must be between [1..15], got `{0}`")); - -command!(b"CNT": GetLcdContrast => LcdContrast); -command!(b"CNT": SetLcdContrast(LcdContrast) => OkResponse); diff --git a/src/command/bc125at/mod.rs b/src/command/bc125at/mod.rs index 087a9be..c07c05c 100644 --- a/src/command/bc125at/mod.rs +++ b/src/command/bc125at/mod.rs @@ -1,19 +1,3 @@ -mod backlight; -mod battery_charge_time; mod firmware_version; -mod lcd_contrast; -mod model; -mod program_mode; -mod squelch; -mod volume; -pub use backlight::{Backlight, BacklightError, GetBacklight, SetBacklight}; -pub use battery_charge_time::{ - BatteryChargeTime, BatteryChargeTimeError, GetBatteryInfo, SetBatteryInfo, -}; pub use firmware_version::{FirmwareVersion, FirmwareVersionError, GetFirmwareVersion}; -pub use lcd_contrast::{GetLcdContrast, LcdContrast, LcdContrastError, SetLcdContrast}; -pub use model::{GetModelInfo, ModelInfo, ModelInfoError}; -pub use program_mode::{EnterProgramMode, ExitProgramMode}; -pub use squelch::{GetSquelchLevel, SetSquelchLevel, SquelchLevel, SquelchLevelError}; -pub use volume::{GetVolumeLevel, SetVolumeLevel, VolumeLevel, VolumeLevelError}; diff --git a/src/command/bc125at/model.rs b/src/command/bc125at/model.rs deleted file mode 100644 index af30ffc..0000000 --- a/src/command/bc125at/model.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::command::{command, macros::string_response}; - -string_response!(ModelInfo => ModelInfoError); - -command!(b"MDL": GetModelInfo => ModelInfo); diff --git a/src/command/bc125at/program_mode.rs b/src/command/bc125at/program_mode.rs deleted file mode 100644 index 61d89eb..0000000 --- a/src/command/bc125at/program_mode.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::command::command; - -command!(b"PRG": EnterProgramMode); -command!(b"EPG": ExitProgramMode); diff --git a/src/command/bc125at/squelch.rs b/src/command/bc125at/squelch.rs deleted file mode 100644 index b77dd99..0000000 --- a/src/command/bc125at/squelch.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::command::{OkResponse, command, range_param, range_response}; - -range_param!(SquelchLevel(0..=15): u8); -range_response!(SquelchLevel => SquelchLevelError::InvalidSquelch("squelch must be between [0..15], got `{0}`")); - -command!(b"SQL": GetSquelchLevel => SquelchLevel); -command!(b"SQL": SetSquelchLevel(SquelchLevel) => OkResponse); diff --git a/src/command/bc125at/volume.rs b/src/command/bc125at/volume.rs deleted file mode 100644 index f414a21..0000000 --- a/src/command/bc125at/volume.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::command::{OkResponse, command, range_param, range_response}; - -range_param!(VolumeLevel(0..=15): u8); -range_response!(VolumeLevel => VolumeLevelError::InvalidVolume("volume must be between [0..15], got `{0}`")); - -command!(b"VOL": GetVolumeLevel => VolumeLevel); -command!(b"VOL": SetVolumeLevel(VolumeLevel) => OkResponse); diff --git a/src/command/macros/command.rs b/src/command/macros/command.rs deleted file mode 100644 index 9fc27f8..0000000 --- a/src/command/macros/command.rs +++ /dev/null @@ -1,85 +0,0 @@ -macro_rules! command { - ($text:literal: $name:ident) => { - pub struct $name; - - impl crate::command::Command for $name { - const TEXT: &'static [u8] = $text; - type Params = crate::command::NoParams; - type Response = crate::command::OkResponse; - - command!(@no_params_fn); - } - }; - ($text:literal: $name:ident => $response:ty) => { - pub struct $name; - - impl crate::command::Command for $name { - const TEXT: &'static [u8] = $text; - type Params = crate::command::NoParams; - type Response = $response; - - command!(@no_params_fn); - } - }; - ($text:literal: $name:ident($param_set:ty) => $response:ty) => { - pub struct $name(pub $param_set); - - impl crate::command::Command for $name { - const TEXT: &'static [u8] = $text; - type Params = $param_set; - type Response = $response; - - fn params(&self) -> &Self::Params { - &self.0 - } - } - }; - (@no_params_fn) => { - fn params(&self) -> &crate::command::NoParams { - &crate::command::NoParams - } - }; -} - -pub(crate) use command; - -#[cfg(test)] -mod tests { - - use crate::{ - Command, OkResponse, - command::{NoParams, range_param, range_response}, - }; - - range_param!(RangeParam(0..=15): u8); - range_response!(RangeParam => ParamError::Invalid("invalid")); - - #[test] - fn no_params_ok_response() { - command!(b"CMD": SimpleCommand); - assert_generated_command(&SimpleCommand, b"CMD", &NoParams); - } - - #[test] - fn no_params_single_value_response() { - command!(b"CMD": ResponseCommand => RangeParam); - - assert_generated_command(&ResponseCommand, b"CMD", &NoParams); - } - - #[test] - fn range_param_ok_response() { - command!(b"CMD": ParamCommand(RangeParam) => OkResponse); - - let param = RangeParam(0); - assert_generated_command(&ParamCommand(param), b"CMD", ¶m); - } - - fn assert_generated_command(command: &C, text: &[u8], params: &C::Params) - where - C::Params: std::fmt::Debug + PartialEq, - { - assert_eq!(C::TEXT, text); - assert_eq!(command.params(), params); - } -} diff --git a/src/command/macros/enum_param_response.rs b/src/command/macros/enum_param_response.rs deleted file mode 100644 index cf2be6d..0000000 --- a/src/command/macros/enum_param_response.rs +++ /dev/null @@ -1,61 +0,0 @@ -macro_rules! enum_param_response { - ($name:ident { $($variant:ident => $val:literal),+ $(,)? } : $error:ident($msg:literal)) => { - #[derive( - ::core::clone::Clone, - ::core::marker::Copy, - ::core::fmt::Debug, - ::core::cmp::PartialEq, - ::core::cmp::Eq, - )] - pub enum $name { - $($variant),+ - } - - impl crate::command::Params for $name { - fn count(&self) -> usize { - 1 - } - - fn max_size(&self) -> usize { - 2 - } - - fn serialize_to(&self, mut buffer: crate::command::ParamBuffer) { - let txt = match self { - $(Self::$variant => $val,)+ - }; - buffer.serialize_param(txt); - } - } - - #[derive( - ::core::clone::Clone, - ::core::marker::Copy, - ::core::fmt::Debug, - ::core::cmp::PartialEq, - ::core::cmp::Eq, - ::thiserror::Error, - )] - #[error($msg)] - pub struct $error; - - impl crate::command::Response for $name { - type Error = $error; - - fn deserialize(raw_values: &[::tokio_util::bytes::Bytes]) -> ::core::result::Result { - let val = match raw_values[0].as_ref() { - $($val => Self::$variant,)+ - _ => return ::core::result::Result::Err($error), - }; - Ok(val) - } - - - fn expected_field_count() -> usize { - 1 - } - } - }; -} - -pub(crate) use enum_param_response; diff --git a/src/command/macros/mod.rs b/src/command/macros/mod.rs deleted file mode 100644 index 045411d..0000000 --- a/src/command/macros/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod command; -mod enum_param_response; -mod range_param; -mod range_response; -mod string_response; - -pub(crate) use command::command; -pub(crate) use enum_param_response::enum_param_response; -pub(crate) use range_param::range_param; -pub(crate) use range_response::range_response; -pub(crate) use string_response::string_response; diff --git a/src/command/macros/range_param.rs b/src/command/macros/range_param.rs deleted file mode 100644 index 3b7888a..0000000 --- a/src/command/macros/range_param.rs +++ /dev/null @@ -1,82 +0,0 @@ -macro_rules! range_param { - ($name:ident($range:expr): $type:ty) => { - #[derive( - ::core::clone::Clone, - ::core::marker::Copy, - ::core::fmt::Debug, - ::core::cmp::PartialEq, - ::core::cmp::Eq, - )] - pub struct $name($type); - - impl $name { - pub fn new(value: $type) -> ::core::option::Option { - ($range).contains(&value).then_some(Self(value)) - } - - #[allow(unused)] - pub fn value(&self) -> $type { - self.0 - } - } - - impl crate::command::Params for $name { - fn count(&self) -> usize { - 1 - } - fn max_size(&self) -> usize { - <$type as ::itoa::Integer>::MAX_STR_LEN - } - fn serialize_to(&self, mut buffer: crate::command::ParamBuffer) { - let mut serialized = ::itoa::Buffer::new(); - buffer.serialize_param(serialized.format(self.0).as_bytes()); - } - } - }; -} - -pub(crate) use range_param; - -#[cfg(test)] -mod tests { - use claims::{assert_none, assert_some_eq}; - use tokio_util::bytes::BytesMut; - - use crate::command::{ParamBuffer, Params}; - - range_param!(U8RangeParam(0..=10): u8); - - #[test] - fn produces_value() { - for i in 0..=10 { - assert_some_eq!(U8RangeParam::new(i), U8RangeParam(i)); - } - } - - #[test] - fn produces_none_for_invalid_value() { - assert_none!(U8RangeParam::new(20)); - } - - #[test] - fn returns_value() { - assert_eq!(U8RangeParam(10).value(), 10); - } - - #[test] - fn generated_param_impl() { - assert_eq!(U8RangeParam(0).count(), 1); - assert_eq!(U8RangeParam(0).max_size(), 3); - - for i in 0..=10 { - let param = U8RangeParam(i); - - let mut bytes = BytesMut::new(); - let buf = ParamBuffer::new(&mut bytes); - - param.serialize_to(buf); - - assert_eq!(bytes, format!(",{i}").as_bytes()); - } - } -} diff --git a/src/command/macros/range_response.rs b/src/command/macros/range_response.rs deleted file mode 100644 index dc794fb..0000000 --- a/src/command/macros/range_response.rs +++ /dev/null @@ -1,68 +0,0 @@ -macro_rules! range_response { - ($name:ty => $error:ident::$invalid_variant:ident($msg:literal)) => { - #[derive(::core::fmt::Debug, ::thiserror::Error)] - pub enum $error { - #[error(transparent)] - Utf8Error(#[from] ::core::str::Utf8Error), - #[error(transparent)] - Parse(#[from] ::core::num::ParseIntError), - #[error($msg)] - $invalid_variant(u8), - } - - impl crate::command::Response for $name { - type Error = $error; - - fn deserialize( - raw_values: &[::tokio_util::bytes::Bytes], - ) -> ::core::result::Result { - let level = ::core::str::from_utf8(&raw_values[0])?.parse()?; - Self::new(level).ok_or(Self::Error::$invalid_variant(level)) - } - fn expected_field_count() -> usize { - 1 - } - } - }; -} - -pub(crate) use range_response; - -#[cfg(test)] -mod tests { - use claims::assert_matches; - - use crate::command::{Response, range_param, test::deserialize}; - - range_param!(U8RangeParam(0..=10): u8); - range_response!(U8RangeParam => ParamError::Invalid("invalid")); - - #[test] - fn expected_field_count() { - assert_eq!(U8RangeParam::expected_field_count(), 1); - } - - #[test] - fn deserializes_valid_response() { - let response = deserialize::(b"0").unwrap(); - assert_eq!(response, U8RangeParam(0)); - } - - #[test] - fn invalid_utf8() { - let err = deserialize::(&[0, 159]).unwrap_err(); - assert_matches!(err, ParamError::Utf8Error(_)); - } - - #[test] - fn invalid_integer() { - let err = deserialize::(b"a").unwrap_err(); - assert_matches!(err, ParamError::Parse(_)); - } - - #[test] - fn out_of_range() { - let err = deserialize::(b"20").unwrap_err(); - assert_matches!(err, ParamError::Invalid(20)); - } -} diff --git a/src/command/macros/string_response.rs b/src/command/macros/string_response.rs deleted file mode 100644 index ec4c5dd..0000000 --- a/src/command/macros/string_response.rs +++ /dev/null @@ -1,56 +0,0 @@ -macro_rules! string_response { - ($name:ident => $error:ident) => { - #[derive(::core::fmt::Debug, ::thiserror::Error)] - #[error("invalid UTF-8 bytes")] - pub struct $error(#[from] pub ::core::str::Utf8Error); - - #[derive( - ::core::clone::Clone, ::core::fmt::Debug, ::core::cmp::PartialEq, ::core::cmp::Eq, - )] - pub struct $name(pub ::std::string::String); - - impl crate::command::Response for $name { - type Error = $error; - - fn deserialize( - raw_values: &[::tokio_util::bytes::Bytes], - ) -> ::core::result::Result { - let val = ::core::str::from_utf8(&raw_values[0])?.to_string(); - Ok(Self(val)) - } - - fn expected_field_count() -> usize { - 1 - } - } - }; -} - -pub(crate) use string_response; - -#[cfg(test)] -mod tests { - use claims::assert_matches; - - use crate::command::{Response, test::deserialize}; - - string_response!(StringResponse => StringResponseError); - - #[test] - fn expected_field_count() { - assert_eq!(StringResponse::expected_field_count(), 1); - } - - #[test] - fn deserializes_valid_response() { - let response = deserialize::(b"hello, world!").unwrap(); - assert_eq!(response, StringResponse("hello, world!".to_string())); - } - - #[test] - fn invalid_utf8() { - let err = deserialize::(&[0, 159]).unwrap_err(); - assert_matches!(err, StringResponseError(_)); - } -} - diff --git a/src/command/mod.rs b/src/command/mod.rs index ce30109..696f84a 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -1,90 +1,29 @@ pub mod bc125at; -#[macro_use] -mod macros; mod no_params; mod ok_response; -use tokio_util::bytes::{BufMut, Bytes, BytesMut}; +use tokio_util::bytes::Bytes; +pub use no_params::NoParams; pub use ok_response::{OkResponse, OkResponseError}; -pub(crate) use macros::{command, range_param, range_response}; -pub(crate) use no_params::NoParams; - -use crate::codec::PARAM_DELIMITER; - -/// Defines a command that can be sent to a scanner, including params and the response type. pub trait Command { - /// The three-character text of the command itself (for example: `VOL`, `EPG`) const TEXT: &'static [u8]; - /// The type of the parameters sent with this command type Params: Params; - /// The values this command expects from the scanner in response type Response: Response; - fn params(&self) -> &Self::Params; + fn params(self) -> Self::Params; +} + +pub trait Params: IntoIterator { + fn size_hint(&self) -> usize; } -/// Tells the codec how to parse a response to a command pub trait Response: Sized { - /// The type returned when parsing fails type Error: std::error::Error; - /// Given a list of raw values, parses the response into a concrete type. - /// - /// The length of `raw_values` will always be equal to [`Response::expected_field_count`] - fn deserialize(raw_values: &[Bytes]) -> Result; + fn deserialize<'i, I: Iterator>(raw_values: I) -> Result; - /// The number of values expected by this command, not including the command itself. fn expected_field_count() -> usize; } - -pub trait Params { - /// How many parameters are included in this command - /// - /// This is used to determine how much space to reserve for `,` delimiters in the command - /// buffer before serialization. - fn count(&self) -> usize; - - /// The maximum number of bytes these parameters could take up, not including their `,` - /// delimiters - /// - /// * For numeric values, this is usually the maximum number of digits in their decimal representation - /// * For string values, this is their max length - /// - /// This is used to determine how much space to reserve for the parameters in the command buffer - /// before serialization. - fn max_size(&self) -> usize; - - /// Serialize the parameters into a given buffer - /// - /// Implementors should sequentially call [`ParamBuffer::serialize_param`] with each parameter. The - /// buffer will handle any delimiters. - fn serialize_to(&self, buffer: ParamBuffer); -} - -/// Thin wrapper around [`BytesMut`] for parameter serialization -pub struct ParamBuffer<'a>(&'a mut BytesMut); - -impl<'a> ParamBuffer<'a> { - pub fn new(bytes: &'a mut BytesMut) -> Self { - Self(bytes) - } - - /// Add a `,` delimiter before pushing the given bytes to the buffer - pub fn serialize_param(&mut self, bytes: &[u8]) { - self.0.put_u8(PARAM_DELIMITER); - self.0.extend_from_slice(bytes); - } -} - -#[cfg(test)] -pub mod test { - use super::*; - - /// Shorthand for commands to deserialize a response from raw bytes - pub fn deserialize>(val: &'static T) -> Result { - R::deserialize(&[Bytes::from(val.as_ref())]) - } -} diff --git a/src/command/no_params.rs b/src/command/no_params.rs index ba94fda..ebe5549 100644 --- a/src/command/no_params.rs +++ b/src/command/no_params.rs @@ -1,18 +1,19 @@ -use crate::command::{ParamBuffer, Params}; +use crate::command::Params; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct NoParams; -impl Params for NoParams { - fn count(&self) -> usize { - 0 - } +impl IntoIterator for NoParams { + type Item = &'static [u8]; - fn max_size(&self) -> usize { - 0 + type IntoIter = std::iter::Empty; + + fn into_iter(self) -> Self::IntoIter { + std::iter::empty() } +} - fn serialize_to(&self, _buffer: ParamBuffer) { - /* noop */ +impl Params for NoParams { + fn size_hint(&self) -> usize { + 0 } } diff --git a/src/command/ok_response.rs b/src/command/ok_response.rs index f6595a6..615efca 100644 --- a/src/command/ok_response.rs +++ b/src/command/ok_response.rs @@ -2,22 +2,26 @@ use tokio_util::bytes::Bytes; use crate::command::Response; +#[derive(Debug, Clone, Copy)] +pub struct OkResponse; #[derive(Debug, thiserror::Error)] #[error("expected `OK`")] pub struct OkResponseError; -#[derive(Clone, Copy, Debug)] -pub struct OkResponse; - impl Response for OkResponse { type Error = OkResponseError; - fn deserialize(raw_values: &[Bytes]) -> Result { - if raw_values[0] != b"OK".as_ref() { - return Err(OkResponseError); + fn deserialize<'i, I: Iterator>( + mut raw_values: I, + ) -> Result { + if let Some(val) = raw_values.next() + && val.as_ref() == b"OK" + && raw_values.next().is_none() + { + Ok(Self) + } else { + Err(OkResponseError) } - - Ok(Self) } fn expected_field_count() -> usize {