622 lines
20 KiB
Rust
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,
|
|
];
|