//! The basic building blocks, definitions and helpers used to define an Mpd command. //! //! An mpd command consists of a pair of serializers and parsers for both the request //! and the corresponding response, as well as the command name used to identify the command. //! //! Each command is modelled as a struct implementing the [`Command`] trait, //! which in turn uses the [`CommandRequest`] and [`CommandResponse`] traits //! to define the request and response types respectively. use crate::{request_tokenizer::RequestTokenizer, response_tokenizer::ResponseAttributes}; use serde::{Deserialize, Serialize}; use thiserror::Error; mod audio_output_devices; mod client_to_client; mod connection_settings; mod controlling_playback; mod mounts_and_neighbors; mod music_database; mod partition_commands; mod playback_options; mod querying_mpd_status; mod queue; mod reflection; mod stickers; mod stored_playlists; pub use audio_output_devices::*; pub use client_to_client::*; pub use connection_settings::*; pub use controlling_playback::*; pub use mounts_and_neighbors::*; pub use music_database::*; pub use partition_commands::*; pub use playback_options::*; pub use querying_mpd_status::*; pub use queue::*; pub use reflection::*; pub use stickers::*; pub use stored_playlists::*; /// A trait modelling a single MPD command request. pub trait CommandRequest where Self: Sized, { /// The command name used within the protocol const COMMAND: &'static str; // TODO: add these for ease of throwing parsing errors /// The minimum number of arguments this command takes const MIN_ARGS: u32; /// The maximum number of arguments this command takes. /// /// Note that in the case of keyworded arguments, such as /// `group `, `sort `, etc., these are /// counted as a single argument despite being two tokens. const MAX_ARGS: Option; /// Helper function to create a [`RequestParserError::TooManyArguments`] error fn too_many_arguments_error(found: u32) -> RequestParserError { RequestParserError::TooManyArguments { expected_min: Self::MIN_ARGS, expected_max: Self::MAX_ARGS, found, } } /// Helper function to throw a [`RequestParserError::TooManyArguments`] error fn throw_if_too_many_arguments(parts: RequestTokenizer<'_>) -> Result<(), RequestParserError> { let remaining_args = parts.count().try_into().unwrap_or(u32::MAX); if remaining_args != 0 { return Err(Self::too_many_arguments_error( remaining_args.saturating_add(Self::MAX_ARGS.unwrap()), )); } Ok(()) } /// Helper function to create a [`RequestParserError::MissingArguments`] error fn missing_arguments_error(found: u32) -> RequestParserError { RequestParserError::MissingArguments { expected_min: Self::MIN_ARGS, expected_max: Self::MAX_ARGS, found, } } /// Serializes the request into a String. fn serialize(&self) -> String; /// Parses the request from its tokenized parts. /// See also [`parse_raw`]. fn parse(parts: RequestTokenizer<'_>) -> Result; /// Parses the request from its raw string representation. /// /// This assumes the raw string starts with the command name, e.g. /// `command_name arg1 "arg2 arg3"` fn parse_raw(raw: &str) -> Result { let (line, _rest) = raw .split_once('\n') .ok_or(RequestParserError::MissingNewline)?; if line.is_empty() { return Err(RequestParserError::EmptyLine); } let mut tokenized = RequestTokenizer::new(line); let command_name_token_length = Self::COMMAND.split_ascii_whitespace().count(); let mut command_name = Vec::with_capacity(command_name_token_length); for _ in 0..command_name_token_length { let token = tokenized .next() .ok_or(RequestParserError::SyntaxError(0, line.to_string()))?; command_name.push(token); } let command_name = command_name.join(" "); if command_name != Self::COMMAND { return Err(RequestParserError::SyntaxError(0, line.to_string())); } Self::parse(tokenized) } } /// A trait modelling a single MPD command response. pub trait CommandResponse where Self: Sized, { // /// Serializes the response into a Vec. // fn serialize(&self) -> Vec; /// Parses the response from its tokenized parts. /// See also [`parse_raw`]. fn parse(parts: ResponseAttributes<'_>) -> Result; /// Parses the response from its raw byte representation. fn parse_raw(raw: &[u8]) -> Result { Self::parse(ResponseAttributes::new_from_bytes(raw)) } } /// A trait modelling the request/response pair of a single MPD command. pub trait Command { /// The request sent from the client to the server type Request: CommandRequest; /// The response sent from the server to the client type Response: CommandResponse; /// The command name used within the protocol const COMMAND: &'static str = Self::Request::COMMAND; /// Parse the request from its tokenized parts. See also [`parse_raw_request`]. fn parse_request(parts: RequestTokenizer) -> Result { Self::Request::parse(parts) } /// Parse the raw request string into a request. /// This assumes the raw string starts with the command name, e.g. `command_name arg1 "arg2 arg3"` fn parse_raw_request(raw: &str) -> Result { Self::Request::parse_raw(raw) } // /// Serialize the response into a string. // fn serialize_response(&self, response: Self::Response) -> String { /// Parse the response from its tokenized parts. See also [`parse_raw_response`]. fn parse_response(parts: ResponseAttributes) -> Result { Self::Response::parse(parts) } /// Parse the raw response string into a response. fn parse_raw_response(raw: &[u8]) -> Result { Self::Response::parse_raw(raw) } } // Request/response implementation helpers macro_rules! empty_command_request { ($name:ident, $command_name:expr) => { paste::paste! { #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct [<$name Request>]; } impl std::default::Default for paste::paste! { [<$name Request>] } { fn default() -> Self { paste::paste! { [<$name Request>] } } } impl $name { pub fn req() -> ::Request { paste::paste! { [<$name Request>] } } } impl crate::commands::CommandRequest for paste::paste! { [<$name Request>] } { const COMMAND: &'static str = $command_name; const MIN_ARGS: u32 = 0; const MAX_ARGS: Option = Some(0); fn serialize(&self) -> String { Self::COMMAND.to_string() + "\n" } fn parse( parts: crate::commands::RequestTokenizer<'_>, ) -> Result { Self::throw_if_too_many_arguments(parts)?; Ok(paste::paste! { [<$name Request>] }) } } }; } macro_rules! empty_command_response { ($name:ident) => { paste::paste! { #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct [<$name Response>]; } impl std::default::Default for paste::paste! { [<$name Response>] } { fn default() -> Self { paste::paste! { [<$name Response>] } } } impl $name { pub fn res() -> ::Response { paste::paste! { [<$name Response>] } } } impl crate::commands::CommandResponse for paste::paste! { [<$name Response>] } { fn parse( _parts: crate::commands::ResponseAttributes<'_>, ) -> Result { debug_assert!(_parts.is_empty()); Ok(paste::paste! { [<$name Response>] }) } } }; } macro_rules! single_item_command_request { ($name:ident, $command_name:expr, $item_type:ty) => { paste::paste! { #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct [<$name Request>] (pub $item_type); } impl paste::paste! { [<$name Request>] } { pub fn new(item: $item_type) -> Self { paste::paste! { crate::commands::[<$name Request>](item) } } } impl $name { pub fn req(item: $item_type) -> ::Request { ::Request::new(item) } } impl crate::commands::CommandRequest for paste::paste! { [<$name Request>] } { const COMMAND: &'static str = $command_name; const MIN_ARGS: u32 = 1; const MAX_ARGS: Option = Some(1); fn serialize(&self) -> String { format!("{} {}\n", Self::COMMAND, self.0) } fn parse( mut parts: crate::commands::RequestTokenizer<'_>, ) -> Result { let item_token = parts.next().ok_or(Self::missing_arguments_error(0))?; let item = item_token.parse::<$item_type>().map_err(|_| { crate::commands::RequestParserError::SubtypeParserError { argument_index: 1, expected_type: stringify!($item_type), raw_input: item_token.to_owned(), } })?; Self::throw_if_too_many_arguments(parts)?; Ok(paste::paste! { [<$name Request>] ( item ) }) } } }; } macro_rules! single_optional_item_command_request { ($name:ident, $command_name:expr, $item_type:ty) => { paste::paste! { #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct [<$name Request>] (pub Option<$item_type>); } impl paste::paste! { [<$name Request>] } { pub fn new(item: Option<$item_type>) -> Self { paste::paste! { crate::commands::[<$name Request>](item) } } } impl $name { pub fn req(item: Option<$item_type>) -> ::Request { ::Request::new(item) } } impl crate::commands::CommandRequest for paste::paste! { [<$name Request>] } { const COMMAND: &'static str = $command_name; const MIN_ARGS: u32 = 0; const MAX_ARGS: Option = Some(1); fn serialize(&self) -> String { match &self.0 { Some(item) => format!("{} {}\n", Self::COMMAND, item), None => Self::COMMAND.to_string() + "\n", } } fn parse( mut parts: crate::commands::RequestTokenizer<'_>, ) -> Result { let item = parts .next() .map(|s| { s.parse().map_err(|_| { crate::commands::RequestParserError::SubtypeParserError { argument_index: 1, expected_type: stringify!($item_type), raw_input: s.to_owned(), } }) }) .transpose()?; Self::throw_if_too_many_arguments(parts)?; Ok(paste::paste! { [<$name Request>] ( item ) }) } } }; } macro_rules! single_item_command_response { ($name:ident, $item_name:expr, $item_type:ty) => { paste::paste! { #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct [<$name Response>] (pub $item_type); } impl paste::paste! { [<$name Response>] } { pub fn new(item: $item_type) -> Self { paste::paste! { crate::commands::[<$name Response>](item) } } } impl $name { pub fn res(item: $item_type) -> ::Response { ::Response::new(item) } } impl crate::commands::CommandResponse for paste::paste! { [<$name Response>] } { fn parse( parts: crate::commands::ResponseAttributes<'_>, ) -> Result { let map = parts.into_map()?; debug_assert!(map.len() == 1, "Expected only one property in response"); let item_token = map.get($item_name).ok_or( crate::commands::ResponseParserError::MissingProperty($item_name.to_string()), )?; let item_ = crate::response_tokenizer::expect_property_type!( Some(item_token), $item_name, Text ); let item = item_.parse::<$item_type>().map_err(|_| { crate::commands::ResponseParserError::InvalidProperty( $item_name.to_string(), item_.to_string(), ) })?; Ok(paste::paste! { [<$name Response>] ( item ) }) } } }; } macro_rules! multi_item_command_response { ($name:ident, $item_name:expr, $item_type:ty) => { paste::paste! { #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct [<$name Response>] (pub Vec<$item_type>); } impl paste::paste! { [<$name Response>] } { pub fn new(items: Vec<$item_type>) -> Self { paste::paste! { crate::commands::[<$name Response>](items) } } } impl $name { pub fn res(items: Vec<$item_type>) -> ::Response { ::Response::new(items) } } impl crate::commands::CommandResponse for paste::paste! { [<$name Response>] } { fn parse( parts: crate::commands::ResponseAttributes<'_>, ) -> Result { // TODO: use lazy vec let parts_: Vec<_> = parts.into_vec()?; if let Some((k, _)) = parts_.iter().find(|(k, _)| *k != $item_name) { return Err(ResponseParserError::UnexpectedProperty(k.to_string())); } let mut items = Vec::with_capacity(parts_.len()); let mut iter = parts_.into_iter(); while let Some(value) = iter.next() { let unwrapped_value = expect_property_type!(Some(value.1), $item_name, Text); let parsed_value = unwrapped_value.parse::<$item_type>().map_err(|_| { crate::commands::ResponseParserError::InvalidProperty( $item_name.to_string(), unwrapped_value.to_string(), ) })?; items.push(parsed_value); } Ok(paste::paste! { [<$name Response>] ( items ) }) } } }; } pub(crate) use empty_command_request; pub(crate) use empty_command_response; pub(crate) use multi_item_command_response; pub(crate) use single_item_command_request; pub(crate) use single_item_command_response; pub(crate) use single_optional_item_command_request; #[derive(Error, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum RequestParserError { #[error("Found empty line while parsing request")] EmptyLine, // TODO: remove this, replaced by various other errors #[error("Could not parse the request due to a syntax error at position {0}: {1}")] SyntaxError(u64, String), // TODO: can we store the parser error as well? #[error( "Could not parse argument {argument_index} of the request (expected type: {expected_type}, raw input: '{raw_input}')" )] SubtypeParserError { /// The index of the argument that failed to parse /// /// Note that in the case of keyworded arguments, such as /// `group `, `sort `, etc., these are /// counted as a single argument despite being two tokens. argument_index: u32, /// The expected type of the argument expected_type: &'static str, /// The raw input that failed to parse raw_input: String, }, #[error( "Too many arguments were provided in the request (expected between {expected_min} and {expected_max:?}, found {found})" )] TooManyArguments { /// The minimum number of arguments that were expected expected_min: u32, /// The maximum number of arguments that were expected /// /// This is `None` if the amount of arguments is unbounded. expected_max: Option, /// The number of arguments that were found /// /// Note that in the case of keyworded arguments, such as /// `group `, `sort `, etc., these are /// counted as a single argument despite being two tokens. found: u32, }, #[error( "Not enough arguments were provided in the request (expected between {expected_min} and {expected_max:?}, found {found})" )] MissingArguments { /// The minimum number of arguments that were expected expected_min: u32, /// The maximum number of arguments that were expected. /// /// This is `None` if the amount of arguments is unbounded. expected_max: Option, /// The number of arguments that were found /// /// Note that in the case of keyworded arguments, such as /// `group `, `sort `, etc., these are /// counted as a single argument despite being two tokens. found: u32, }, #[error("Keyword argument {keyword} at position {argument_index} is missing its value")] MissingKeywordValue { /// The unexpected keyword that was found keyword: &'static str, /// The index of the argument that was missing it's value /// /// Note that in the case of keyworded arguments, such as /// `group `, `sort `, etc., these are /// counted as a single argument despite being two tokens. argument_index: u32, }, #[error("A command list was expected to be closed, but the end was not found")] MissingCommandListEnd, #[error("A command list was found inside another command list at line {line}")] NestedCommandList { /// The line where the nested command list was found line: u32, }, #[error("An unexpected command list end was found")] UnexpectedCommandListEnd, // TODO: remove this, replaced by EmptyLine + MissingArguments #[error("Request ended early, while more arguments were expected")] UnexpectedEOF, #[error("Request is missing terminating newline")] MissingNewline, } // TODO: should these be renamed to fit the mpd docs? // "Attribute" instead of "Property"? #[derive(Error, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ResponseParserError { #[error("A property was expected to be present in the response, but was not found: {0}")] MissingProperty(String), // TODO: change name to UnexpectedPropertyEncoding #[error( "An expected property was found in the response, but its encoding was not as expected: {0}: {1}" )] UnexpectedPropertyType(String, String), #[error("A property was found in the response that was not expected: {0}")] UnexpectedProperty(String), #[error("A property was found multiple times in the response, but was only expected once: {0}")] DuplicateProperty(String), #[error("The property value is parsable, but the value is invalid or nonsensical: {0}: {1}")] InvalidProperty(String, String), #[error("Could not parse the response due to a syntax error at position {0}: {1}")] SyntaxError(u64, String), #[error("Response ended early, while more properties were expected")] UnexpectedEOF, // #[error("Response is missing terminating newline")] // MissingNewline, } /*******************/ pub const COMMAND_NAMES: &[&str] = &[ // Audio output devices DisableOutput::COMMAND, EnableOutput::COMMAND, Outputs::COMMAND, OutputSet::COMMAND, ToggleOutput::COMMAND, // Client to client Channels::COMMAND, ReadMessages::COMMAND, SendMessage::COMMAND, Subscribe::COMMAND, Unsubscribe::COMMAND, // Connection settings BinaryLimit::COMMAND, Close::COMMAND, Kill::COMMAND, Password::COMMAND, Ping::COMMAND, Protocol::COMMAND, ProtocolAll::COMMAND, ProtocolAvailable::COMMAND, ProtocolClear::COMMAND, ProtocolDisable::COMMAND, ProtocolEnable::COMMAND, TagTypes::COMMAND, TagTypesAll::COMMAND, TagTypesAvailable::COMMAND, TagTypesClear::COMMAND, TagTypesDisable::COMMAND, TagTypesEnable::COMMAND, TagTypesReset::COMMAND, // Controlling playback Next::COMMAND, Pause::COMMAND, Play::COMMAND, PlayId::COMMAND, Previous::COMMAND, Seek::COMMAND, SeekCur::COMMAND, SeekId::COMMAND, Stop::COMMAND, // Mounts and neighbors ListMounts::COMMAND, ListNeighbors::COMMAND, Mount::COMMAND, Unmount::COMMAND, // Music database AlbumArt::COMMAND, Count::COMMAND, Find::COMMAND, FindAdd::COMMAND, GetFingerprint::COMMAND, List::COMMAND, ListAll::COMMAND, ListAllInfo::COMMAND, ListFiles::COMMAND, LsInfo::COMMAND, ReadComments::COMMAND, ReadPicture::COMMAND, Rescan::COMMAND, Search::COMMAND, SearchAdd::COMMAND, SearchAddPl::COMMAND, SearchCount::COMMAND, Update::COMMAND, // Partition commands DelPartition::COMMAND, ListPartitions::COMMAND, MoveOutput::COMMAND, NewPartition::COMMAND, Partition::COMMAND, // Playback options Consume::COMMAND, Crossfade::COMMAND, GetVol::COMMAND, MixRampDb::COMMAND, MixRampDelay::COMMAND, Random::COMMAND, Repeat::COMMAND, ReplayGainMode::COMMAND, ReplayGainStatus::COMMAND, SetVol::COMMAND, Single::COMMAND, Volume::COMMAND, // Querying mpd status ClearError::COMMAND, CurrentSong::COMMAND, Idle::COMMAND, Stats::COMMAND, Status::COMMAND, // Queue Add::COMMAND, AddId::COMMAND, AddTagId::COMMAND, Clear::COMMAND, ClearTagId::COMMAND, Delete::COMMAND, DeleteId::COMMAND, Move::COMMAND, MoveId::COMMAND, Playlist::COMMAND, PlaylistFind::COMMAND, PlaylistId::COMMAND, PlaylistInfo::COMMAND, PlaylistSearch::COMMAND, PlChanges::COMMAND, PlChangesPosId::COMMAND, Prio::COMMAND, PrioId::COMMAND, RangeId::COMMAND, Shuffle::COMMAND, Swap::COMMAND, SwapId::COMMAND, // Reflection Commands::COMMAND, Config::COMMAND, Decoders::COMMAND, NotCommands::COMMAND, UrlHandlers::COMMAND, // Stickers StickerDec::COMMAND, StickerDelete::COMMAND, StickerFind::COMMAND, StickerGet::COMMAND, StickerInc::COMMAND, StickerList::COMMAND, StickerSet::COMMAND, StickerNames::COMMAND, StickerNamesTypes::COMMAND, StickerTypes::COMMAND, // Stored playlists ListPlaylist::COMMAND, ListPlaylistInfo::COMMAND, ListPlaylists::COMMAND, Load::COMMAND, PlaylistAdd::COMMAND, PlaylistClear::COMMAND, PlaylistDelete::COMMAND, PlaylistLength::COMMAND, PlaylistMove::COMMAND, Rename::COMMAND, Rm::COMMAND, Save::COMMAND, SearchPlaylist::COMMAND, ];