From 3339a24f903b47b0a9ff35ff956a2d24b3b5e44c Mon Sep 17 00:00:00 2001 From: Alex van de Sandt Date: Wed, 6 May 2026 18:47:07 -0500 Subject: [PATCH] Overhaul command params traits to avoid lifetime errors --- examples/version.rs | 4 +-- src/codec.rs | 23 ++++++--------- src/command/bc125at.rs | 59 ++++++++++++++++++++++++++++++++++---- src/command/macros.rs | 25 ++++++++++++---- src/command/mod.rs | 42 +++++++++++++++++---------- src/command/no_params.rs | 19 +++++------- src/command/ok_response.rs | 1 + src/lib.rs | 4 +-- src/scanner.rs | 7 ++--- 9 files changed, 121 insertions(+), 63 deletions(-) diff --git a/examples/version.rs b/examples/version.rs index 2739a41..857aaab 100644 --- a/examples/version.rs +++ b/examples/version.rs @@ -14,11 +14,11 @@ async fn print_response( cmd: Cmd, ) -> Result<(), Box> where - Cmd: Command<'static> + 'static, + 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:#?}"); + println!("{name} => {r:?}"); Ok(()) } diff --git a/src/codec.rs b/src/codec.rs index 8e6b51c..aae97bb 100644 --- a/src/codec.rs +++ b/src/codec.rs @@ -3,7 +3,7 @@ use tokio_util::codec::{AnyDelimiterCodec, AnyDelimiterCodecError, Decoder, Enco use crate::{ bytes_split::BytesSplit, - command::{Command, ParamSet, Response}, + command::{Command, ParamBuffer, Params, Response}, }; const RETURN_CODE: u8 = b'\r'; @@ -22,9 +22,9 @@ impl Codec { } } -impl<'p, Cmd> Encoder for Codec +impl Encoder for Codec where - Cmd: Command<'p>, + Cmd: Command, { type Error = std::io::Error; @@ -33,16 +33,12 @@ where item: Cmd, dst: &mut tokio_util::bytes::BytesMut, ) -> Result<(), Self::Error> { - let params = item.param_set(); - let est_len = Cmd::TEXT.len() + params.count() + params.size() + 1; + let params = item.params(); + let est_len = Cmd::TEXT.len() + params.count() + params.total_size() + 1; dst.reserve(est_len); dst.extend_from_slice(Cmd::TEXT); - - for param in item.param_set() { - dst.put_u8(PARAM_DELIMITER); - param.serialize_to(dst); - } + params.serialize_to(ParamBuffer::new(dst)); dst.put_u8(b'\r'); @@ -68,12 +64,9 @@ pub struct RawResponse { } impl RawResponse { - pub fn deserialize<'p, Cmd>( + pub fn deserialize( &self, - ) -> Result::Error>> - where - Cmd: Command<'p>, - { + ) -> Result::Error>> { if self.cmd != Cmd::TEXT { return Err(ResponseError::WrongCommand); } diff --git a/src/command/bc125at.rs b/src/command/bc125at.rs index 1f7cf6c..1298843 100644 --- a/src/command/bc125at.rs +++ b/src/command/bc125at.rs @@ -1,9 +1,8 @@ -use std::str::Utf8Error; +use std::{num::ParseIntError, str::Utf8Error}; -use crate::{ - NoParams, OkResponse, - command::{Command, Response, command}, -}; +use crate::command::{Command, NoParams, OkResponse, Params, Response, command}; + +use super::ParamBuffer; command!(b"PRG": EnterProgramMode); command!(b"EPG": ExitProgramMode); @@ -30,3 +29,53 @@ impl Response for FirmwareVersion { } command!(b"VER": GetFirmwareVersion => FirmwareVersion); + +#[derive(Debug, thiserror::Error)] +pub enum VolumeLevelError { + #[error("invalid UTF-8 bytes")] + Utf8Error(#[from] Utf8Error), + #[error(transparent)] + Parse(#[from] ParseIntError), + #[error("volume must be between [0..15], got `{0}`")] + InvalidVolume(u8), +} + +#[derive(Debug)] +pub struct VolumeLevel(u8); + +impl VolumeLevel { + pub fn new(level: u8) -> Option { + (0..=15).contains(&level).then_some(Self(level)) + } +} + +impl Params for VolumeLevel { + fn count(&self) -> usize { + 1 + } + + fn total_size(&self) -> usize { + if self.0 >= 10 { 2 } else { 1 } + } + + fn serialize_to(&self, mut buffer: ParamBuffer) { + let mut serialized = itoa::Buffer::new(); + buffer.serialize_param(serialized.format(self.0).as_bytes()); + } +} + +impl Response for VolumeLevel { + type Error = VolumeLevelError; + + fn deserialize(raw_values: &[bytes::Bytes]) -> Result { + let level = str::from_utf8(&raw_values[0])?.parse()?; + Self::new(level).ok_or(VolumeLevelError::InvalidVolume(level)) + } + + fn expected_field_count() -> usize { + 1 + } +} + +command!(b"VOL": GetVolumeLevel => VolumeLevel); +command!(b"VOL": SetVolumeLevel(VolumeLevel) => OkResponse); diff --git a/src/command/macros.rs b/src/command/macros.rs index ae837ab..011c5a5 100644 --- a/src/command/macros.rs +++ b/src/command/macros.rs @@ -2,26 +2,39 @@ macro_rules! command { ($text:literal: $name:ident) => { pub struct $name; - impl Command<'static> for $name { + impl Command for $name { const TEXT: &'static [u8] = $text; type Params = NoParams; type Response = OkResponse; - fn param_set(&self) -> Self::Params { - NoParams + fn params(&self) -> &Self::Params { + &NoParams } } }; ($text:literal: $name:ident => $response:ident) => { pub struct $name; - impl Command<'static> for $name { + impl Command for $name { const TEXT: &'static [u8] = $text; type Params = NoParams; type Response = $response; - fn param_set(&self) -> Self::Params { - NoParams + fn params(&self) -> &Self::Params { + &NoParams + } + } + }; + ($text:literal: $name:ident($param_set:ident) => $response:ident) => { + pub struct $name(pub $param_set); + + impl Command for $name { + const TEXT: &'static [u8] = $text; + type Params = $param_set; + type Response = $response; + + fn params(&self) -> &Self::Params { + &self.0 } } }; diff --git a/src/command/mod.rs b/src/command/mod.rs index d1c2c26..6444648 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -1,28 +1,21 @@ pub mod bc125at; #[macro_use] mod macros; -pub mod no_params; -pub mod ok_response; +mod no_params; +mod ok_response; -use bytes::{Bytes, BytesMut}; +use bytes::{BufMut, Bytes, BytesMut}; pub(crate) use macros::command; +pub(crate) use no_params::NoParams; +pub use ok_response::{OkResponse, OkResponseError}; -pub trait Command<'p> { +pub trait Command { const TEXT: &'static [u8]; - type Params: ParamSet<'p>; + type Params: Params; type Response: Response; - fn param_set(&self) -> Self::Params; -} - -pub trait ParamSet<'p>: IntoIterator { - fn count(&self) -> usize; - fn size(&self) -> usize; -} - -pub trait Param { - fn serialize_to(&self, dst: &mut BytesMut); + fn params(&self) -> &Self::Params; } pub trait Response: Sized { @@ -32,3 +25,22 @@ pub trait Response: Sized { fn expected_field_count() -> usize; } + +pub trait Params { + fn count(&self) -> usize; + fn total_size(&self) -> usize; + fn serialize_to(&self, buffer: ParamBuffer); +} + +pub struct ParamBuffer<'a>(&'a mut BytesMut); + +impl<'a> ParamBuffer<'a> { + pub fn new(bytes: &'a mut BytesMut) -> Self { + Self(bytes) + } + + fn serialize_param(&mut self, bytes: &[u8]) { + self.0.put_u8(b','); + self.0.extend_from_slice(bytes); + } +} diff --git a/src/command/no_params.rs b/src/command/no_params.rs index 8f3dd45..b74ddf5 100644 --- a/src/command/no_params.rs +++ b/src/command/no_params.rs @@ -1,22 +1,17 @@ -use crate::command::{Param, ParamSet}; +use crate::command::{ParamBuffer, Params}; pub struct NoParams; -impl IntoIterator for NoParams { - type Item = &'static dyn Param; - type IntoIter = std::iter::Empty; - - fn into_iter(self) -> Self::IntoIter { - std::iter::empty() - } -} - -impl ParamSet<'static> for NoParams { +impl Params for NoParams { fn count(&self) -> usize { 0 } - fn size(&self) -> usize { + fn total_size(&self) -> usize { 0 } + + fn serialize_to(&self, _buffer: ParamBuffer) { + /* noop */ + } } diff --git a/src/command/ok_response.rs b/src/command/ok_response.rs index c09497c..f41e7b7 100644 --- a/src/command/ok_response.rs +++ b/src/command/ok_response.rs @@ -8,6 +8,7 @@ pub enum OkResponseError { WrongNumberOfFields, } +#[derive(Clone, Copy, Debug)] pub struct OkResponse; impl Response for OkResponse { diff --git a/src/lib.rs b/src/lib.rs index 3130324..8115d25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,8 +4,6 @@ mod command; mod scanner; pub use crate::{ - command::{ - Command, Param, ParamSet, Response, bc125at, no_params::NoParams, ok_response::OkResponse, - }, + command::{Command, OkResponse, OkResponseError, Params, Response, bc125at}, scanner::{Scanner, ScannerError}, }; diff --git a/src/scanner.rs b/src/scanner.rs index b3a1914..06c070a 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -64,13 +64,10 @@ impl Scanner { Ok(Self(framed)) } - pub async fn command<'p, Cmd>( + pub async fn command( &mut self, cmd: Cmd, - ) -> Result::Error>> - where - Cmd: Command<'p>, - { + ) -> Result::Error>> { self.0.send(cmd).await.map_err(ScannerError::from)?; let raw_response = self.0.next().await.ok_or(ScannerError::PortClosed)??; let response = raw_response.deserialize::()?; -- 2.51.2