Files
empidee/src/commands.rs

622 lines
20 KiB
Rust

//! Module containing the basic building blocks, definitions and helpers
//! for implementing an MPD command. A 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};
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;
/// Converts this specific request type to it's corresponding variant in the generic Request enum.
fn into_request_enum(self) -> crate::Request;
/// Converts from the generic Request enum to this specific request type.
///
/// If the enum variant does not match this type, returns None.
fn from_request_enum(request: crate::Request) -> Option<Self>;
/// 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<Self, RequestParserError>;
/// 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<Self, RequestParserError> {
let (line, rest) = raw
.split_once('\n')
.ok_or(RequestParserError::UnexpectedEOF)?;
debug_assert!(rest.is_empty());
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,
{
/// Converts this specific response type to it's corresponding variant in the generic Response enum.
fn into_response_enum(self) -> crate::Response;
/// Converts from the generic Response enum to this specific response type.
///
/// If the enum variant does not match this type, returns None.
fn from_response_enum(response: crate::Response) -> Option<Self>;
// /// Serializes the response into a Vec<u8>.
// fn serialize(&self) -> Vec<u8>;
/// Parses the response from its tokenized parts.
/// See also [`parse_raw`].
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError>;
/// Parses the response from its raw byte representation.
fn parse_raw(raw: &[u8]) -> Result<Self, ResponseParserError> {
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;
/// Serialize the request into a string.
/// This should optimally produce an input that can be parsed by [`parse_request`]
fn serialize_request(&self, request: Self::Request) -> String {
request.serialize().to_owned()
}
/// Serialize the request into a bytestring.
fn serialize_request_to_bytes(&self, request: Self::Request) -> Vec<u8> {
self.serialize_request(request).into_bytes()
}
/// Parse the request from its tokenized parts. See also [`parse_raw_request`].
fn parse_request(parts: RequestTokenizer) -> Result<Self::Request, RequestParserError> {
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, RequestParserError> {
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, ResponseParserError> {
Self::Response::parse(parts)
}
/// Parse the raw response string into a response.
fn parse_raw_response(raw: &[u8]) -> Result<Self::Response, ResponseParserError> {
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 crate::commands::CommandRequest for paste::paste! { [<$name Request>] } {
const COMMAND: &'static str = $command_name;
fn into_request_enum(self) -> crate::Request {
match Self::COMMAND {
$command_name => crate::Request::$name,
_ => unimplemented!(),
}
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match (Self::COMMAND, request) {
($command_name, crate::Request::$name) => {
Some(paste::paste! { [<$name Request>] })
}
_ => None,
}
}
fn serialize(&self) -> String {
Self::COMMAND.to_string() + "\n"
}
fn parse(
mut parts: crate::commands::RequestTokenizer<'_>,
) -> Result<Self, crate::commands::RequestParserError> {
debug_assert!(parts.next().is_none());
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 crate::commands::CommandResponse for paste::paste! { [<$name Response>] } {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn from_response_enum(_response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(
_parts: crate::commands::ResponseAttributes<'_>,
) -> Result<Self, crate::commands::ResponseParserError> {
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>] ($item_type);
}
impl crate::commands::CommandRequest for paste::paste! { [<$name Request>] } {
const COMMAND: &'static str = $command_name;
fn into_request_enum(self) -> crate::Request {
match Self::COMMAND {
$command_name => crate::Request::$name(self.0),
_ => unimplemented!(),
}
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match (Self::COMMAND, request) {
($command_name, crate::Request::$name(item)) => {
Some(paste::paste! { [<$name Request>] ( item ) })
}
_ => None,
}
}
fn serialize(&self) -> String {
format!("{} {}\n", Self::COMMAND, self.0)
}
fn parse(
mut parts: crate::commands::RequestTokenizer<'_>,
) -> Result<Self, crate::commands::RequestParserError> {
let item_token = parts
.next()
.ok_or(crate::commands::RequestParserError::UnexpectedEOF)?;
let item = item_token.parse::<$item_type>().map_err(|_| {
crate::commands::RequestParserError::SyntaxError(0, item_token.to_owned())
})?;
debug_assert!(parts.next().is_none());
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>] (Option<$item_type>);
}
impl crate::commands::CommandRequest for paste::paste! { [<$name Request>] } {
const COMMAND: &'static str = $command_name;
fn into_request_enum(self) -> crate::Request {
match Self::COMMAND {
$command_name => crate::Request::$name(self.0),
_ => unimplemented!(),
}
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match (Self::COMMAND, request) {
($command_name, crate::Request::$name(item)) => {
Some(paste::paste! { [<$name Request>] ( item ) })
}
_ => None,
}
}
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<Self, crate::commands::RequestParserError> {
let item = parts
.next()
.map(|s| {
s.parse().map_err(|_| {
crate::commands::RequestParserError::SyntaxError(0, s.to_owned())
})
})
.transpose()?;
debug_assert!(parts.next().is_none());
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>] ( $item_type );
}
impl crate::commands::CommandResponse for paste::paste! { [<$name Response>] } {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn from_response_enum(_response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(
parts: crate::commands::ResponseAttributes<'_>,
) -> Result<Self, crate::commands::ResponseParserError> {
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>] ( Vec<$item_type> );
}
impl crate::commands::CommandResponse for paste::paste! { [<$name Response>] } {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn from_response_enum(_response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(
parts: crate::commands::ResponseAttributes<'_>,
) -> Result<Self, crate::commands::ResponseParserError> {
// 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(Debug, Clone, PartialEq)]
pub enum RequestParserError {
SyntaxError(u64, String),
MissingCommandListEnd(u64),
NestedCommandList(u64),
UnexpectedCommandListEnd(u64),
UnexpectedEOF,
MissingNewline,
}
pub type ResponseParserResult<'a, T> = Result<T, ResponseParserError>;
// TODO: should these be renamed to fit the mpd docs?
// "Attribute" instead of "Property"?
#[derive(Error, Debug, Clone, PartialEq)]
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,
// 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,
];