response_tokenizer: rewrite

This commit contains a rewrite of the response tokenizer, which
introduces lazy parsing of the response, handling of binary data, some
tests, as well as just generally more robustness against errors.
This commit is contained in:
2025-11-24 23:49:27 +09:00
parent 5188809327
commit 65c7798d01
39 changed files with 365 additions and 107 deletions

View File

@@ -70,7 +70,7 @@ pub trait Command {
fn parse_response(parts: ResponseAttributes<'_>) -> ResponseParserResult<'_, Self::Response>;
fn parse_raw_response(raw: &str) -> ResponseParserResult<'_, Self::Response> {
Self::parse_response(ResponseAttributes::new(raw)?)
Self::parse_response(ResponseAttributes::new(raw))
}
}
@@ -102,6 +102,9 @@ pub enum ResponseParserError<'a> {
/// A property was found in the response that was not expected.
UnexpectedProperty(&'a str),
/// A property was found multiple times in the response, but was only expected once.
DuplicateProperty(&'a str),
/// The property value is parsable, but the value is invalid or nonsensical.
InvalidProperty(&'a str, &'a str),

View File

@@ -46,7 +46,7 @@ impl Command for Outputs {
let mut enabled: Option<bool> = None;
let mut attributes: HashMap<String, String> = HashMap::new();
for (k, v) in Vec::from(parts).into_iter() {
for (k, v) in parts.into_vec()?.into_iter() {
match k {
"outputid" => {
// Reset and store the previous output if all fields are present

View File

@@ -32,7 +32,7 @@ impl Command for Channels {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
let mut channel_names = Vec::with_capacity(parts.len());
for (key, value) in parts {
debug_assert!(key == "channels");

View File

@@ -35,7 +35,7 @@ impl Command for ReadMessages {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
debug_assert!(parts.len() % 2 == 0);
let mut messages = Vec::with_capacity(parts.len() / 2);

View File

@@ -26,7 +26,7 @@ impl Command for Protocol {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts_: Vec<_> = parts.into();
let parts_: Vec<_> = parts.into_vec()?;
if let Some((k, _)) = parts_.iter().find(|(k, _)| *k != "feature") {
return Err(ResponseParserError::UnexpectedProperty(k));
}

View File

@@ -26,7 +26,7 @@ impl Command for ProtocolAvailable {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts_: Vec<_> = parts.into();
let parts_: Vec<_> = parts.into_vec()?;
if let Some((k, _)) = parts_.iter().find(|(k, _)| *k != "feature") {
return Err(ResponseParserError::UnexpectedProperty(k));
}

View File

@@ -25,7 +25,7 @@ impl Command for TagTypes {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
let mut tagtypes = Vec::with_capacity(parts.len());
for (key, value) in parts.into_iter() {

View File

@@ -26,7 +26,7 @@ impl Command for TagTypesAvailable {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
let mut tagtypes = Vec::with_capacity(parts.len());
for (key, value) in parts.into_iter() {

View File

@@ -24,7 +24,7 @@ impl Command for ListMounts {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
let mut result = Vec::with_capacity(parts.len());
for (key, value) in parts.into_iter() {
if key != "mount" {

View File

@@ -28,7 +28,7 @@ impl Command for ListNeighbors {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
debug_assert!(parts.len() % 2 == 0);
let mut result = HashMap::with_capacity(parts.len() / 2);

View File

@@ -53,7 +53,7 @@ impl Command for AlbumArt {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
let size = get_and_parse_property!(parts, "size", Text);

View File

@@ -65,7 +65,7 @@ impl Command for Count {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
let songs = get_and_parse_property!(parts, "songs", Text);
let playtime = get_and_parse_property!(parts, "playtime", Text);

View File

@@ -39,7 +39,7 @@ impl Command for GetFingerprint {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
let chromaprint = get_and_parse_property!(parts, "chromaprint", Text);

View File

@@ -90,7 +90,7 @@ impl Command for List {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts_: Vec<_> = parts.into();
let parts_: Vec<_> = parts.into_vec()?;
debug_assert!({
let key = parts_.first().map(|(k, _)| k);
parts_.iter().all(|(k, _)| k == key.unwrap())

View File

@@ -46,7 +46,7 @@ impl Command for LsInfo {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
let mut playlists = Vec::new();
for (key, value) in parts {

View File

@@ -34,7 +34,7 @@ impl Command for ReadComments {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
let comments = parts
.iter()

View File

@@ -56,7 +56,7 @@ impl Command for ReadPicture {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
if parts.is_empty() {
return Ok(None);

View File

@@ -39,7 +39,7 @@ impl Command for Rescan {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
let updating_db = get_and_parse_property!(parts, "updating_db", Text);

View File

@@ -64,7 +64,7 @@ impl Command for SearchCount {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
let songs = get_and_parse_property!(parts, "songs", Text);
let playtime = get_and_parse_property!(parts, "playtime", Text);

View File

@@ -39,7 +39,7 @@ impl Command for Update {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
let updating_db = get_and_parse_property!(parts, "updating_db", Text);

View File

@@ -26,7 +26,7 @@ impl Command for ListPartitions {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
let mut partitions = Vec::with_capacity(parts.len());
for (key, value) in parts.into_iter() {

View File

@@ -26,7 +26,7 @@ impl Command for GetVol {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
assert_eq!(parts.len(), 1);
let volume = get_and_parse_property!(parts, "volume", Text);
Ok(volume)

View File

@@ -33,7 +33,7 @@ impl Command for ReplayGainStatus {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
let replay_gain_mode = get_property!(parts, "replay_gain_mode", Text);
Ok(ReplayGainStatusResponse {

View File

@@ -41,7 +41,7 @@ impl Command for Stats {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
let uptime = get_and_parse_property!(parts, "uptime", Text);
let playtime = get_and_parse_property!(parts, "playtime", Text);

View File

@@ -66,7 +66,7 @@ pub struct StatusResponse {
fn parse_status_response(
parts: ResponseAttributes<'_>,
) -> Result<StatusResponse, ResponseParserError<'_>> {
let parts: HashMap<&str, GenericResponseValue> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
let partition = get_property!(parts, "partition", Text).to_string();
let volume = match get_property!(parts, "volume", Text) {

View File

@@ -15,6 +15,12 @@ pub struct AddRequest {
position: Option<SongPosition>,
}
impl AddRequest {
pub fn new(uri: Uri, position: Option<SongPosition>) -> Self {
Self { uri, position }
}
}
impl Command for Add {
type Request = AddRequest;
type Response = ();

View File

@@ -25,7 +25,7 @@ impl Command for Commands {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
let mut result = Vec::with_capacity(parts.len());
for (key, value) in parts.into_iter() {
if key != "command" {

View File

@@ -34,7 +34,7 @@ impl Command for Config {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
let music_directory = get_property!(parts, "music_directory", Text).to_string();
let playlist_directory = get_property!(parts, "playlist_directory", Text).to_string();

View File

@@ -36,7 +36,7 @@ impl Command for Decoders {
) -> Result<Self::Response, ResponseParserError<'_>> {
let mut result = Vec::new();
let mut current_decoder: Option<Decoder> = None;
for (key, value) in Vec::from(parts).into_iter() {
for (key, value) in parts.into_vec()?.into_iter() {
match key {
"plugin" => {
if let Some(decoder) = current_decoder.take() {

View File

@@ -25,7 +25,7 @@ impl Command for NotCommands {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
let mut result = Vec::with_capacity(parts.len());
for (key, value) in parts.into_iter() {
if key != "command" {

View File

@@ -25,7 +25,7 @@ impl Command for UrlHandlers {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
let mut url_handlers = Vec::with_capacity(parts.len());
for (key, value) in parts.into_iter() {
if key != "handler" {

View File

@@ -100,7 +100,7 @@ impl Command for StickerFind {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
let mut stickers = Vec::with_capacity(parts.len() / 2);
for sticker_uri_pair in parts.chunks_exact(2) {

View File

@@ -47,7 +47,7 @@ impl Command for StickerList {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
if let Some((k, _)) = parts.iter().find(|(k, _)| *k != "sticker") {
return Err(ResponseParserError::UnexpectedProperty(k));
}

View File

@@ -26,7 +26,7 @@ impl Command for StickerNames {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts_: Vec<_> = parts.into();
let parts_: Vec<_> = parts.into_vec()?;
if let Some((k, _)) = parts_.iter().find(|(k, _)| *k != "name") {
return Err(ResponseParserError::UnexpectedProperty(k));
}

View File

@@ -31,7 +31,7 @@ impl Command for StickerNamesTypes {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
let mut result = HashMap::new();
for name_type_pair in parts.chunks_exact(2) {

View File

@@ -26,7 +26,7 @@ impl Command for StickerTypes {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts_: Vec<_> = parts.into();
let parts_: Vec<_> = parts.into_vec()?;
if let Some((k, _)) = parts_.iter().find(|(k, _)| *k != "stickertype") {
return Err(ResponseParserError::UnexpectedProperty(k));
}

View File

@@ -52,7 +52,7 @@ impl Command for ListPlaylist {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let parts: Vec<_> = parts.into_vec()?;
let mut files = Vec::with_capacity(parts.len());
for (key, value) in parts.into_iter() {
if key != "file" {

View File

@@ -40,7 +40,7 @@ impl Command for PlaylistLength {
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into();
let parts: HashMap<_, _> = parts.into_map()?;
let songs = get_and_parse_property!(parts, "songs", Text);
let playtime = get_and_parse_property!(parts, "playtime", Text);

View File

@@ -9,38 +9,56 @@ pub enum GenericResponseValue<'a> {
// Many(Vec<GenericResponseValue<'a>>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct ResponseAttributes<'a>(Vec<(&'a str, GenericResponseValue<'a>)>);
#[derive(Debug, Clone)]
pub struct ResponseAttributes<'a> {
bytestring: &'a [u8],
cursor: usize,
}
impl<'a> ResponseAttributes<'a> {
pub fn new(raw: &'a str) -> Result<Self, ResponseParserError<'a>> {
let mut parts = Vec::new();
let mut lines = raw.lines();
loop {
let line = lines.next().ok_or(ResponseParserError::UnexpectedEOF)?;
if line.is_empty() {
println!("Warning: empty line in response");
continue;
}
pub fn new(raw: &'a str) -> Self {
Self::new_from_bytes(raw.as_bytes())
}
if line == "OK" {
break;
}
let mut keyval = line.splitn(2, ": ");
let key = keyval
.next()
.ok_or(ResponseParserError::SyntaxError(0, line))?;
// TODO: handle binary data, also verify binarylimit
let value = keyval
.next()
.ok_or(ResponseParserError::SyntaxError(0, line))?;
parts.push((key, GenericResponseValue::Text(value)));
pub fn new_from_bytes(bytes: &'a [u8]) -> Self {
Self {
bytestring: bytes,
cursor: 0,
}
}
Ok(parts.into())
pub fn is_empty(&self) -> bool {
self.cursor >= self.bytestring.len() || self.bytestring[self.cursor..].starts_with(b"OK")
}
pub fn into_map(
self,
) -> Result<HashMap<&'a str, GenericResponseValue<'a>>, ResponseParserError<'a>> {
self.into()
}
pub fn into_vec(
self,
) -> Result<Vec<(&'a str, GenericResponseValue<'a>)>, ResponseParserError<'a>> {
self.into()
}
pub fn into_lazy_vec(
self,
) -> Vec<Result<(&'a str, GenericResponseValue<'a>), ResponseParserError<'a>>> {
self.into()
}
pub fn verify_all_keys_equal(&self, expected_key: &str) -> Result<(), ResponseParserError<'a>> {
let mut copy = self.clone();
copy.cursor = 0;
for item in copy {
let (key, _) = item?;
if key != expected_key {
return Err(ResponseParserError::UnexpectedProperty(key));
}
}
Ok(())
}
// pub fn get<'a>(&self, key: &str) -> Option<&GenericResponseValue<'a>> {
@@ -48,38 +66,138 @@ impl<'a> ResponseAttributes<'a> {
// }
}
impl ResponseAttributes<'_> {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
impl<'a> Iterator for ResponseAttributes<'a> {
type Item = Result<(&'a str, GenericResponseValue<'a>), ResponseParserError<'a>>;
fn next(&mut self) -> Option<Self::Item> {
if self.cursor >= self.bytestring.len() {
return Some(Err(ResponseParserError::UnexpectedEOF));
}
if self.bytestring[self.cursor..].starts_with(b"OK") {
return None;
}
let remaining = &self.bytestring[self.cursor..];
let newline_pos = remaining
.iter()
.position(|&b| b == b'\n')
.unwrap_or(remaining.len());
let line = &remaining[..newline_pos];
// Skip empty lines
if line.is_empty() {
self.cursor += newline_pos + 1;
return self.next();
}
// NOTE: it is important that this happens before any None returns,
// so that the iterator advances despite errors.
self.cursor += newline_pos + 1;
let mut keyval = line.splitn(2, |&b| b == b':');
let key_bytes = keyval.next()?;
// TODO: should this be a proper runtime error?
debug_assert!(!key_bytes.is_empty());
debug_assert!(std::str::from_utf8(key_bytes).is_ok());
let key = std::str::from_utf8(key_bytes).ok()?;
// In the case of binary data, the following value will be the byte count
// in decimal, and the actual binary data will follow in the next N bytes,
// followed by a newline.
//
// We parse the number and assign the binary data to the "binary" key.
if key == "binary" {
let byte_count = match keyval.next() {
Some(count) => count.trim_ascii_start(),
None => {
// TODO: throw more specific error
return Some(Err(ResponseParserError::UnexpectedEOF));
}
};
let byte_count_str = match std::str::from_utf8(byte_count) {
Ok(s) => s,
Err(_) => {
// TODO: throw more specific error
return Some(Err(ResponseParserError::SyntaxError(
0,
"Invalid byte count",
)));
}
};
let byte_count: usize = match byte_count_str.parse() {
Ok(n) => n,
Err(_) => {
// TODO: throw more specific error
return Some(Err(ResponseParserError::SyntaxError(
0,
"Invalid byte count",
)));
}
};
let value_start = self.cursor;
let value_end = self.cursor + byte_count;
if value_end > self.bytestring.len() {
return Some(Err(ResponseParserError::UnexpectedEOF));
}
let value_bytes = &self.bytestring[value_start..value_end];
debug_assert!(
self.bytestring[value_end..]
.iter()
.next()
.is_none_or(|&b| b == b'\n')
);
// Skip the binary data and the following newline
self.cursor = value_end + 1;
Some(Ok((key, GenericResponseValue::Binary(value_bytes))))
} else {
let value_bytes = match keyval.next() {
Some(v) => v.trim_ascii_start(),
None => b"",
};
// TODO: this should be a proper runtime error, the specification
// declares that all string values are UTF-8.
debug_assert!(std::str::from_utf8(value_bytes).is_ok());
let value_str = std::str::from_utf8(value_bytes).ok()?;
Some(Ok((key, GenericResponseValue::Text(value_str))))
}
}
}
impl<'a> From<HashMap<&'a str, GenericResponseValue<'a>>> for ResponseAttributes<'a> {
fn from(map: HashMap<&'a str, GenericResponseValue<'a>>) -> Self {
Self(map.into_iter().collect())
}
}
impl<'a> From<ResponseAttributes<'a>> for HashMap<&'a str, GenericResponseValue<'a>> {
impl<'a> From<ResponseAttributes<'a>>
for Result<HashMap<&'a str, GenericResponseValue<'a>>, ResponseParserError<'a>>
{
fn from(val: ResponseAttributes<'a>) -> Self {
debug_assert!({
let mut uniq = HashSet::new();
val.0.iter().all(move |x| uniq.insert(*x))
});
val.0.into_iter().collect()
let mut map = HashMap::new();
for item in val {
let (k, v) = item?;
if map.contains_key(k) {
return Err(ResponseParserError::DuplicateProperty(k));
}
map.insert(k, v);
}
Ok(map)
}
}
impl<'a> From<ResponseAttributes<'a>> for Vec<(&'a str, GenericResponseValue<'a>)> {
impl<'a> From<ResponseAttributes<'a>>
for Vec<Result<(&'a str, GenericResponseValue<'a>), ResponseParserError<'a>>>
{
fn from(val: ResponseAttributes<'a>) -> Self {
val.0
val.collect()
}
}
impl<'a> From<Vec<(&'a str, GenericResponseValue<'a>)>> for ResponseAttributes<'a> {
fn from(val: Vec<(&'a str, GenericResponseValue<'a>)>) -> Self {
Self(val)
impl<'a> From<ResponseAttributes<'a>>
for Result<Vec<(&'a str, GenericResponseValue<'a>)>, ResponseParserError<'a>>
{
fn from(val: ResponseAttributes<'a>) -> Self {
val.collect()
}
}
@@ -205,11 +323,12 @@ macro_rules! get_next_optional_property {
macro_rules! get_next_property {
($parts:ident, $variant:ident) => {
match $parts.next() {
Some((name, value)) => (
Some(Ok((name, value))) => (
name,
crate::response_tokenizer::_expect_property_type!({ Some(value) }, name, $variant)
.unwrap(),
),
Some(Err(e)) => return Err(e),
None => return Err(crate::commands::ResponseParserError::UnexpectedEOF),
}
};
@@ -239,7 +358,7 @@ macro_rules! get_next_and_parse_optional_property {
macro_rules! get_next_and_parse_property {
($parts:ident, $variant:ident) => {
match $parts.next() {
Some((name, value)) => {
Some(Ok((name, value))) => {
let prop = crate::response_tokenizer::_expect_property_type!(
{ Some(value) },
name,
@@ -251,12 +370,13 @@ macro_rules! get_next_and_parse_property {
crate::response_tokenizer::_unwrap_optional_property_type!(name, prop),
)
}
Some(Err(e)) => return Err(e),
None => return Err(crate::commands::ResponseParserError::UnexpectedEOF),
}
};
}
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
pub(crate) use _expect_property_type;
pub(crate) use _parse_optional_property_type;
@@ -276,36 +396,165 @@ use crate::commands::ResponseParserError;
#[cfg(test)]
mod tests {
use indoc::indoc;
use super::*;
#[test]
#[cfg(debug_assertions)]
fn test_valid_hashmap_uniqueness_assert() {
let valid_maplike_attrs: ResponseAttributes = vec![
("a", GenericResponseValue::Text("1")),
("A", GenericResponseValue::Text("2")),
("A ", GenericResponseValue::Text("3")),
("b", GenericResponseValue::Text("4")),
]
.into();
let map: HashMap<_, _> = valid_maplike_attrs.into();
fn test_valid_hashmap_uniqueness_assert() -> Result<(), ResponseParserError<'static>> {
let raw_response = indoc! {
"a: 1
A: 2
A : 3
b: 4
OK"
};
let attrs = ResponseAttributes::new(raw_response);
let map: HashMap<_, _> = attrs.into_map()?;
assert_eq!(map.len(), 4);
Ok(())
}
#[test]
#[cfg(debug_assertions)]
#[should_panic]
fn test_invalid_hashmap_uniqueness_assert() {
let invalid_maplike_attrs: ResponseAttributes = vec![
("a", GenericResponseValue::Text("1")),
("b", GenericResponseValue::Text("2")),
("c", GenericResponseValue::Text("3")),
("a", GenericResponseValue::Text("4")),
]
.into();
let raw_response = indoc! {
"a: 1
b: 2
c: 3
a: 4
OK"
};
ResponseAttributes::new(raw_response).into_map().unwrap();
}
let map: HashMap<_, _> = invalid_maplike_attrs.into();
assert_eq!(map.len(), 4);
#[test]
fn test_response_attributes_single_attribute() -> Result<(), ResponseParserError<'static>> {
let raw_response = indoc! {
"name: Sticker1
OK"
};
let attrs = ResponseAttributes::new(raw_response);
let map: HashMap<_, _> = attrs.into_map()?;
assert_eq!(map.len(), 1);
assert_eq!(
map.get("name"),
Some(&GenericResponseValue::Text("Sticker1"))
);
Ok(())
}
#[test]
fn test_response_attributes_multiple_attributes() -> Result<(), ResponseParserError<'static>> {
let raw_response = indoc! {
"name: Sticker1
type: emoji
size: 128
OK"
};
let attrs = ResponseAttributes::new(raw_response);
let map: HashMap<_, _> = attrs.into_map()?;
assert_eq!(map.len(), 3);
assert_eq!(
map.get("name"),
Some(&GenericResponseValue::Text("Sticker1"))
);
assert_eq!(map.get("type"), Some(&GenericResponseValue::Text("emoji")));
assert_eq!(map.get("size"), Some(&GenericResponseValue::Text("128")));
Ok(())
}
#[test]
fn test_response_attributes_empty_response() -> Result<(), ResponseParserError<'static>> {
let raw_response = indoc! {
"OK"
};
let attrs = ResponseAttributes::new(raw_response);
let map: HashMap<_, _> = attrs.into_map()?;
assert_eq!(map.len(), 0);
Ok(())
}
#[test]
fn test_response_attributes_unexpected_eof() -> Result<(), ResponseParserError<'static>> {
let raw_response = indoc! {
"name: Sticker1
type: emoji"
};
let attrs = ResponseAttributes::new(raw_response);
let vec: Result<Vec<_>, ResponseParserError> = attrs.into_vec();
assert!(matches!(vec, Err(ResponseParserError::UnexpectedEOF)));
Ok(())
}
#[test]
fn test_response_attributes_repeated_attribute() -> Result<(), ResponseParserError<'static>> {
let raw_response = indoc! {
"name: Sticker1
name: Sticker2
OK"
};
let attrs = ResponseAttributes::new(raw_response);
let vec = attrs.into_vec()?;
assert_eq!(vec.len(), 2);
assert_eq!(vec[0], ("name", GenericResponseValue::Text("Sticker1")));
assert_eq!(vec[1], ("name", GenericResponseValue::Text("Sticker2")));
Ok(())
}
#[test]
fn test_response_attributes_empty_line() -> Result<(), ResponseParserError<'static>> {
let raw_response = indoc! {
"name: Sticker1
type: emoji
OK"
};
let attrs = ResponseAttributes::new(raw_response);
let map: HashMap<_, _> = attrs.into_map()?;
assert_eq!(map.len(), 2);
assert_eq!(
map.get("name"),
Some(&GenericResponseValue::Text("Sticker1"))
);
assert_eq!(map.get("type"), Some(&GenericResponseValue::Text("emoji")));
Ok(())
}
#[test]
fn test_response_attributes_no_value() -> Result<(), ResponseParserError<'static>> {
let raw_response = indoc! {
"name:
type: emoji
OK"
};
let attrs = ResponseAttributes::new(raw_response);
let map: HashMap<_, _> = attrs.into_map()?;
assert_eq!(map.len(), 2);
assert_eq!(map.get("name"), Some(&GenericResponseValue::Text("")));
assert_eq!(map.get("type"), Some(&GenericResponseValue::Text("emoji")));
Ok(())
}
#[test]
fn test_response_attributes_binary_data() -> Result<(), ResponseParserError<'static>> {
let bytestring: &[u8] = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09";
let raw_response = {
let mut response = format!("binary: {}\n", bytestring.len()).into_bytes();
response.extend_from_slice(bytestring);
response.extend_from_slice(b"\nOK");
response
};
let attrs = ResponseAttributes::new_from_bytes(&raw_response);
let map: HashMap<_, _> = attrs.into_map().unwrap();
assert_eq!(map.len(), 1);
assert_eq!(
map.get("binary"),
Some(&GenericResponseValue::Binary(bytestring))
);
Ok(())
}
}