48 Commits

Author SHA1 Message Date
ba116b5d3e WIP: commands: split Command trait into req + res parts 2025-12-06 01:56:19 +09:00
07e1c76aa9 commands: parse to Self::Request
Some checks failed
Build and test / check (push) Failing after 43s
Build and test / build (push) Successful in 1m2s
Build and test / docs (push) Successful in 1m12s
Build and test / test (push) Successful in 1m44s
2025-12-05 22:00:11 +09:00
d84e653db2 filter: export all types from inner module 2025-12-05 21:00:01 +09:00
7834bbd956 common: don't expose types directly 2025-12-05 20:59:19 +09:00
249ffb2a36 commands: add docs for Command trait
Some checks failed
Build and test / check (push) Failing after 58s
Build and test / build (push) Successful in 1m10s
Build and test / docs (push) Successful in 1m10s
Build and test / test (push) Successful in 2m53s
2025-11-25 05:28:58 +09:00
fdd4880d05 common/types: add better alias for MountPath 2025-11-25 05:28:19 +09:00
9ca6544057 common/types: add better alias for AudioOutputId 2025-11-25 05:28:19 +09:00
e5f70ca87a common/types: add type for ChannelName 2025-11-25 05:28:19 +09:00
b03f60c985 common/types: add better alias for PlaylistVersion 2025-11-25 05:28:19 +09:00
b22016e970 common/types: add type for Sort 2025-11-25 03:56:01 +09:00
65c7798d01 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.
2025-11-24 23:53:03 +09:00
5188809327 commands: fix request de/serialization for list 2025-11-24 22:00:42 +09:00
a4276a2caa commands: remove some fixed TODOs 2025-11-24 21:59:42 +09:00
7b27a650a1 commands: split response tokenizer into separate file 2025-11-24 19:28:28 +09:00
062dbcafb8 filter: implement basic parser 2025-11-24 19:25:01 +09:00
57a6b0a3ee common/types: case insensitive tags 2025-11-24 19:25:00 +09:00
b5fbaadca2 Implement a proper request tokenizer 2025-11-24 19:25:00 +09:00
3bd7aaaad2 WIP: serialize requests 2025-11-21 18:50:58 +09:00
8aba3d8859 filter: implement fmt::Display 2025-11-21 18:50:58 +09:00
e4ece7d6b2 common/types: implement serialization helpers for Tag 2025-11-21 18:50:58 +09:00
f07f90aee5 common/types: implement fmt::Display 2025-11-21 18:50:58 +09:00
6e2aa2ba65 Cargo.toml: bump deps 2025-11-21 16:15:16 +09:00
1ef8ef669a flake.lock: bump 2025-11-21 16:14:13 +09:00
c6a123a6e1 commands: implement response parser for lsinfo 2025-11-21 16:13:09 +09:00
06e24f0ce0 cargo fmt + clippy 2025-11-21 16:04:46 +09:00
da31ab75e2 filter: add unit tests 2025-11-21 15:49:27 +09:00
d09ca013d5 commands: implement response parser for tagtypes available 2025-11-21 15:16:30 +09:00
10913fd48c commands: implement response parser for protocol 2025-11-21 15:14:43 +09:00
73ddb6d498 commands: implement response parser for protocol available 2025-11-21 15:12:21 +09:00
ede28623ef commands: return runtime errors on invalid property names 2025-11-21 15:07:50 +09:00
130fe49597 commands: create result struct for readmessages 2025-11-21 14:55:57 +09:00
4a1df97ad6 commands: precalculate capacity and use iterators 2025-11-21 14:44:17 +09:00
7a966051d5 commands: make better use of expect_property_type! 2025-11-21 14:38:09 +09:00
7dc3d7f9cf commands: implement response parser for listmounts 2025-11-21 14:32:06 +09:00
2b1e99445a commands: implement common traits for responses 2025-11-21 14:18:44 +09:00
e932b62195 commands: implement response parser for decoders 2025-11-21 14:14:25 +09:00
49f440770e commands: implement response parser for listneighbors 2025-11-21 14:04:12 +09:00
153ae9520f commands: implement response parser for outputs 2025-11-21 13:55:10 +09:00
424c530d5d common/types: add debug assertions for range orders 2025-10-13 15:35:56 +09:00
a637f24e66 flake.lock: bump, Cargo.toml: update inputs 2025-10-12 23:01:17 +09:00
1d693b7b2a commands: fix clippy warnings about confusing elided lifetimes 2025-10-12 22:59:50 +09:00
484b1fb68d flake.lock: bump, Cargo.toml: update inputs 2025-09-20 18:05:21 +02:00
2fa58533ba .gitignore: ignore Cargo.lock
This is okay because this is a library
2025-09-20 18:03:35 +02:00
088665c9ff flake.lock: bump
Some checks failed
Build and test / build (push) Successful in 45s
Build and test / check (push) Failing after 46s
Build and test / test (push) Successful in 1m11s
Build and test / docs (push) Successful in 1m37s
2025-08-03 05:01:37 +02:00
3e2e3fdc68 .gitea/workflows: update gitea-web target host
Some checks failed
Build and test / build (push) Failing after 1m17s
Build and test / check (push) Failing after 1m18s
Build and test / docs (push) Failing after 1m26s
Build and test / test (push) Successful in 1m31s
2025-08-03 04:50:54 +02:00
59994ce740 Cargo.toml: update deps
Some checks failed
Build and test / build (push) Successful in 1m29s
Build and test / check (push) Failing after 1m37s
Build and test / docs (push) Failing after 2m1s
Build and test / test (push) Failing after 2m29s
2025-07-11 20:37:16 +02:00
b42cad0b52 flake.lock: bump 2025-07-11 20:36:58 +02:00
3c49ece1a9 flake.nix: add cargo-edit to devshell 2025-07-11 20:36:50 +02:00
164 changed files with 6794 additions and 3039 deletions

View File

@@ -83,8 +83,8 @@ jobs:
target: ${{ gitea.ref_name }}/coverage/
username: gitea-web
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }}
host: bekkalokk.pvv.ntnu.no
known-hosts: "bekkalokk.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEI6VSaDrMG8+flg4/AeHlAFIen8RUzWh6URQKqFegSx"
host: pages.pvv.ntnu.no
known-hosts: "pages.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH2QjfFB+city1SYqltkVqWACfo1j37k+oQQfj13mtgg"
docs:
runs-on: ubuntu-latest
@@ -107,6 +107,6 @@ jobs:
target: ${{ gitea.ref_name }}/docs/
username: gitea-web
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }}
host: bekkalokk.pvv.ntnu.no
known-hosts: "bekkalokk.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEI6VSaDrMG8+flg4/AeHlAFIen8RUzWh6URQKqFegSx"
host: pages.pvv.ntnu.no
known-hosts: "pages.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH2QjfFB+city1SYqltkVqWACfo1j37k+oQQfj13mtgg"

4
.gitignore vendored
View File

@@ -1,3 +1,5 @@
/target
result
result-*
result-*
Cargo.lock

95
Cargo.lock generated
View File

@@ -1,95 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "empidee"
version = "0.1.0"
dependencies = [
"indoc",
"pretty_assertions",
"serde",
]
[[package]]
name = "indoc"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"

View File

@@ -11,8 +11,14 @@ edition = "2024"
rust-version = "1.85.0"
[dependencies]
serde = { version = "1.0.210", features = ["derive"] }
chrono = { version = "0.4.42", features = ["serde"] }
lalrpop-util = { version = "0.22.2", features = ["lexer"] }
paste = "1.0.15"
serde = { version = "1.0.228", features = ["derive"] }
[dev-dependencies]
indoc = "2.0.5"
indoc = "2.0.7"
pretty_assertions = "1.4.1"
[build-dependencies]
lalrpop = "0.22.2"

8
build.rs Normal file
View File

@@ -0,0 +1,8 @@
fn main() {
lalrpop::process_root().unwrap();
// let debug_mode = std::env::var("PROFILE").unwrap() == "debug";
// lalrpop::Configuration::new()
// .emit_comments(debug_mode)
// .process()
// .unwrap();
}

12
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1746461020,
"narHash": "sha256-7+pG1I9jvxNlmln4YgnlW4o+w0TZX24k688mibiFDUE=",
"lastModified": 1763421233,
"narHash": "sha256-Stk9ZYRkGrnnpyJ4eqt9eQtdFWRRIvMxpNRf4sIegnw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3730d8a308f94996a9ba7c7138ede69c1b9ac4ae",
"rev": "89c2b2330e733d6cdb5eae7b899326930c2c0648",
"type": "github"
},
"original": {
@@ -29,11 +29,11 @@
]
},
"locked": {
"lastModified": 1746585402,
"narHash": "sha256-Pf+ufu6bYNA1+KQKHnGMNEfTwpD9ZIcAeLoE2yPWIP0=",
"lastModified": 1763692705,
"narHash": "sha256-tCKCyMYU0Vy+ph/xswlNsYXXjnFVweWBV+ew/5FS9tA=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "72dd969389583664f87aa348b3458f2813693617",
"rev": "6fbf5d328dce1828d887b8ee7d44a785196a34e7",
"type": "github"
},
"original": {

View File

@@ -36,6 +36,7 @@
default = pkgs.mkShell {
nativeBuildInputs = [
toolchain
pkgs.cargo-edit
];
RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";

View File

@@ -1,9 +1,4 @@
use std::{
collections::{HashMap, HashSet},
str::SplitWhitespace,
};
use crate::Request;
use crate::{Request, request_tokenizer::RequestTokenizer, response_tokenizer::ResponseAttributes};
mod audio_output_devices;
mod client_to_client;
@@ -33,44 +28,342 @@ pub use reflection::*;
pub use stickers::*;
pub use stored_playlists::*;
pub trait Command {
type Response;
// The command name used within the protocol
/// A trait modelling a single MPD command request.
pub trait CommandRequest<'a>
where
Self: Sized,
{
/// The command name used within the protocol
const COMMAND: &'static str;
// TODO: `parse_request` should be using a more custom splitter, that can handle
// quoted strings and escape characters. This is what mpd uses to provide arguments
// with spaces and whatnot.
/// Converts this specific request type to it's corresponding variant in the generic Request enum.
fn into_request_enum(self) -> Request;
// A function to parse the remaining parts of the command, split by whitespace
fn parse_request(parts: SplitWhitespace<'_>) -> RequestParserResult<'_>;
/// 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: Request) -> Option<Self>;
fn parse_raw_request(raw: &str) -> RequestParserResult<'_> {
/// 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<'a>) -> 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: &'a str) -> Result<Self, RequestParserError> {
let (line, rest) = raw
.split_once('\n')
.ok_or(RequestParserError::UnexpectedEOF)?;
let mut parts = line.split_whitespace();
let command_name = parts
.next()
.ok_or(RequestParserError::SyntaxError(0, line.to_string()))?
.trim();
debug_assert!(rest.is_empty());
debug_assert!(command_name == Self::COMMAND);
let mut tokenized = RequestTokenizer::new(line);
Self::parse_request(parts).map(|(req, _)| (req, rest))
}
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(" ");
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>>;
if command_name != Self::COMMAND {
return Err(RequestParserError::SyntaxError(0, line.to_string()));
}
fn parse_raw_response(raw: &str) -> Result<Self::Response, ResponseParserError<'_>> {
Self::parse_response(ResponseAttributes::new(raw)?)
Self::parse(tokenized)
}
}
pub type RequestParserResult<'a> = Result<(Request, &'a str), RequestParserError>;
/// A trait modelling a single MPD command response.
pub trait CommandResponse<'a>
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<'a>) -> Result<Self, ResponseParserError<'a>>;
/// Parses the response from its raw byte representation.
fn parse_raw(raw: &'a [u8]) -> Result<Self, ResponseParserError<'a>> {
Self::parse(ResponseAttributes::new_from_bytes(raw))
}
}
/// A trait modelling the request/response pair of a single MPD command.
pub trait Command<'req, 'res> {
/// The request sent from the client to the server
type Request: CommandRequest<'req>;
/// The response sent from the server to the client
type Response: CommandResponse<'res>;
/// 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<'req>) -> 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: &'req 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<'res>,
) -> Result<Self::Response, ResponseParserError<'res>> {
Self::Response::parse(parts)
}
/// Parse the raw response string into a response.
fn parse_raw_response(raw: &'res [u8]) -> Result<Self::Response, ResponseParserError<'res>> {
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()
}
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!("{} {}", 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_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),
)?;
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, item_)
})?;
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(
mut parts: crate::commands::ResponseAttributes<'_>,
) -> Result<Self, crate::commands::ResponseParserError<'_>> {
let parts_: Vec<_> = parts.into_vec()?;
if let Some((k, _)) = parts_.iter().find(|(k, _)| *k != $item_name) {
return Err(ResponseParserError::UnexpectedProperty(k));
}
let mut items = Vec::with_capacity(parts_.len());
while let Some(value) = parts_.into_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,
unwrapped_value,
)
})?;
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;
///
#[derive(Debug, Clone, PartialEq)]
pub enum RequestParserError {
@@ -82,6 +375,8 @@ pub enum RequestParserError {
MissingNewline,
}
pub type ResponseParserResult<'a, T> = Result<T, ResponseParserError<'a>>;
// TODO: should these be renamed to fit the mpd docs?
// "Attribute" instead of "Property"?
#[derive(Debug, Clone, PartialEq)]
@@ -96,6 +391,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),
@@ -107,297 +405,151 @@ pub enum ResponseParserError<'a> {
// MissingNewline,
}
pub type GenericResponseResult<'a> = Result<GenericResponse<'a>, &'a str>;
pub type GenericResponse<'a> = HashMap<&'a str, GenericResponseValue<'a>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GenericResponseValue<'a> {
Text(&'a str),
Binary(&'a [u8]),
// Many(Vec<GenericResponseValue<'a>>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct ResponseAttributes<'a>(Vec<(&'a str, GenericResponseValue<'a>)>);
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;
}
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)));
}
Ok(parts.into())
}
// pub fn get<'a>(&self, key: &str) -> Option<&GenericResponseValue<'a>> {
// self.0.iter().find_map(|(k, v)| if k == &key { Some(v) } else { None })
// }
}
impl ResponseAttributes<'_> {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
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>> {
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()
}
}
impl<'a> From<ResponseAttributes<'a>> for Vec<(&'a str, GenericResponseValue<'a>)> {
fn from(val: ResponseAttributes<'a>) -> Self {
val.0
}
}
impl<'a> From<Vec<(&'a str, GenericResponseValue<'a>)>> for ResponseAttributes<'a> {
fn from(val: Vec<(&'a str, GenericResponseValue<'a>)>) -> Self {
Self(val)
}
}
// TODO: There should probably be a helper that lets you extract and verify one, two or maybe
// three properties without having to allocate a hashmap to get a nice API. We can retrieve
// the properties by name with a loop on the inner vec.
/*******************/
/* Parsing Helpers */
/*******************/
macro_rules! _expect_property_type {
($property:expr, $name:expr, $variant:ident) => {
match $property {
Some(crate::commands::GenericResponseValue::$variant(value)) => Some(value),
Some(value) => {
let actual_type = match value {
crate::commands::GenericResponseValue::Text(_) => "Text",
crate::commands::GenericResponseValue::Binary(_) => "Binary",
};
return Err(
crate::commands::ResponseParserError::UnexpectedPropertyType(
$name,
actual_type,
),
);
}
None => None,
}
};
}
macro_rules! _parse_optional_property_type {
($name:expr, $property:expr) => {
$property
.map(|value| {
value.parse().map_err(|_| {
crate::commands::ResponseParserError::InvalidProperty($name, value)
})
})
.transpose()?
};
}
macro_rules! _unwrap_optional_property_type {
($name:expr, $property:expr) => {
match $property {
Some(value) => value,
None => return Err(crate::commands::ResponseParserError::MissingProperty($name)),
}
};
}
macro_rules! expect_optional_property_type {
($property:expr, $name:expr, $variant:ident) => {
crate::commands::_expect_property_type!($property, $name, $variant)
};
}
macro_rules! expect_property_type {
($property:expr, $name:expr, $variant:ident) => {{
let prop = crate::commands::_expect_property_type!($property, $name, $variant);
crate::commands::_unwrap_optional_property_type!($name, prop)
}};
}
macro_rules! get_optional_property {
($parts:expr, $name:literal, $variant:ident) => {
crate::commands::_expect_property_type!({ $parts.get($name).map(|v| *v) }, $name, $variant)
};
}
macro_rules! get_property {
($parts:expr, $name:literal, $variant:ident) => {{
let prop = crate::commands::_expect_property_type!(
{ $parts.get($name).map(|v| *v) },
$name,
$variant
);
crate::commands::_unwrap_optional_property_type!($name, prop)
}};
}
macro_rules! get_and_parse_optional_property {
($parts:ident, $name:literal, $variant:ident) => {{
let prop = crate::commands::_expect_property_type!(
{ $parts.get($name).map(|v| *v) },
$name,
$variant
);
crate::commands::_parse_optional_property_type!($name, prop)
}};
}
macro_rules! get_and_parse_property {
($parts:ident, $name:literal, $variant:ident) => {{
let prop = crate::commands::_expect_property_type!(
{ $parts.get($name).map(|v| *v) },
$name,
$variant
);
let prop = crate::commands::_parse_optional_property_type!($name, prop);
crate::commands::_unwrap_optional_property_type!($name, prop)
}};
}
macro_rules! get_next_optional_property {
($parts:ident, $variant:ident) => {
match $parts.next() {
Some((name, value)) => {
crate::commands::_expect_property_type!({ Some(value) }, name, $variant)
.map(|value| (name, value))
}
None => None,
}
};
}
macro_rules! get_next_property {
($parts:ident, $variant:ident) => {
match $parts.next() {
Some((name, value)) => (
name,
crate::commands::_expect_property_type!({ Some(value) }, name, $variant).unwrap(),
),
None => return Err(crate::commands::ResponseParserError::UnexpectedEOF),
}
};
}
macro_rules! get_next_and_parse_optional_property {
($parts:ident, $variant:ident) => {
match $parts.next() {
Some((name, value)) => {
let prop = crate::commands::_expect_property_type!({ Some(value) }, name, $variant);
prop.map(|value| {
(
name,
crate::commands::_parse_optional_property_type!(name, value),
)
})
}
None => None,
}
};
}
macro_rules! get_next_and_parse_property {
($parts:ident, $variant:ident) => {
match $parts.next() {
Some((name, value)) => {
let prop = crate::commands::_expect_property_type!({ Some(value) }, name, $variant);
let prop = crate::commands::_parse_optional_property_type!(name, prop);
(
name,
crate::commands::_unwrap_optional_property_type!(name, prop),
)
}
None => return Err(crate::commands::ResponseParserError::UnexpectedEOF),
}
};
}
pub(crate) use _expect_property_type;
pub(crate) use _parse_optional_property_type;
pub(crate) use _unwrap_optional_property_type;
pub(crate) use expect_property_type;
// pub(crate) use expect_optional_property_type;
pub(crate) use get_and_parse_optional_property;
pub(crate) use get_and_parse_property;
// pub(crate) use get_next_and_parse_optional_property;
pub(crate) use get_next_and_parse_property;
// pub(crate) use get_next_optional_property;
pub(crate) use get_next_property;
pub(crate) use get_optional_property;
pub(crate) use get_property;
#[cfg(test)]
mod tests {
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();
assert_eq!(map.len(), 4);
}
#[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 map: HashMap<_, _> = invalid_maplike_attrs.into();
assert_eq!(map.len(), 4);
}
}
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,
];

View File

@@ -4,8 +4,8 @@ pub mod outputs;
pub mod outputset;
pub mod toggleoutput;
pub use disableoutput::DisableOutput;
pub use enableoutput::EnableOutput;
pub use outputs::Outputs;
pub use outputset::OutputSet;
pub use toggleoutput::ToggleOutput;
pub use disableoutput::*;
pub use enableoutput::*;
pub use outputs::*;
pub use outputset::*;
pub use toggleoutput::*;

View File

@@ -1,26 +1,15 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
common::types::AudioOutputId,
};
pub struct DisableOutput;
impl Command for DisableOutput {
type Response = ();
const COMMAND: &'static str = "disableoutput";
single_item_command_request!(DisableOutput, "disableoutput", AudioOutputId);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let output_id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
empty_command_response!(DisableOutput);
debug_assert!(parts.next().is_none());
Ok((Request::DisableOutput(output_id.to_string()), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for DisableOutput {
type Request = DisableOutputRequest;
type Response = DisableOutputResponse;
}

View File

@@ -1,26 +1,20 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError,
empty_command_response, single_item_command_request,
},
common::types::AudioOutputId,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct EnableOutput;
impl Command for EnableOutput {
type Response = ();
const COMMAND: &'static str = "enableoutput";
single_item_command_request!(EnableOutput, "enableoutput", AudioOutputId);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let output_id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
empty_command_response!(EnableOutput);
debug_assert!(parts.next().is_none());
Ok((Request::EnableOutput(output_id.to_string()), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for EnableOutput {
type Request = EnableOutputRequest;
type Response = EnableOutputResponse;
}

View File

@@ -2,15 +2,23 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
use crate::{
commands::{
Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError,
empty_command_request,
},
common::types::AudioOutputId,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
};
pub struct Outputs;
empty_command_request!(Outputs, "outputs");
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Output {
pub id: u64,
pub id: AudioOutputId,
pub name: String,
pub plugin: String,
pub enabled: bool,
@@ -19,18 +27,146 @@ pub struct Output {
pub type OutputsResponse = Vec<Output>;
impl Command for Outputs {
type Response = OutputsResponse;
const COMMAND: &'static str = "outputs";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Outputs, ""))
impl CommandResponse<'_> for OutputsResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
_parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
let mut outputs = Vec::new();
let mut id: Option<AudioOutputId> = None;
let mut name: Option<String> = None;
let mut plugin: Option<String> = None;
let mut enabled: Option<bool> = None;
let mut attributes: HashMap<String, String> = HashMap::new();
for (k, v) in parts.into_vec()?.into_iter() {
match k {
"outputid" => {
// Reset and store the previous output if all fields are present
if let (Some(id), Some(name), Some(plugin), Some(enabled)) =
(id.take(), name.take(), plugin.take(), enabled.take())
{
outputs.push(Output {
id,
name,
plugin,
enabled,
attribute: attributes,
});
}
attributes = HashMap::new();
let id_s = expect_property_type!(Some(v), k, Text);
id = Some(
id_s.parse()
.map_err(|_| ResponseParserError::SyntaxError(0, id_s))?,
);
}
"outputname" => {
name = Some(expect_property_type!(Some(v), k, Text).to_string());
}
"plugin" => {
plugin = Some(expect_property_type!(Some(v), k, Text).to_string());
}
"outputenabled" => {
let val_s = expect_property_type!(Some(v), k, Text);
let val: u64 = val_s
.parse::<u64>()
.map_err(|_| ResponseParserError::SyntaxError(0, val_s))?;
enabled = Some(val != 0);
}
"attribute" => {
let value = expect_property_type!(Some(v), k, Text);
let mut parts = value.splitn(2, '=');
let attr_key = parts
.next()
.ok_or(ResponseParserError::SyntaxError(0, value))?
.to_string();
let attr_value = parts
.next()
.ok_or(ResponseParserError::SyntaxError(0, value))?
.to_string();
attributes.insert(attr_key, attr_value);
}
_ => {
return Err(ResponseParserError::UnexpectedProperty(k));
}
}
}
// Store the last output if all fields are present
if let (Some(id), Some(name), Some(plugin), Some(enabled)) =
(id.take(), name.take(), plugin.take(), enabled.take())
{
outputs.push(Output {
id,
name,
plugin,
enabled,
attribute: attributes,
});
}
Ok(outputs)
}
}
impl Command<'_, '_> for Outputs {
type Request = OutputsRequest;
type Response = OutputsResponse;
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use super::*;
#[test]
fn test_parse_response() {
let input = indoc! {"
outputid: 0
outputname: PipeWire Sound Server
plugin: pipewire
outputenabled: 1
outputid: 1
outputname: Visualizer feed
plugin: fifo
outputenabled: 1
attribute: fifo_path=/tmp/empidee-visualizer.fifo
OK
"};
let result = Outputs::parse_raw_response(input.as_bytes());
assert_eq!(
result,
Ok(vec![
Output {
id: 0,
name: "PipeWire Sound Server".to_string(),
plugin: "pipewire".to_string(),
enabled: true,
attribute: HashMap::new(),
},
Output {
id: 1,
name: "Visualizer feed".to_string(),
plugin: "fifo".to_string(),
enabled: true,
attribute: {
let mut map = HashMap::new();
map.insert(
"fifo_path".to_string(),
"/tmp/empidee-visualizer.fifo".to_string(),
);
map
},
},
])
);
}
}

View File

@@ -1,35 +1,75 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError,
empty_command_response,
},
common::types::AudioOutputId,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct OutputSet;
impl Command for OutputSet {
type Response = ();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OutputSetRequest {
pub output_id: AudioOutputId,
pub attribute_name: String,
pub attribute_value: String,
}
impl CommandRequest<'_> for OutputSetRequest {
const COMMAND: &'static str = "outputset";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::OutputSet(self.output_id, self.attribute_name, self.attribute_value)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::OutputSet(output_id, attribute_name, attribute_value) => {
Some(OutputSetRequest {
output_id,
attribute_name,
attribute_value,
})
}
_ => None,
}
}
fn serialize(&self) -> String {
format!(
"{} {} {} {}",
Self::COMMAND,
self.output_id,
self.attribute_name,
self.attribute_value
)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let output_id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let output_id = output_id
.parse()
.map_err(|_| RequestParserError::SyntaxError(0, output_id.to_owned()))?;
let attribute_name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let attribute_value = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
debug_assert!(parts.next().is_none());
Ok((
Request::OutputSet(
output_id.to_string(),
attribute_name.to_string(),
attribute_value.to_string(),
),
"",
))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(OutputSetRequest {
output_id,
attribute_name: attribute_name.to_string(),
attribute_value: attribute_value.to_string(),
})
}
}
empty_command_response!(OutputSet);
impl Command<'_, '_> for OutputSet {
type Request = OutputSetRequest;
type Response = OutputSetResponse;
}

View File

@@ -1,26 +1,20 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError,
empty_command_response, single_item_command_request,
},
common::types::AudioOutputId,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct ToggleOutput;
impl Command for ToggleOutput {
type Response = ();
const COMMAND: &'static str = "toggleoutput";
single_item_command_request!(ToggleOutput, "toggleoutput", AudioOutputId);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let output_id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
empty_command_response!(ToggleOutput);
debug_assert!(parts.next().is_none());
Ok((Request::ToggleOutput(output_id.to_string()), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for ToggleOutput {
type Request = ToggleOutputRequest;
type Response = ToggleOutputResponse;
}

View File

@@ -1,36 +1,43 @@
use serde::{Deserialize, Serialize};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
expect_property_type,
use crate::{
commands::{
Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError,
empty_command_request,
},
common::types::ChannelName,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
};
pub struct Channels;
empty_command_request!(Channels, "channels");
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChannelsResponse {
pub channels: Vec<String>,
pub channels: Vec<ChannelName>,
}
impl Command for Channels {
type Response = ChannelsResponse;
const COMMAND: &'static str = "channels";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Channels, ""))
impl<'a> CommandResponse<'a> for ChannelsResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: Vec<_> = parts.into();
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into_vec()?;
let mut channel_names = Vec::with_capacity(parts.len());
for (key, value) in parts {
debug_assert!(key == "channels");
let channel_name = expect_property_type!(Some(value), "channels", Text);
channel_names.push(channel_name.to_string());
let channel_name = channel_name
.parse()
.map_err(|_| ResponseParserError::SyntaxError(0, channel_name))?;
channel_names.push(channel_name);
}
Ok(ChannelsResponse {
@@ -39,6 +46,11 @@ impl Command for Channels {
}
}
impl Command<'_, '_> for Channels {
type Request = ChannelsRequest;
type Response = ChannelsResponse;
}
#[cfg(test)]
mod tests {
use super::*;
@@ -53,11 +65,15 @@ mod tests {
channels: baz
OK
"};
let response = Channels::parse_raw_response(response).unwrap();
let response = Channels::parse_raw_response(response.as_bytes()).unwrap();
assert_eq!(
response,
ChannelsResponse {
channels: vec!["foo".to_string(), "bar".to_string(), "baz".to_string()]
channels: vec![
"foo".parse().unwrap(),
"bar".parse().unwrap(),
"baz".parse().unwrap(),
]
}
);
}

View File

@@ -1,31 +1,39 @@
use serde::{Deserialize, Serialize};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
expect_property_type,
use crate::{
commands::{
Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError,
empty_command_request,
},
common::types::ChannelName,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
};
pub struct ReadMessages;
empty_command_request!(ReadMessages, "readmessages");
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReadMessagesResponse {
pub messages: Vec<(String, String)>,
pub struct ReadMessagesResponse(Vec<ReadMessagesResponseEntry>);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReadMessagesResponseEntry {
channel: ChannelName,
message: String,
}
impl Command for ReadMessages {
type Response = ReadMessagesResponse;
const COMMAND: &'static str = "readmessages";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ReadMessages, ""))
impl CommandResponse<'_> for ReadMessagesResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: Vec<_> = parts.into();
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into_vec()?;
debug_assert!(parts.len() % 2 == 0);
let mut messages = Vec::with_capacity(parts.len() / 2);
@@ -37,16 +45,25 @@ impl Command for ReadMessages {
debug_assert!(ckey == "channel");
debug_assert!(mkey == "message");
let channel = expect_property_type!(Some(cvalue), "channel", Text).to_string();
let channel = expect_property_type!(Some(cvalue), "channel", Text);
let channel = channel
.parse()
.map_err(|_| ResponseParserError::SyntaxError(0, channel))?;
let message = expect_property_type!(Some(mvalue), "message", Text).to_string();
messages.push((channel, message));
messages.push(ReadMessagesResponseEntry { channel, message });
}
Ok(ReadMessagesResponse { messages })
Ok(ReadMessagesResponse(messages))
}
}
impl Command<'_, '_> for ReadMessages {
type Request = ReadMessagesRequest;
type Response = ReadMessagesResponse;
}
#[cfg(test)]
mod tests {
use indoc::indoc;
@@ -62,15 +79,19 @@ mod tests {
message: message2
OK
"};
let result = ReadMessages::parse_raw_response(input);
let result = ReadMessages::parse_raw_response(input.as_bytes());
assert_eq!(
result,
Ok(ReadMessagesResponse {
messages: vec![
("channel1".to_string(), "message1".to_string()),
("channel2".to_string(), "message2".to_string()),
]
})
Ok(ReadMessagesResponse(vec![
ReadMessagesResponseEntry {
channel: "channel1".parse().unwrap(),
message: "message1".to_string(),
},
ReadMessagesResponseEntry {
channel: "channel2".parse().unwrap(),
message: "message2".to_string(),
},
]))
);
}
}

View File

@@ -1,29 +1,60 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandRequest, CommandResponse, RequestParserError, empty_command_response,
},
common::types::ChannelName,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct SendMessage;
impl Command for SendMessage {
type Response = ();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SendMessageRequest {
pub channel: ChannelName,
pub message: String,
}
impl CommandRequest<'_> for SendMessageRequest {
const COMMAND: &'static str = "sendmessage";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::SendMessage(self.channel, self.message)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::SendMessage(channel, message) => {
Some(SendMessageRequest { channel, message })
}
_ => None,
}
}
fn serialize(&self) -> String {
format!("{} {} {}", Self::COMMAND, self.channel, self.message)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let channel = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let channel = channel
.parse()
.map_err(|_| RequestParserError::SyntaxError(0, channel.to_owned()))?;
// TODO: SplitWhitespace::remainder() is unstable, use when stable
let message = parts.collect::<Vec<_>>().join(" ");
debug_assert!(!message.is_empty());
Ok((Request::SendMessage(channel.to_string(), message), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(SendMessageRequest { channel, message })
}
}
empty_command_response!(SendMessage);
impl Command<'_, '_> for SendMessage {
type Request = SendMessageRequest;
type Response = SendMessageResponse;
}

View File

@@ -1,26 +1,20 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
common::types::ChannelName,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Subscribe;
impl Command for Subscribe {
type Response = ();
const COMMAND: &'static str = "subscribe";
single_item_command_request!(Subscribe, "subscribe", ChannelName);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let channel_name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
empty_command_response!(Subscribe);
debug_assert!(parts.next().is_none());
Ok((Request::Subscribe(channel_name.to_string()), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Subscribe {
type Request = SubscribeRequest;
type Response = SubscribeResponse;
}

View File

@@ -1,26 +1,19 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
single, single_item_command_request,
},
common::types::ChannelName,
request_tokenizer::RequestTokenizer,
};
pub struct Unsubscribe;
impl Command for Unsubscribe {
type Response = ();
const COMMAND: &'static str = "unsubscribe";
single_item_command_request!(Unsubscribe, "unsubscribe", ChannelName);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let channel_name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
empty_command_response!(Unsubscribe);
debug_assert!(parts.next().is_none());
Ok((Request::Unsubscribe(channel_name.to_string()), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Unsubscribe {
type Request = UnsubscribeRequest;
type Response = UnsubscribeResponse;
}

View File

@@ -1,27 +1,19 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response, single_item_command_request,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct BinaryLimit;
impl Command for BinaryLimit {
type Response = ();
const COMMAND: &'static str = "binarylimit";
single_item_command_request!(BinaryLimit, "binarylimit", u64);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let limit = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let limit = limit
.parse()
.map_err(|_| RequestParserError::SyntaxError(0, limit.to_string()))?;
debug_assert!(parts.next().is_none());
Ok((Request::BinaryLimit(limit), ""))
}
empty_command_response!(BinaryLimit);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for BinaryLimit {
type Request = BinaryLimitRequest;
type Response = BinaryLimitResponse;
}

View File

@@ -1,22 +1,19 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Close;
impl Command for Close {
type Response = ();
const COMMAND: &'static str = "close";
empty_command_request!(Close, "close");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Close, ""))
}
empty_command_response!(Close);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Close {
type Request = CloseRequest;
type Response = CloseResponse;
}

View File

@@ -1,22 +1,19 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Kill;
impl Command for Kill {
type Response = ();
const COMMAND: &'static str = "kill";
empty_command_request!(Kill, "kill");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Kill, ""))
}
empty_command_response!(Kill);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Kill {
type Request = KillRequest;
type Response = KillResponse;
}

View File

@@ -1,27 +1,19 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response, single_item_command_request,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Password;
impl Command for Password {
type Response = ();
const COMMAND: &'static str = "password";
single_item_command_request!(Password, "password", String);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let password = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
debug_assert!(parts.next().is_none());
Ok((Request::Password(password), ""))
}
empty_command_response!(Password);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Password {
type Request = PasswordRequest;
type Response = PasswordResponse;
}

View File

@@ -1,22 +1,19 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Ping;
impl Command for Ping {
type Response = ();
const COMMAND: &'static str = "ping";
empty_command_request!(Ping, "ping");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Ping, ""))
}
empty_command_response!(Ping);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Ping {
type Request = PingRequest;
type Response = PingResponse;
}

View File

@@ -1,22 +1,19 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{
Command, CommandResponse, RequestParserError, ResponseParserError, empty_command_request,
multi_item_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
};
pub struct Protocol;
impl Command for Protocol {
type Response = ();
const COMMAND: &'static str = "protocol";
empty_command_request!(Protocol, "protocol");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Protocol, ""))
}
multi_item_command_response!(Protocol, "feature", String);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
impl Command<'_, '_> for Protocol {
type Request = ProtocolRequest;
type Response = ProtocolResponse;
}

View File

@@ -1,23 +1,19 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct ProtocolAll;
impl Command for ProtocolAll {
type Response = ();
const COMMAND: &'static str = "protocol all";
empty_command_request!(ProtocolAll, "protocol all");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ProtocolAll, ""))
}
empty_command_response!(ProtocolAll);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for ProtocolAll {
type Request = ProtocolAllRequest;
type Response = ProtocolAllResponse;
}

View File

@@ -1,22 +1,19 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
multi_item_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
};
pub struct ProtocolAvailable;
impl Command for ProtocolAvailable {
type Response = ();
const COMMAND: &'static str = "protocol available";
empty_command_request!(ProtocolAvailable, "protocol available");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ProtocolAvailable, ""))
}
multi_item_command_response!(ProtocolAvailable, "feature", String);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
impl Command<'_, '_> for ProtocolAvailable {
type Request = ProtocolAvailableRequest;
type Response = ProtocolAvailableResponse;
}

View File

@@ -1,23 +1,19 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct ProtocolClear;
impl Command for ProtocolClear {
type Response = ();
const COMMAND: &'static str = "protocol clear";
empty_command_request!(ProtocolClear, "protocol clear");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ProtocolClear, ""))
}
empty_command_response!(ProtocolClear);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for ProtocolClear {
type Request = ProtocolClearRequest;
type Response = ProtocolClearResponse;
}

View File

@@ -1,35 +1,61 @@
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
common::types::Feature,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct ProtocolDisable;
impl Command for ProtocolDisable {
type Response = ();
pub struct ProtocolDisableRequest(Vec<Feature>);
impl CommandRequest<'_> for ProtocolDisableRequest {
const COMMAND: &'static str = "protocol disable";
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::ProtocolDisable(self.0)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::ProtocolDisable(features) => Some(ProtocolDisableRequest(features)),
_ => None,
}
}
fn serialize(&self) -> String {
let features = self
.0
.iter()
.map(|f| f.to_string())
.collect::<Vec<String>>()
.join(" ");
format!("{} {}", Self::COMMAND, features)
}
fn parse(parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let mut parts = parts.peekable();
if parts.peek().is_none() {
return Err(RequestParserError::UnexpectedEOF);
}
// TODO: verify that the features are split by whitespace
let mut features = Vec::new();
for feature in parts {
features.push(feature.to_string());
for part in parts {
let feature = part
.parse()
.map_err(|_| RequestParserError::SyntaxError(1, part.to_owned()))?;
features.push(feature);
}
Ok((Request::ProtocolDisable(features), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(ProtocolDisableRequest(features))
}
}
empty_command_response!(ProtocolDisable);
impl Command<'_, '_> for ProtocolDisable {
type Request = ProtocolDisableRequest;
type Response = ProtocolDisableResponse;
}

View File

@@ -1,35 +1,61 @@
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
common::types::Feature,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct ProtocolEnable;
impl Command for ProtocolEnable {
type Response = ();
pub struct ProtocolEnableRequest(Vec<Feature>);
impl CommandRequest<'_> for ProtocolEnableRequest {
const COMMAND: &'static str = "protocol enable";
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::ProtocolEnable(self.0)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::ProtocolEnable(features) => Some(ProtocolEnableRequest(features)),
_ => None,
}
}
fn serialize(&self) -> String {
let features = self
.0
.iter()
.map(|f| f.to_string())
.collect::<Vec<String>>()
.join(" ");
format!("{} {}", Self::COMMAND, features)
}
fn parse(parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let mut parts = parts.peekable();
if parts.peek().is_none() {
return Err(RequestParserError::UnexpectedEOF);
}
// TODO: verify that the features are split by whitespace
let mut features = Vec::new();
for feature in parts {
features.push(feature.to_string());
for part in parts {
let feature = part
.parse()
.map_err(|_| RequestParserError::SyntaxError(1, part.to_owned()))?;
features.push(feature);
}
Ok((Request::ProtocolEnable(features), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(ProtocolEnableRequest(features))
}
}
empty_command_response!(ProtocolEnable);
impl Command<'_, '_> for ProtocolEnable {
type Request = ProtocolEnableRequest;
type Response = ProtocolEnableResponse;
}

View File

@@ -1,42 +1,19 @@
use crate::commands::{
Command, GenericResponseValue, Request, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
multi_item_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
};
pub struct TagTypes;
pub type TagTypesResponse = Vec<String>;
empty_command_request!(TagTypes, "tagtypes");
impl Command for TagTypes {
multi_item_command_response!(TagTypes, "tagtype", String);
impl Command<'_, '_> for TagTypes {
type Request = TagTypesRequest;
type Response = TagTypesResponse;
const COMMAND: &'static str = "tagtypes";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::TagTypes, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: Vec<_> = parts.into();
let mut tagtypes = Vec::with_capacity(parts.len());
for (key, value) in parts.into_iter() {
debug_assert_eq!(key, "tagtype");
let tagtype = match value {
GenericResponseValue::Text(name) => name.to_string(),
GenericResponseValue::Binary(_) => {
return Err(ResponseParserError::UnexpectedPropertyType(
"tagtype", "Binary",
));
}
};
tagtypes.push(tagtype);
}
Ok(tagtypes)
}
}

View File

@@ -1,23 +1,19 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct TagTypesAll;
impl Command for TagTypesAll {
type Response = ();
const COMMAND: &'static str = "tagtypes all";
empty_command_request!(TagTypesAll, "tagtypes all");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::TagTypesAll, ""))
}
empty_command_response!(TagTypesAll);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for TagTypesAll {
type Request = TagTypesAllRequest;
type Response = TagTypesAllResponse;
}

View File

@@ -1,22 +1,19 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
multi_item_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
};
pub struct TagTypesAvailable;
impl Command for TagTypesAvailable {
type Response = ();
const COMMAND: &'static str = "tagtypes available";
empty_command_request!(TagTypesAvailable, "tagtypes available");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::TagTypesAvailable, ""))
}
multi_item_command_response!(TagTypesAvailable, "tagtype", String);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
impl Command<'_, '_> for TagTypesAvailable {
type Request = TagTypesAvailableRequest;
type Response = TagTypesAvailableResponse;
}

View File

@@ -1,23 +1,19 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct TagTypesClear;
impl Command for TagTypesClear {
type Response = ();
const COMMAND: &'static str = "tagtypes clear";
empty_command_request!(TagTypesClear, "tagtypes clear");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::TagTypesClear, ""))
}
empty_command_response!(TagTypesClear);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for TagTypesClear {
type Request = TagTypesClearRequest;
type Response = TagTypesClearResponse;
}

View File

@@ -1,35 +1,62 @@
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
common::types::TagName,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct TagTypesDisable;
impl Command for TagTypesDisable {
type Response = ();
pub struct TagTypesDisableRequest(Vec<TagName>);
impl CommandRequest<'_> for TagTypesDisableRequest {
const COMMAND: &'static str = "tagtypes disable";
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::TagTypesDisable(self.0)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::TagTypesDisable(req) => Some(TagTypesDisableRequest(req)),
_ => None,
}
}
fn serialize(&self) -> String {
format!(
"{} {}",
Self::COMMAND,
self.0
.iter()
.map(|tag| tag.as_str())
.collect::<Vec<&str>>()
.join(" ")
)
}
fn parse(parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let mut parts = parts.peekable();
if parts.peek().is_none() {
return Err(RequestParserError::UnexpectedEOF);
}
// TODO: verify that the tag types are split by whitespace
let mut tag_types = Vec::new();
for tag_type in parts {
tag_types.push(tag_type.to_string());
}
let tag_types = parts
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))
})
.collect::<Result<Vec<TagName>, RequestParserError>>()?;
Ok((Request::TagTypesDisable(tag_types), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(TagTypesDisableRequest(tag_types))
}
}
empty_command_response!(TagTypesDisable);
impl Command<'_, '_> for TagTypesDisable {
type Request = TagTypesDisableRequest;
type Response = TagTypesDisableResponse;
}

View File

@@ -1,35 +1,62 @@
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
common::types::TagName,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct TagTypesEnable;
impl Command for TagTypesEnable {
type Response = ();
pub struct TagTypesEnableRequest(Vec<TagName>);
impl CommandRequest<'_> for TagTypesEnableRequest {
const COMMAND: &'static str = "tagtypes enable";
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::TagTypesEnable(self.0)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::TagTypesEnable(req) => Some(TagTypesEnableRequest(req)),
_ => None,
}
}
fn serialize(&self) -> String {
format!(
"{} {}",
Self::COMMAND,
self.0
.iter()
.map(|tag| tag.as_str())
.collect::<Vec<&str>>()
.join(" ")
)
}
fn parse(parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let mut parts = parts.peekable();
if parts.peek().is_none() {
return Err(RequestParserError::UnexpectedEOF);
}
// TODO: verify that the tag types are split by whitespace
let mut tag_types = Vec::new();
for tag_type in parts {
tag_types.push(tag_type.to_string());
}
let tag_types = parts
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))
})
.collect::<Result<Vec<TagName>, RequestParserError>>()?;
Ok((Request::TagTypesEnable(tag_types), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(TagTypesEnableRequest(tag_types))
}
}
empty_command_response!(TagTypesEnable);
impl Command<'_, '_> for TagTypesEnable {
type Request = TagTypesEnableRequest;
type Response = TagTypesEnableResponse;
}

View File

@@ -1,35 +1,62 @@
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
common::types::TagName,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct TagTypesReset;
impl Command for TagTypesReset {
type Response = ();
pub struct TagTypesResetRequest(Vec<TagName>);
impl CommandRequest<'_> for TagTypesResetRequest {
const COMMAND: &'static str = "tagtypes reset";
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::TagTypesReset(self.0)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::TagTypesReset(req) => Some(TagTypesResetRequest(req)),
_ => None,
}
}
fn serialize(&self) -> String {
format!(
"{} {}",
Self::COMMAND,
self.0
.iter()
.map(|tag| tag.as_str())
.collect::<Vec<&str>>()
.join(" ")
)
}
fn parse(parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let mut parts = parts.peekable();
if parts.peek().is_none() {
return Err(RequestParserError::UnexpectedEOF);
}
// TODO: verify that the tag types are split by whitespace
let mut tag_types = Vec::new();
for tag_type in parts {
tag_types.push(tag_type.to_string());
}
let tag_types = parts
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))
})
.collect::<Result<Vec<TagName>, RequestParserError>>()?;
Ok((Request::TagTypesReset(tag_types), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(TagTypesResetRequest(tag_types))
}
}
empty_command_response!(TagTypesReset);
impl Command<'_, '_> for TagTypesReset {
type Request = TagTypesResetRequest;
type Response = TagTypesResetResponse;
}

View File

@@ -1,22 +1,19 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Next;
impl Command for Next {
type Response = ();
const COMMAND: &'static str = "next";
empty_command_request!(Next, "next");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Next, ""))
}
empty_command_response!(Next);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Next {
type Request = NextRequest;
type Response = NextResponse;
}

View File

@@ -1,31 +1,54 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Pause;
impl Command for Pause {
type Response = ();
pub struct PauseRequest(Option<bool>);
impl CommandRequest<'_> for PauseRequest {
const COMMAND: &'static str = "pause";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::Pause(self.0)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Pause(value) => Some(PauseRequest(value)),
_ => None,
}
}
fn serialize(&self) -> String {
match self.0 {
Some(true) => format!("{} 1", Self::COMMAND),
Some(false) => format!("{} 0", Self::COMMAND),
None => Self::COMMAND.to_string(),
}
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let result = match parts.next() {
Some("0") => Ok((Request::Pause(Some(false)), "")),
Some("1") => Ok((Request::Pause(Some(true)), "")),
Some("0") => Ok(Some(false)),
Some("1") => Ok(Some(true)),
Some(s) => Err(RequestParserError::SyntaxError(0, s.to_string())),
None => Ok((Request::Pause(None), "")),
None => Ok(None),
};
debug_assert!(parts.next().is_none());
result
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
result.map(PauseRequest)
}
}
empty_command_response!(Pause);
impl Command<'_, '_> for Pause {
type Request = PauseRequest;
type Response = PauseResponse;
}

View File

@@ -1,34 +1,20 @@
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
common::SongPosition,
common::types::SongPosition,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Play;
impl Command for Play {
type Response = ();
const COMMAND: &'static str = "play";
single_item_command_request!(Play, "play", SongPosition);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let songpos = match parts.next() {
Some(s) => s
.parse::<SongPosition>()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
empty_command_response!(Play);
debug_assert!(parts.next().is_none());
Ok((Request::Play(songpos), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Play {
type Request = PlayRequest;
type Response = PlayResponse;
}

View File

@@ -1,34 +1,20 @@
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response, single_item_command_request,
},
common::SongId,
common::types::SongId,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct PlayId;
impl Command for PlayId {
type Response = ();
const COMMAND: &'static str = "playid";
single_item_command_request!(PlayId, "playid", SongId);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let songid = match parts.next() {
Some(s) => s
.parse::<SongId>()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
empty_command_response!(PlayId);
debug_assert!(parts.next().is_none());
Ok((Request::PlayId(songid), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for PlayId {
type Request = PlayIdRequest;
type Response = PlayIdResponse;
}

View File

@@ -1,22 +1,19 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Previous;
impl Command for Previous {
type Response = ();
const COMMAND: &'static str = "previous";
empty_command_request!(Previous, "previous");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Previous, ""))
}
empty_command_response!(Previous);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Previous {
type Request = PreviousRequest;
type Response = PreviousResponse;
}

View File

@@ -1,18 +1,41 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
common::{SongPosition, TimeWithFractions},
common::types::{SongPosition, TimeWithFractions},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Seek;
impl Command for Seek {
type Response = ();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SeekRequest {
pub songpos: SongPosition,
pub time: TimeWithFractions,
}
impl CommandRequest<'_> for SeekRequest {
const COMMAND: &'static str = "seek";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::Seek(self.songpos, self.time)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Seek(songpos, time) => Some(SeekRequest { songpos, time }),
_ => None,
}
}
fn serialize(&self) -> String {
format!("{} {} {}", Self::COMMAND, self.songpos, self.time)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let songpos = match parts.next() {
Some(s) => s
.parse::<SongPosition>()
@@ -29,13 +52,13 @@ impl Command for Seek {
debug_assert!(parts.next().is_none());
Ok((Request::Seek(songpos, time), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(SeekRequest { songpos, time })
}
}
empty_command_response!(Seek);
impl Command<'_, '_> for Seek {
type Request = SeekRequest;
type Response = SeekResponse;
}

View File

@@ -1,18 +1,48 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseAttributes, ResponseParserError,
empty_command_response,
},
common::{SeekMode, TimeWithFractions},
common::types::{SeekMode, TimeWithFractions},
request_tokenizer::RequestTokenizer,
};
pub struct SeekCur;
impl Command for SeekCur {
type Response = ();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SeekCurRequest {
pub mode: SeekMode,
pub time: TimeWithFractions,
}
impl CommandRequest<'_> for SeekCurRequest {
const COMMAND: &'static str = "seekcur";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::SeekCur(self.mode, self.time)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::SeekCur(mode, time) => Some(SeekCurRequest { mode, time }),
_ => None,
}
}
fn serialize(&self) -> String {
let time_str = match self.mode {
SeekMode::Absolute => format!("{}", self.time),
SeekMode::Relative if self.time >= 0.0 => format!("+{}", self.time),
SeekMode::Relative => format!("-{}", -self.time),
SeekMode::RelativeReverse => unimplemented!(), // TODO: should this happen?
};
format!("{} {}", Self::COMMAND, time_str)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let time_raw = match parts.next() {
Some(t) => t,
None => return Err(RequestParserError::UnexpectedEOF),
@@ -41,13 +71,13 @@ impl Command for SeekCur {
debug_assert!(parts.next().is_none());
Ok((Request::SeekCur(mode, time), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(SeekCurRequest { mode, time })
}
}
empty_command_response!(SeekCur);
impl Command<'_, '_> for SeekCur {
type Request = SeekCurRequest;
type Response = SeekCurResponse;
}

View File

@@ -1,18 +1,41 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
common::{SongId, TimeWithFractions},
common::types::{SongId, TimeWithFractions},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct SeekId;
impl Command for SeekId {
type Response = ();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SeekIdRequest {
pub songid: SongId,
pub time: TimeWithFractions,
}
impl CommandRequest<'_> for SeekIdRequest {
const COMMAND: &'static str = "seekid";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::SeekId(self.songid, self.time)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::SeekId(songid, time) => Some(SeekIdRequest { songid, time }),
_ => None,
}
}
fn serialize(&self) -> String {
format!("{} {} {}", Self::COMMAND, self.songid, self.time)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let songid = match parts.next() {
Some(s) => s
.parse::<SongId>()
@@ -29,13 +52,13 @@ impl Command for SeekId {
debug_assert!(parts.next().is_none());
Ok((Request::SeekId(songid, time), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(SeekIdRequest { songid, time })
}
}
empty_command_response!(SeekId);
impl Command<'_, '_> for SeekId {
type Request = SeekIdRequest;
type Response = SeekIdResponse;
}

View File

@@ -1,22 +1,19 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Stop;
impl Command for Stop {
type Response = ();
const COMMAND: &'static str = "stop";
empty_command_request!(Stop, "stop");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Stop, ""))
}
empty_command_response!(Stop);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Stop {
type Request = StopRequest;
type Response = StopResponse;
}

View File

@@ -1,22 +1,43 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
multi_item_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
};
pub struct ListMounts;
impl Command for ListMounts {
type Response = Vec<(String, String)>;
const COMMAND: &'static str = "listmounts";
empty_command_request!(ListMounts, "listmounts");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ListMounts, ""))
}
multi_item_command_response!(ListMounts, "mount", String);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
impl Command<'_, '_> for ListMounts {
type Request = ListMountsRequest;
type Response = ListMountsResponse;
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use super::*;
#[test]
fn test_parse_response() {
let input = indoc! {"
mount:
mount: /mnt/music
OK
"};
let result = ListMounts::parse_raw_response(input.as_bytes());
assert_eq!(
result,
Ok(ListMountsResponse(vec![
"".to_string(),
"/mnt/music".to_string(),
]))
);
}
}

View File

@@ -1,22 +1,52 @@
use std::collections::HashMap;
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{
Command, CommandResponse, RequestParserError, ResponseParserError, empty_command_request,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
};
pub struct ListNeighbors;
impl Command for ListNeighbors {
type Response = Vec<(String, String)>;
const COMMAND: &'static str = "listneighbors";
empty_command_request!(ListNeighbors, "listneighbors");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ListNeighbors, ""))
pub struct ListNeighborsResponse(HashMap<String, String>);
impl CommandResponse<'_> for ListNeighborsResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into_vec()?;
debug_assert!(parts.len() % 2 == 0);
let mut result = HashMap::with_capacity(parts.len() / 2);
for channel_message_pair in parts.chunks_exact(2) {
let (neigh_key, neigh_value) = channel_message_pair[0];
let (name_key, name_value) = channel_message_pair[1];
debug_assert!(neigh_key == "neighbor");
debug_assert!(name_key == "name");
let neighbor = expect_property_type!(Some(neigh_value), "neighbor", Text).to_string();
let name = expect_property_type!(Some(name_value), "name", Text).to_string();
result.insert(neighbor, name);
}
Ok(ListNeighborsResponse(result))
}
}
impl Command<'_, '_> for ListNeighbors {
type Request = ListNeighborsRequest;
type Response = ListNeighborsResponse;
}

View File

@@ -1,21 +1,52 @@
use serde::{Deserialize, Serialize};
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response,
},
common::types::{MountPath, Uri},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Mount;
impl Command for Mount {
type Response = ();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MountRequest {
pub path: MountPath,
pub uri: Uri,
}
impl CommandRequest<'_> for MountRequest {
const COMMAND: &'static str = "mount";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let path = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
fn into_request_enum(self) -> crate::Request {
crate::Request::Mount(self.path, self.uri)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Mount(path, uri) => Some(MountRequest { path, uri }),
_ => None,
}
}
fn serialize(&self) -> String {
debug_assert!(!self.path.to_str().is_none());
format!(
"{} {} {}",
Self::COMMAND,
self.path.to_str().unwrap_or("<invalid path>"),
self.uri
)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let path = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let path = path
.parse()
.map_err(|_| RequestParserError::SyntaxError(0, path.to_string()))?;
let uri = parts
.next()
@@ -24,13 +55,13 @@ impl Command for Mount {
debug_assert!(parts.next().is_none());
Ok((Request::Mount(path, uri), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(MountRequest { path, uri })
}
}
empty_command_response!(Mount);
impl Command<'_, '_> for Mount {
type Request = MountRequest;
type Response = MountResponse;
}

View File

@@ -1,31 +1,60 @@
use std::path::PathBuf;
use crate::{
Request,
MountPath,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, CommandRequest, RequestParserError, RequestTokenizer, ResponseParserError,
empty_command_response, single_item_command_request,
},
response_tokenizer::ResponseAttributes,
};
pub struct Unmount;
impl Command for Unmount {
type Response = ();
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct UnmountRequest(MountPath);
impl CommandRequest<'_> for UnmountRequest {
const COMMAND: &'static str = "unmount";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let path = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
fn into_request_enum(self) -> crate::Request {
match Self::COMMAND {
"unmount" => crate::Request::Unmount(self.0),
_ => unimplemented!(),
}
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match (Self::COMMAND, request) {
("unmount", crate::Request::Unmount(item)) => Some(UnmountRequest(item)),
_ => None,
}
}
fn serialize(&self) -> String {
debug_assert!(!self.0.to_str().is_none());
format!(
"{} {}",
Self::COMMAND,
self.0.to_str().unwrap_or("<invalid path>")
)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let item_token = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let item = item_token
.parse::<MountPath>()
.map_err(|_| RequestParserError::SyntaxError(0, item_token.to_owned()))?;
debug_assert!(parts.next().is_none());
Ok((Request::Unmount(path), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(UnmountRequest(item))
}
}
empty_command_response!(Unmount);
impl Command<'_, '_> for Unmount {
type Request = UnmountRequest;
type Response = UnmountResponse;
}

View File

@@ -1,25 +1,41 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError, get_and_parse_property, get_property,
},
common::Offset,
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
common::types::{Offset, Uri},
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_and_parse_property, get_property},
};
pub struct AlbumArt;
pub struct AlbumArtResponse {
pub size: usize,
pub binary: Vec<u8>,
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AlbumArtRequest {
uri: Uri,
offset: Offset,
}
impl Command for AlbumArt {
type Response = AlbumArtResponse;
impl CommandRequest<'_> for AlbumArtRequest {
const COMMAND: &'static str = "albumart";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::AlbumArt(self.uri, self.offset)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::AlbumArt(uri, offset) => Some(AlbumArtRequest { uri, offset }),
_ => None,
}
}
fn serialize(&self) -> String {
format!("{} {} {}", Self::COMMAND, self.uri, self.offset)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let uri = match parts.next() {
Some(s) => s,
None => return Err(RequestParserError::UnexpectedEOF),
@@ -34,13 +50,30 @@ impl Command for AlbumArt {
debug_assert!(parts.next().is_none());
Ok((Request::AlbumArt(uri.to_string(), offset), ""))
Ok(AlbumArtRequest {
uri: uri.to_string(),
offset,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AlbumArtResponse {
pub size: usize,
pub binary: Vec<u8>,
}
impl CommandResponse<'_> for AlbumArtResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into_map()?;
let size = get_and_parse_property!(parts, "size", Text);
@@ -49,3 +82,8 @@ impl Command for AlbumArt {
Ok(AlbumArtResponse { size, binary })
}
}
impl Command<'_, '_> for AlbumArt {
type Request = AlbumArtRequest;
type Response = AlbumArtResponse;
}

View File

@@ -1,26 +1,52 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError, get_and_parse_property,
},
filter::parse_filter,
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
common::types::GroupType,
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_and_parse_property},
};
pub struct Count;
pub struct CountResponse {
pub songs: usize,
pub playtime: u64,
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CountRequest {
filter: Filter,
group: Option<GroupType>,
}
impl Command for Count {
type Response = CountResponse;
impl CommandRequest<'_> for CountRequest {
const COMMAND: &'static str = "count";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
fn into_request_enum(self) -> crate::Request {
crate::Request::Count(self.filter, self.group)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Count(filter, group) => Some(CountRequest { filter, group }),
_ => None,
}
}
fn serialize(&self) -> String {
let mut cmd = format!("{} {}", Self::COMMAND, self.filter);
if let Some(group) = self.group.as_ref() {
cmd.push_str(&format!(" group {}", group));
}
cmd
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let filter = match parts.next() {
Some(f) => {
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?
}
None => return Err(RequestParserError::UnexpectedEOF),
};
let group = if let Some("group") = parts.next() {
let group = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
@@ -36,13 +62,27 @@ impl Command for Count {
debug_assert!(parts.next().is_none());
Ok((Request::Count(filter, group), ""))
Ok(CountRequest { filter, group })
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CountResponse {
pub songs: usize,
pub playtime: u64,
}
impl CommandResponse<'_> for CountResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into_map()?;
let songs = get_and_parse_property!(parts, "songs", Text);
let playtime = get_and_parse_property!(parts, "playtime", Text);
@@ -50,3 +90,8 @@ impl Command for Count {
Ok(CountResponse { songs, playtime })
}
}
impl Command<'_, '_> for Count {
type Request = CountRequest;
type Response = CountResponse;
}

View File

@@ -1,30 +1,66 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
common::types::{Sort, WindowRange},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Find;
pub struct FindResponse {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FindRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
}
impl Command for Find {
type Response = FindResponse;
impl CommandRequest<'_> for FindRequest {
const COMMAND: &'static str = "find";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
fn into_request_enum(self) -> crate::Request {
crate::Request::Find(self.filter, self.sort, self.window)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Find(filter, sort, window) => Some(FindRequest {
filter,
sort,
window,
}),
_ => None,
}
}
fn serialize(&self) -> String {
let mut cmd = format!("{} {}", Self::COMMAND, self.filter);
if let Some(sort) = &self.sort {
cmd.push_str(&format!(" sort {}", sort));
}
if let Some(window) = &self.window {
cmd.push_str(&format!(" window {}", window));
}
cmd
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let filter = match parts.next() {
Some(f) => {
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?
}
None => return Err(RequestParserError::UnexpectedEOF),
};
let mut sort_or_window = parts.next();
let mut sort = None;
if let Some("sort") = sort_or_window {
let s = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
sort = Some(
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
s.parse()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_string()))?,
);
sort_or_window = parts.next();
}
@@ -40,12 +76,32 @@ impl Command for Find {
debug_assert!(parts.next().is_none());
Ok((Request::Find(filter, sort, window), ""))
Ok(FindRequest {
filter,
sort,
window,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FindResponse {}
impl CommandResponse<'_> for FindResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
unimplemented!()
}
}
impl Command<'_, '_> for Find {
type Request = FindRequest;
type Response = FindResponse;
}

View File

@@ -1,28 +1,73 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
filter::parse_filter,
common::types::{SongPosition, Sort, WindowRange},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct FindAdd;
impl Command for FindAdd {
type Response = ();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FindAddRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
}
impl CommandRequest<'_> for FindAddRequest {
const COMMAND: &'static str = "findadd";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
fn into_request_enum(self) -> crate::Request {
crate::Request::FindAdd(self.filter, self.sort, self.window, self.position)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::FindAdd(filter, sort, window, position) => Some(FindAddRequest {
filter,
sort,
window,
position,
}),
_ => None,
}
}
fn serialize(&self) -> String {
let mut cmd = format!("{} {}", Self::COMMAND, self.filter);
if let Some(sort) = &self.sort {
cmd.push_str(&format!(" sort {}", sort));
}
if let Some(window) = &self.window {
cmd.push_str(&format!(" window {}", window));
}
if let Some(position) = &self.position {
cmd.push_str(&format!(" position {}", position));
}
cmd
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let filter = match parts.next() {
Some(f) => {
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?
}
None => return Err(RequestParserError::UnexpectedEOF),
};
let mut sort_or_window_or_position = parts.next();
let mut sort = None;
if let Some("sort") = sort_or_window_or_position {
let s = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
sort = Some(
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
s.parse()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_string()))?,
);
sort_or_window_or_position = parts.next();
}
@@ -48,13 +93,18 @@ impl Command for FindAdd {
debug_assert!(parts.next().is_none());
Ok((Request::FindAdd(filter, sort, window, position), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(FindAddRequest {
filter,
sort,
window,
position,
})
}
}
empty_command_response!(FindAdd);
impl Command<'_, '_> for FindAdd {
type Request = FindAddRequest;
type Response = FindAddResponse;
}

View File

@@ -1,38 +1,45 @@
use std::collections::HashMap;
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError, get_and_parse_property,
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandResponse, RequestParserError, ResponseParserError,
single_item_command_request,
},
common::types::Uri,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_and_parse_property},
};
pub struct GetFingerprint;
single_item_command_request!(GetFingerprint, "getfingerprint", Uri);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GetFingerprintResponse {
pub chromaprint: String,
}
impl Command for GetFingerprint {
type Response = GetFingerprintResponse;
const COMMAND: &'static str = "getfingerprint";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let uri = uri
.parse()
.map_err(|_| RequestParserError::SyntaxError(1, uri.to_owned()))?;
debug_assert!(parts.next().is_none());
Ok((Request::GetFingerprint(uri), ""))
impl CommandResponse<'_> for GetFingerprintResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into_map()?;
let chromaprint = get_and_parse_property!(parts, "chromaprint", Text);
Ok(GetFingerprintResponse { chromaprint })
}
}
impl Command<'_, '_> for GetFingerprint {
type Request = GetFingerprintRequest;
type Response = GetFingerprintResponse;
}

View File

@@ -1,58 +1,141 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError, expect_property_type,
Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError,
multi_item_command_response,
},
filter::parse_filter,
common::types::{GroupType, TagName, WindowRange},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
};
pub struct List;
pub type ListResponse = Vec<String>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ListRequest {
tagname: TagName,
filter: Option<Filter>,
groups: Vec<GroupType>,
window: Option<WindowRange>,
}
impl Command for List {
type Response = ListResponse;
impl CommandRequest<'_> for ListRequest {
const COMMAND: &'static str = "list";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let tagtype = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let tagtype = tagtype
.parse()
.map_err(|_| RequestParserError::SyntaxError(1, tagtype.to_owned()))?;
fn into_request_enum(self) -> crate::Request {
crate::Request::List(self.tagname, self.filter, self.groups, self.window)
}
// TODO: This should be optional
let filter = parse_filter(&mut parts)?;
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::List(tagname, filter, groups, window) => Some(ListRequest {
tagname,
filter,
groups,
window,
}),
_ => None,
}
}
let group = if let Some("group") = parts.next() {
let group = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
Some(
group
.parse()
.map_err(|_| RequestParserError::SyntaxError(1, group.to_owned()))?,
)
} else {
None
fn serialize(&self) -> String {
let mut cmd = match &self.filter {
Some(f) => format!("{} {} {}", Self::COMMAND, self.tagname, f),
None => format!("{} {}", Self::COMMAND, self.tagname),
};
for group in &self.groups {
cmd.push_str(&format!(" group {}", group));
}
if let Some(window) = &self.window {
cmd.push_str(&format!(" window {}", window));
}
cmd
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let tagname = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let tagname = tagname
.parse()
.map_err(|_| RequestParserError::SyntaxError(1, tagname.to_owned()))?;
let mut filter = None;
let mut groups = Vec::new();
let mut window = None;
let mut next = parts.next();
if let Some(f) = next
&& f != "group"
&& f != "window"
{
let parsed_filter =
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?;
filter = Some(parsed_filter);
next = parts.next();
}
while let Some(g) = next
&& g == "group"
{
let group = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let parsed_group = group
.parse()
.map_err(|_| RequestParserError::SyntaxError(1, group.to_owned()))?;
groups.push(parsed_group);
next = parts.next();
}
if let Some(w) = next
&& w == "window"
{
let window_str = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let parsed_window = window_str
.parse()
.map_err(|_| RequestParserError::SyntaxError(1, window_str.to_owned()))?;
window = Some(parsed_window);
}
debug_assert!(parts.next().is_none());
Ok((Request::List(tagtype, filter, group), ""))
Ok(ListRequest {
tagname,
filter,
groups,
window,
})
}
}
pub struct ListResponse(Vec<String>);
impl CommandResponse<'_> for ListResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
let parts_: Vec<_> = parts.into_vec()?;
debug_assert!({
let key = parts.0.first().map(|(k, _)| k);
parts.0.iter().all(|(k, _)| k == key.unwrap())
let key = parts_.first().map(|(k, _)| k);
parts_.iter().all(|(k, _)| k == key.unwrap())
});
let list = parts
.0
let list = parts_
.into_iter()
.map(|(k, v)| Ok(expect_property_type!(Some(v), k, Text).to_string()))
.collect::<Result<Vec<_>, ResponseParserError>>()?;
Ok(list)
Ok(ListResponse(list))
}
}
impl Command<'_, '_> for List {
type Request = ListRequest;
type Response = ListResponse;
}

View File

@@ -1,6 +1,8 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, RequestParserError, ResponseParserError},
common::types::Uri,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct ListAll;
@@ -8,11 +10,20 @@ pub struct ListAll;
// TODO: This is supposed to be a tree-like structure, with directories containing files and playlists
pub type ListAllResponse = Vec<String>;
impl Command for ListAll {
impl Command<'_, '_> for ListAll {
type Request = Option<Uri>;
type Response = ListAllResponse;
const COMMAND: &'static str = "listall";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn serialize_request(&self, request: Self::Request) -> String {
if let Some(uri) = request {
format!("{} {}", Self::COMMAND, uri)
} else {
Self::COMMAND.to_string()
}
}
fn parse_request(mut parts: RequestTokenizer<'_>) -> Result<Self::Request, RequestParserError> {
let uri = parts
.next()
.map(|s| {
@@ -23,12 +34,12 @@ impl Command for ListAll {
debug_assert!(parts.next().is_none());
Ok((Request::ListAll(uri), ""))
Ok(uri)
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
) -> Result<Self::Response, ResponseParserError<'_>> {
unimplemented!()
}
}

View File

@@ -1,6 +1,8 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, RequestParserError, ResponseParserError},
common::types::Uri,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct ListAllInfo;
@@ -9,11 +11,20 @@ pub struct ListAllInfo;
// in addition to the metadata of each entry
pub type ListAllInfoResponse = Vec<String>;
impl Command for ListAllInfo {
impl Command<'_, '_> for ListAllInfo {
type Request = Option<Uri>;
type Response = ListAllInfoResponse;
const COMMAND: &'static str = "listallinfo";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn serialize_request(&self, request: Self::Request) -> String {
if let Some(uri) = request {
format!("{} {}", Self::COMMAND, uri)
} else {
Self::COMMAND.to_string()
}
}
fn parse_request(mut parts: RequestTokenizer<'_>) -> Result<Self::Request, RequestParserError> {
let uri = parts
.next()
.map(|s| {
@@ -24,12 +35,12 @@ impl Command for ListAllInfo {
debug_assert!(parts.next().is_none());
Ok((Request::ListAllInfo(uri), ""))
Ok(uri)
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
) -> Result<Self::Response, ResponseParserError<'_>> {
unimplemented!()
}
}

View File

@@ -1,6 +1,8 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, RequestParserError, ResponseParserError},
common::types::Uri,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct ListFiles;
@@ -8,11 +10,20 @@ pub struct ListFiles;
// TODO: fix this type
pub type ListFilesResponse = Vec<String>;
impl Command for ListFiles {
impl Command<'_, '_> for ListFiles {
type Request = Option<Uri>;
type Response = ListFilesResponse;
const COMMAND: &'static str = "listfiles";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn serialize_request(&self, request: Self::Request) -> String {
if let Some(uri) = request {
format!("{} {}", Self::COMMAND, uri)
} else {
Self::COMMAND.to_string()
}
}
fn parse_request(mut parts: RequestTokenizer<'_>) -> Result<Self::Request, RequestParserError> {
let uri = parts
.next()
.map(|s| {
@@ -23,12 +34,12 @@ impl Command for ListFiles {
debug_assert!(parts.next().is_none());
Ok((Request::ListFiles(uri), ""))
Ok(uri)
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
) -> Result<Self::Response, ResponseParserError<'_>> {
unimplemented!()
}
}

View File

@@ -1,18 +1,35 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, RequestParserError, ResponseParserError},
common::types::Uri,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
};
pub struct LsInfo;
// TODO: fix this type
pub type LsInfoResponse = Vec<String>;
pub type LsInfoResponse = Vec<LsInfoResponseEntry>;
impl Command for LsInfo {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LsInfoResponseEntry {
playlist: String,
last_modified: Option<String>,
}
impl Command<'_, '_> for LsInfo {
type Request = Option<Uri>;
type Response = LsInfoResponse;
const COMMAND: &'static str = "lsinfo";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn serialize_request(&self, request: Self::Request) -> String {
match request {
Some(uri) => format!("{} {}", Self::COMMAND, uri),
None => Self::COMMAND.to_string(),
}
}
fn parse_request(mut parts: RequestTokenizer<'_>) -> Result<Self::Request, RequestParserError> {
let uri = parts
.next()
.map(|s| {
@@ -23,12 +40,36 @@ impl Command for LsInfo {
debug_assert!(parts.next().is_none());
Ok((Request::LsInfo(uri), ""))
Ok(uri)
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into_vec()?;
let mut playlists = Vec::new();
for (key, value) in parts {
match key {
"playlist" => {
playlists.push(LsInfoResponseEntry {
playlist: expect_property_type!(Some(value), "playlist", Text).to_string(),
last_modified: None,
});
}
"Last-Modified" => {
if let Some(last) = playlists.last_mut() {
last.last_modified = Some(
expect_property_type!(Some(value), "Last-Modified", Text).to_string(),
);
} else {
return Err(ResponseParserError::UnexpectedProperty("Last-Modified"));
}
}
_ => return Err(ResponseParserError::UnexpectedProperty(key)),
}
}
Ok(playlists)
}
}

View File

@@ -1,19 +1,26 @@
use std::collections::HashMap;
use crate::commands::{
Command, GenericResponseValue, Request, RequestParserError, RequestParserResult,
ResponseAttributes, ResponseParserError,
use crate::{
commands::{Command, RequestParserError, ResponseParserError},
common::types::Uri,
request_tokenizer::RequestTokenizer,
response_tokenizer::{GenericResponseValue, ResponseAttributes},
};
pub struct ReadComments;
pub type ReadCommentsResponse = HashMap<String, String>;
impl Command for ReadComments {
impl Command<'_, '_> for ReadComments {
type Request = Uri;
type Response = ReadCommentsResponse;
const COMMAND: &'static str = "readcomments";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn serialize_request(&self, request: Self::Request) -> String {
format!("{} {}", Self::COMMAND, request)
}
fn parse_request(mut parts: RequestTokenizer<'_>) -> Result<Self::Request, RequestParserError> {
let uri = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let uri = uri
.parse()
@@ -21,13 +28,13 @@ impl Command for ReadComments {
debug_assert!(parts.next().is_none());
Ok((Request::ReadComments(uri), ""))
Ok(uri)
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into_map()?;
let comments = parts
.iter()

View File

@@ -1,26 +1,41 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError, get_and_parse_property, get_optional_property, get_property,
commands::{Command, RequestParserError, ResponseParserError},
common::types::{Offset, Uri},
request_tokenizer::RequestTokenizer,
response_tokenizer::{
ResponseAttributes, get_and_parse_property, get_optional_property, get_property,
},
common::Offset,
};
pub struct ReadPicture;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReadPictureRequest {
pub uri: Uri,
pub offset: Offset,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReadPictureResponse {
pub size: usize,
pub binary: Vec<u8>,
pub mimetype: Option<String>,
}
impl Command for ReadPicture {
impl Command<'_, '_> for ReadPicture {
type Request = ReadPictureRequest;
type Response = Option<ReadPictureResponse>;
const COMMAND: &'static str = "readpicture";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn serialize_request(&self, request: Self::Request) -> String {
format!("{} {} {}", Self::COMMAND, request.uri, request.offset)
}
fn parse_request(mut parts: RequestTokenizer<'_>) -> Result<Self::Request, RequestParserError> {
let uri = match parts.next() {
Some(s) => s,
None => return Err(RequestParserError::UnexpectedEOF),
@@ -35,13 +50,16 @@ impl Command for ReadPicture {
debug_assert!(parts.next().is_none());
Ok((Request::ReadPicture(uri.to_string(), offset), ""))
Ok(ReadPictureRequest {
uri: uri.to_string(),
offset,
})
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into_map()?;
if parts.is_empty() {
return Ok(None);

View File

@@ -1,32 +1,45 @@
use std::collections::HashMap;
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
get_and_parse_property,
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, RequestParserError, ResponseParserError},
common::types::Uri,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_and_parse_property},
};
pub struct Rescan;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RescanResponse {
pub updating_db: usize,
}
impl Command for Rescan {
impl Command<'_, '_> for Rescan {
type Request = Option<Uri>;
type Response = RescanResponse;
const COMMAND: &'static str = "rescan";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn serialize_request(&self, request: Self::Request) -> String {
match request {
Some(uri) => format!("{} {}", Self::COMMAND, uri),
None => Self::COMMAND.to_string(),
}
}
fn parse_request(mut parts: RequestTokenizer<'_>) -> Result<Self::Request, RequestParserError> {
let uri = parts.next().map(|s| s.to_string());
debug_assert!(parts.next().is_none());
Ok((Request::Rescan(uri), ""))
Ok(uri)
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into_map()?;
let updating_db = get_and_parse_property!(parts, "updating_db", Text);

View File

@@ -1,30 +1,56 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
commands::{Command, RequestParserError, ResponseParserError},
common::types::{Sort, WindowRange},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Search;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchResponse {}
impl Command for Search {
impl Command<'_, '_> for Search {
type Request = SearchRequest;
type Response = SearchResponse;
const COMMAND: &'static str = "search";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
fn serialize_request(&self, request: Self::Request) -> String {
let mut cmd = format!("{} {}", Self::COMMAND, request.filter);
if let Some(sort) = request.sort {
cmd.push_str(&format!(" sort {}", sort));
}
if let Some(window) = request.window {
cmd.push_str(&format!(" window {}", window));
}
cmd
}
fn parse_request(mut parts: RequestTokenizer<'_>) -> Result<Self::Request, RequestParserError> {
let filter = match parts.next() {
Some(f) => {
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?
}
None => return Err(RequestParserError::UnexpectedEOF),
};
let mut sort_or_window = parts.next();
let mut sort = None;
if let Some("sort") = sort_or_window {
let s = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
sort = Some(
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
s.parse()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_string()))?,
);
sort_or_window = parts.next();
}
@@ -40,12 +66,16 @@ impl Command for Search {
debug_assert!(parts.next().is_none());
Ok((Request::Search(filter, sort, window), ""))
Ok(SearchRequest {
filter,
sort,
window,
})
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
) -> Result<Self::Response, ResponseParserError<'_>> {
unimplemented!()
}
}

View File

@@ -1,28 +1,56 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
commands::{Command, RequestParserError, ResponseAttributes, ResponseParserError},
common::types::{SongPosition, Sort, WindowRange},
filter::Filter,
request_tokenizer::RequestTokenizer,
};
pub struct SearchAdd;
impl Command for SearchAdd {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchAddRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
}
impl Command<'_, '_> for SearchAdd {
type Request = SearchAddRequest;
type Response = ();
const COMMAND: &'static str = "searchadd";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
fn serialize_request(&self, request: Self::Request) -> String {
let mut cmd = format!("{} {}", Self::COMMAND, request.filter);
if let Some(sort) = request.sort {
cmd.push_str(&format!(" sort {}", sort));
}
if let Some(window) = request.window {
cmd.push_str(&format!(" window {}", window));
}
if let Some(position) = request.position {
cmd.push_str(&format!(" position {}", position));
}
cmd
}
fn parse_request(mut parts: RequestTokenizer<'_>) -> Result<Self::Request, RequestParserError> {
let filter = match parts.next() {
Some(f) => {
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?
}
None => return Err(RequestParserError::UnexpectedEOF),
};
let mut sort_or_window_or_position = parts.next();
let mut sort = None;
if let Some("sort") = sort_or_window_or_position {
let s = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
sort = Some(
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
s.parse()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_string()))?,
);
sort_or_window_or_position = parts.next();
}
@@ -48,12 +76,17 @@ impl Command for SearchAdd {
debug_assert!(parts.next().is_none());
Ok((Request::SearchAdd(filter, sort, window, position), ""))
Ok(SearchAddRequest {
filter,
sort,
window,
position,
})
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
) -> Result<Self::Response, ResponseParserError<'_>> {
debug_assert!(parts.is_empty());
Ok(())
}

View File

@@ -1,33 +1,67 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
commands::{Command, RequestParserError, ResponseAttributes, ResponseParserError},
common::types::{PlaylistName, SongPosition, Sort, WindowRange},
filter::Filter,
request_tokenizer::RequestTokenizer,
};
pub struct SearchAddPl;
impl Command for SearchAddPl {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchAddPlRequest {
playlist_name: PlaylistName,
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
}
impl Command<'_, '_> for SearchAddPl {
type Request = SearchAddPlRequest;
type Response = ();
const COMMAND: &'static str = "searchaddpl";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn serialize_request(&self, request: Self::Request) -> String {
let mut cmd = format!(
"{} {} {}",
Self::COMMAND,
request.playlist_name,
request.filter
);
if let Some(sort) = request.sort {
cmd.push_str(&format!(" sort {}", sort));
}
if let Some(window) = request.window {
cmd.push_str(&format!(" window {}", window));
}
if let Some(position) = request.position {
cmd.push_str(&format!(" position {}", position));
}
cmd
}
fn parse_request(mut parts: RequestTokenizer<'_>) -> Result<Self::Request, RequestParserError> {
let playlist_name = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
let filter = parse_filter(&mut parts)?;
let filter = match parts.next() {
Some(f) => {
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?
}
None => return Err(RequestParserError::UnexpectedEOF),
};
let mut sort_or_window_or_position = parts.next();
let mut sort = None;
if let Some("sort") = sort_or_window_or_position {
let s = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
sort = Some(
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
s.parse()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_string()))?,
);
sort_or_window_or_position = parts.next();
}
@@ -53,15 +87,18 @@ impl Command for SearchAddPl {
debug_assert!(parts.next().is_none());
Ok((
Request::SearchAddPl(playlist_name, filter, sort, window, position),
"",
))
Ok(SearchAddPlRequest {
playlist_name,
filter,
sort,
window,
position,
})
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
) -> Result<Self::Response, ResponseParserError<'_>> {
debug_assert!(parts.is_empty());
Ok(())
}

View File

@@ -1,26 +1,49 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError, get_and_parse_property,
},
filter::parse_filter,
commands::{Command, RequestParserError, ResponseParserError},
common::types::GroupType,
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_and_parse_property},
};
pub struct SearchCount;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchCountRequest {
filter: Filter,
group: Option<GroupType>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchCountResponse {
pub songs: usize,
pub playtime: u64,
}
impl Command for SearchCount {
impl Command<'_, '_> for SearchCount {
type Request = SearchCountRequest;
type Response = SearchCountResponse;
const COMMAND: &'static str = "searchcount";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
fn serialize_request(&self, request: Self::Request) -> String {
let mut cmd = format!("{} {}", Self::COMMAND, request.filter);
if let Some(group) = request.group {
cmd.push_str(&format!(" group {}", group));
}
cmd
}
fn parse_request(mut parts: RequestTokenizer<'_>) -> Result<Self::Request, RequestParserError> {
let filter = match parts.next() {
Some(f) => {
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?
}
None => return Err(RequestParserError::UnexpectedEOF),
};
let group = if let Some("group") = parts.next() {
let group = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
@@ -35,13 +58,13 @@ impl Command for SearchCount {
debug_assert!(parts.next().is_none());
Ok((Request::SearchCount(filter, group), ""))
Ok(SearchCountRequest { filter, group })
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
) -> Result<Self::Response, ResponseParserError<'_>> {
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

@@ -1,32 +1,45 @@
use std::collections::HashMap;
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
get_and_parse_property,
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, RequestParserError, ResponseParserError},
common::types::Uri,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_and_parse_property},
};
pub struct Update;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UpdateResponse {
updating_db: usize,
}
impl Command for Update {
impl Command<'_, '_> for Update {
type Request = Option<Uri>;
type Response = UpdateResponse;
const COMMAND: &'static str = "update";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn serialize_request(&self, request: Self::Request) -> String {
match request {
Some(uri) => format!("{} {}", Self::COMMAND, uri),
None => Self::COMMAND.to_string(),
}
}
fn parse_request(mut parts: RequestTokenizer<'_>) -> Result<Self::Request, RequestParserError> {
let uri = parts.next().map(|s| s.to_string());
debug_assert!(parts.next().is_none());
Ok((Request::Update(uri), ""))
Ok(uri)
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
) -> Result<Self::Response, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into_map()?;
let updating_db = get_and_parse_property!(parts, "updating_db", Text);

View File

@@ -1,29 +1,20 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
common::types::PartitionName,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct DelPartition;
impl Command for DelPartition {
type Response = ();
const COMMAND: &'static str = "delpartition";
single_item_command_request!(DelPartition, "delpartition", PartitionName);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let partition = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
empty_command_response!(DelPartition);
debug_assert!(parts.next().is_none());
Ok((Request::DelPartition(partition), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for DelPartition {
type Request = DelPartitionRequest;
type Response = DelPartitionResponse;
}

View File

@@ -1,33 +1,20 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
expect_property_type,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
multi_item_command_response, single_item_command_request,
},
common::types::PartitionName,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
};
pub struct ListPartitions;
pub type ListPartitionsResponse = Vec<String>;
empty_command_request!(ListPartitions, "listpartitions");
impl Command for ListPartitions {
multi_item_command_response!(ListPartitions, "partition", PartitionName);
impl Command<'_, '_> for ListPartitions {
type Request = ListPartitionsRequest;
type Response = ListPartitionsResponse;
const COMMAND: &'static str = "listpartitions";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ListPartitions, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: Vec<_> = parts.into();
let mut partitions = Vec::with_capacity(parts.len());
for (key, value) in parts.into_iter() {
debug_assert_eq!(key, "partition");
let partition = expect_property_type!(Some(value), "partition", Text).to_string();
partitions.push(partition);
}
Ok(partitions)
}
}

View File

@@ -1,29 +1,19 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct MoveOutput;
impl Command for MoveOutput {
type Response = ();
const COMMAND: &'static str = "moveoutput";
single_item_command_request!(MoveOutput, "moveoutput", String);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let output_name = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
empty_command_response!(MoveOutput);
debug_assert!(parts.next().is_none());
Ok((Request::MoveOutput(output_name), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for MoveOutput {
type Request = MoveOutputRequest;
type Response = MoveOutputResponse;
}

View File

@@ -1,29 +1,20 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response, single_item_command_request,
},
common::types::PartitionName,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct NewPartition;
impl Command for NewPartition {
type Response = ();
const COMMAND: &'static str = "newpartition";
single_item_command_request!(NewPartition, "newpartition", PartitionName);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let partition = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
empty_command_response!(NewPartition);
debug_assert!(parts.next().is_none());
Ok((Request::NewPartition(partition), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for NewPartition {
type Request = NewPartitionRequest;
type Response = NewPartitionResponse;
}

View File

@@ -1,29 +1,20 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
common::types::PartitionName,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Partition;
impl Command for Partition {
type Response = ();
const COMMAND: &'static str = "partition";
single_item_command_request!(Partition, "partition", PartitionName);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let partition = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
empty_command_response!(Partition);
debug_assert!(parts.next().is_none());
Ok((Request::Partition(partition), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Partition {
type Request = PartitionRequest;
type Response = PartitionResponse;
}

View File

@@ -1,32 +1,22 @@
use std::str::FromStr;
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
common::types::BoolOrOneshot,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Consume;
impl Command for Consume {
type Response = ();
const COMMAND: &'static str = "consume";
single_item_command_request!(Consume, "consume", BoolOrOneshot);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let state = match parts.next() {
Some(s) => crate::common::BoolOrOneshot::from_str(s)
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
empty_command_response!(Consume);
debug_assert!(parts.next().is_none());
Ok((Request::Consume(state), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Consume {
type Request = ConsumeRequest;
type Response = ConsumeResponse;
}

View File

@@ -1,34 +1,20 @@
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
common::Seconds,
common::types::Seconds,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Crossfade;
impl Command for Crossfade {
type Response = ();
const COMMAND: &'static str = "crossfade";
single_item_command_request!(Crossfade, "crossfade", Seconds);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let seconds = match parts.next() {
Some(s) => s
.parse::<Seconds>()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
empty_command_response!(Crossfade);
debug_assert!(parts.next().is_none());
Ok((Request::Crossfade(seconds), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Crossfade {
type Request = CrossfadeRequest;
type Response = CrossfadeResponse;
}

View File

@@ -2,29 +2,21 @@ use std::collections::HashMap;
use crate::{
commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
get_and_parse_property,
Command, RequestParserError, ResponseParserError, empty_command_request,
single_item_command_response,
},
common::VolumeValue,
common::types::VolumeValue,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_and_parse_property},
};
pub struct GetVol;
impl Command for GetVol {
type Response = VolumeValue;
const COMMAND: &'static str = "getvol";
empty_command_request!(GetVol, "getvol");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::GetVol, ""))
}
single_item_command_response!(GetVol, "volume", VolumeValue);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
assert_eq!(parts.len(), 1);
let volume = get_and_parse_property!(parts, "volume", Text);
Ok(volume)
}
impl Command<'_, '_> for GetVol {
type Request = GetVolRequest;
type Response = GetVolResponse;
}

View File

@@ -1,31 +1,19 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct MixRampDb;
impl Command for MixRampDb {
type Response = ();
const COMMAND: &'static str = "mixrampdb";
single_item_command_request!(MixRampDb, "mixrampdb", f32);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let db = match parts.next() {
Some(s) => s
.parse::<f32>()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
empty_command_response!(MixRampDb);
debug_assert!(parts.next().is_none());
Ok((Request::MixRampDb(db), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for MixRampDb {
type Request = MixRampDbRequest;
type Response = MixRampDbResponse;
}

View File

@@ -1,34 +1,19 @@
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
common::Seconds,
common::types::Seconds,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct MixRampDelay;
impl Command for MixRampDelay {
type Response = ();
const COMMAND: &'static str = "mixrampdelay";
single_item_command_request!(MixRampDelay, "mixrampdelay", Seconds);
empty_command_response!(MixRampDelay);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let seconds = match parts.next() {
Some(s) => s
.parse::<Seconds>()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
debug_assert!(parts.next().is_none());
Ok((Request::MixRampDelay(seconds), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for MixRampDelay {
type Request = MixRampDelayRequest;
type Response = MixRampDelayResponse;
}

View File

@@ -1,15 +1,35 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Random;
impl Command for Random {
type Response = ();
pub struct RandomRequest(bool);
impl CommandRequest<'_> for RandomRequest {
const COMMAND: &'static str = "random";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::Random(self.0)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Random(state) => Some(RandomRequest(state)),
_ => None,
}
}
fn serialize(&self) -> String {
let state = if self.0 { "1" } else { "0" };
format!("{} {}", Self::COMMAND, state)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let state = match parts.next() {
Some("0") => false,
Some("1") => true,
@@ -19,13 +39,13 @@ impl Command for Random {
debug_assert!(parts.next().is_none());
Ok((Request::Random(state), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(RandomRequest(state))
}
}
empty_command_response!(Random);
impl Command<'_, '_> for Random {
type Request = RandomRequest;
type Response = RandomResponse;
}

View File

@@ -1,15 +1,35 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Repeat;
impl Command for Repeat {
type Response = ();
pub struct RepeatRequest(bool);
impl CommandRequest<'_> for RepeatRequest {
const COMMAND: &'static str = "repeat";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::Repeat(self.0)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Repeat(state) => Some(RepeatRequest(state)),
_ => None,
}
}
fn serialize(&self) -> String {
let state = if self.0 { "1" } else { "0" };
format!("{} {}", Self::COMMAND, state)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let state = match parts.next() {
Some("0") => false,
Some("1") => true,
@@ -19,13 +39,13 @@ impl Command for Repeat {
debug_assert!(parts.next().is_none());
Ok((Request::Repeat(state), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(RepeatRequest(state))
}
}
empty_command_response!(Repeat);
impl Command<'_, '_> for Repeat {
type Request = RepeatRequest;
type Response = RepeatResponse;
}

View File

@@ -2,34 +2,21 @@ use std::str::FromStr;
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
common::ReplayGainModeMode,
common::types::ReplayGainModeMode,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct ReplayGainMode;
impl Command for ReplayGainMode {
type Response = ();
const COMMAND: &'static str = "replay_gain_mode";
single_item_command_request!(ReplayGainMode, "replay_gain_mode", ReplayGainModeMode);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let mode = match parts.next() {
Some(s) => ReplayGainModeMode::from_str(s)
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
empty_command_response!(ReplayGainMode);
debug_assert!(parts.next().is_none());
Ok((Request::ReplayGainMode(mode), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for ReplayGainMode {
type Request = ReplayGainModeRequest;
type Response = ReplayGainModeResponse;
}

View File

@@ -4,32 +4,33 @@ use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
get_property,
Command, CommandResponse, RequestParserError, ResponseParserError, empty_command_request,
},
common::ReplayGainModeMode,
common::types::ReplayGainModeMode,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_property},
};
pub struct ReplayGainStatus;
empty_command_request!(ReplayGainStatus, "replay_gain_status");
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReplayGainStatusResponse {
pub replay_gain_mode: ReplayGainModeMode,
}
impl Command for ReplayGainStatus {
type Response = ReplayGainStatusResponse;
const COMMAND: &'static str = "replay_gain_status";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ReplayGainStatus, ""))
impl CommandResponse<'_> for ReplayGainStatusResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into_map()?;
let replay_gain_mode = get_property!(parts, "replay_gain_mode", Text);
Ok(ReplayGainStatusResponse {
@@ -39,3 +40,8 @@ impl Command for ReplayGainStatus {
})
}
}
impl Command<'_, '_> for ReplayGainStatus {
type Request = ReplayGainStatusRequest;
type Response = ReplayGainStatusResponse;
}

View File

@@ -2,34 +2,21 @@ use std::str::FromStr;
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
common::VolumeValue,
common::types::VolumeValue,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct SetVol;
impl Command for SetVol {
type Response = ();
const COMMAND: &'static str = "setvol";
single_item_command_request!(SetVol, "setvol", VolumeValue);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let volume = match parts.next() {
Some(s) => VolumeValue::from_str(s)
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
empty_command_response!(SetVol);
debug_assert!(parts.next().is_none());
Ok((Request::SetVol(volume), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for SetVol {
type Request = SetVolRequest;
type Response = SetVolResponse;
}

View File

@@ -1,32 +1,22 @@
use std::str::FromStr;
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
common::types::BoolOrOneshot,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Single;
impl Command for Single {
type Response = ();
const COMMAND: &'static str = "single";
single_item_command_request!(Single, "single", BoolOrOneshot);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let state = match parts.next() {
Some(s) => crate::common::BoolOrOneshot::from_str(s)
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
empty_command_response!(Single);
debug_assert!(parts.next().is_none());
Ok((Request::Single(state), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Single {
type Request = SingleRequest;
type Response = SingleResponse;
}

View File

@@ -2,34 +2,21 @@ use std::str::FromStr;
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
common::VolumeValue,
common::types::VolumeValue,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Volume;
impl Command for Volume {
type Response = ();
const COMMAND: &'static str = "volume";
single_item_command_request!(Volume, "volume", VolumeValue);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let change = match parts.next() {
Some(s) => VolumeValue::from_str(s)
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
empty_command_response!(Volume);
debug_assert!(parts.next().is_none());
Ok((Request::Volume(change), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Volume {
type Request = VolumeRequest;
type Response = VolumeResponse;
}

View File

@@ -1,24 +1,20 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
use crate::{
commands::{
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
/// Clears the current error message in status (this is also accomplished by any command that starts playback)
pub struct ClearError;
impl Command for ClearError {
type Response = ();
const COMMAND: &'static str = "clearerror";
empty_command_request!(ClearError, "clearerror");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
empty_command_response!(ClearError);
Ok((Request::ClearError, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for ClearError {
type Request = ClearErrorRequest;
type Response = ClearErrorResponse;
}

View File

@@ -1,28 +1,36 @@
use serde::{Deserialize, Serialize};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
use crate::{
commands::{
Command, CommandResponse, RequestParserError, ResponseParserError, empty_command_request,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
/// Displays the song info of the current song (same song that is identified in status)
pub struct CurrentSong;
empty_command_request!(CurrentSong, "currentsong");
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CurrentSongResponse {}
impl Command for CurrentSong {
type Response = CurrentSongResponse;
const COMMAND: &'static str = "currentsong";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::CurrentSong, ""))
impl CommandResponse<'_> for CurrentSongResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
_parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
unimplemented!()
}
}
impl Command<'_, '_> for CurrentSong {
type Request = CurrentSongRequest;
type Response = CurrentSongResponse;
}

View File

@@ -1,37 +1,65 @@
use std::str::{FromStr, SplitWhitespace};
use std::str::FromStr;
use crate::common::SubSystem;
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
use crate::{
commands::{
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
common::types::SubSystem,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Idle;
impl Command for Idle {
type Response = ();
pub struct IdleRequest(Option<Vec<SubSystem>>);
impl CommandRequest<'_> for IdleRequest {
const COMMAND: &'static str = "idle";
fn parse_request(mut parts: SplitWhitespace<'_>) -> RequestParserResult<'_> {
let result = parts
.next()
.map_or(Ok((Request::Idle(None), "")), |subsystems| {
let subsystems = subsystems
.split(',')
.map(|subsystem| SubSystem::from_str(subsystem).unwrap())
.collect();
Ok((Request::Idle(Some(subsystems)), ""))
});
fn into_request_enum(self) -> crate::Request {
crate::Request::Idle(self.0)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Idle(subsystems) => Some(IdleRequest(subsystems)),
_ => None,
}
}
fn serialize(&self) -> String {
match &self.0 {
Some(subsystems) => {
let subsystems_str = subsystems
.iter()
.map(|subsystem| subsystem.to_string())
.collect::<Vec<_>>()
.join(",");
format!("{} {}", Self::COMMAND, subsystems_str)
}
None => Self::COMMAND.to_string(),
}
}
fn parse(parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let mut parts = parts;
let result = parts.next().map_or(Ok(None), |subsystems| {
let subsystems = subsystems
.split(',')
.map(|subsystem| SubSystem::from_str(subsystem).unwrap())
.collect();
Ok(Some(subsystems))
});
debug_assert!(parts.next().is_none());
result
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
result.map(IdleRequest)
}
}
empty_command_response!(Idle);
impl Command<'_, '_> for Idle {
type Request = IdleRequest;
type Response = IdleResponse;
}

View File

@@ -2,13 +2,20 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
get_and_parse_optional_property, get_and_parse_property,
use crate::{
commands::{
Command, CommandResponse, RequestParserError, ResponseParserError, empty_command_request,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::{
ResponseAttributes, get_and_parse_optional_property, get_and_parse_property,
},
};
pub struct Stats;
empty_command_request!(Stats, "stats");
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StatsResponse {
pub uptime: u64,
@@ -20,20 +27,17 @@ pub struct StatsResponse {
pub db_update: Option<u64>,
}
impl Command for Stats {
type Response = StatsResponse;
const COMMAND: &'static str = "stats";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Stats, ""))
impl CommandResponse<'_> for StatsResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into_map()?;
let uptime = get_and_parse_property!(parts, "uptime", Text);
let playtime = get_and_parse_property!(parts, "playtime", Text);
@@ -54,3 +58,8 @@ impl Command for Stats {
})
}
}
impl Command<'_, '_> for Stats {
type Request = StatsRequest;
type Response = StatsResponse;
}

View File

@@ -3,14 +3,22 @@ use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::common::{Audio, BoolOrOneshot, SongId, SongPosition};
use crate::commands::{
Command, GenericResponseValue, Request, RequestParserResult, ResponseAttributes,
ResponseParserError, get_and_parse_optional_property, get_and_parse_property,
get_optional_property, get_property,
use crate::{
commands::{
Command, CommandResponse, RequestParserError, ResponseParserError, empty_command_request,
},
common::types::{Audio, BoolOrOneshot, SongId, SongPosition},
request_tokenizer::RequestTokenizer,
response_tokenizer::{
ResponseAttributes, get_and_parse_optional_property, get_and_parse_property,
get_optional_property, get_property,
},
};
pub struct Status;
empty_command_request!(Status, "status");
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum StatusResponseState {
Play,
@@ -60,119 +68,114 @@ pub struct StatusResponse {
pub last_loaded_playlist: Option<String>,
}
#[inline]
fn parse_status_response(
parts: ResponseAttributes<'_>,
) -> Result<StatusResponse, ResponseParserError> {
let parts: HashMap<&str, GenericResponseValue> = parts.into();
let partition = get_property!(parts, "partition", Text).to_string();
impl CommandResponse<'_> for StatusResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
let volume = match get_property!(parts, "volume", Text) {
"-1" => None,
volume => Some(
volume
.parse()
.map_err(|_| ResponseParserError::InvalidProperty("volume", volume))?,
),
};
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
let repeat = match get_property!(parts, "repeat", Text) {
"0" => Ok(false),
"1" => Ok(true),
repeat => Err(ResponseParserError::InvalidProperty("repeat", repeat)),
}?;
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
let parts: HashMap<_, _> = parts.into_map()?;
let partition = get_property!(parts, "partition", Text).to_string();
let random = match get_property!(parts, "random", Text) {
"0" => Ok(false),
"1" => Ok(true),
random => Err(ResponseParserError::InvalidProperty("random", random)),
}?;
let volume = match get_property!(parts, "volume", Text) {
"-1" => None,
volume => Some(
volume
.parse()
.map_err(|_| ResponseParserError::InvalidProperty("volume", volume))?,
),
};
let single = get_and_parse_property!(parts, "single", Text);
let consume = get_and_parse_property!(parts, "consume", Text);
let playlist: u32 = get_and_parse_property!(parts, "playlist", Text);
let playlist_length: u64 = get_and_parse_property!(parts, "playlistlength", Text);
let state: StatusResponseState = get_and_parse_property!(parts, "state", Text);
let song: Option<SongPosition> = get_and_parse_optional_property!(parts, "song", Text);
let song_id: Option<SongId> = get_and_parse_optional_property!(parts, "songid", Text);
let next_song: Option<SongPosition> = get_and_parse_optional_property!(parts, "nextsong", Text);
let next_song_id: Option<SongId> = get_and_parse_optional_property!(parts, "nextsongid", Text);
let repeat = match get_property!(parts, "repeat", Text) {
"0" => Ok(false),
"1" => Ok(true),
repeat => Err(ResponseParserError::InvalidProperty("repeat", repeat)),
}?;
let time = match get_optional_property!(parts, "time", Text) {
Some(time) => {
let mut parts = time.split(':');
let elapsed = parts
.next()
.ok_or(ResponseParserError::SyntaxError(0, time))?
.parse()
.map_err(|_| ResponseParserError::InvalidProperty("time", time))?;
let duration = parts
.next()
.ok_or(ResponseParserError::SyntaxError(0, time))?
.parse()
.map_err(|_| ResponseParserError::InvalidProperty("time", time))?;
Some((elapsed, duration))
}
None => None,
};
let random = match get_property!(parts, "random", Text) {
"0" => Ok(false),
"1" => Ok(true),
random => Err(ResponseParserError::InvalidProperty("random", random)),
}?;
let elapsed = get_and_parse_optional_property!(parts, "elapsed", Text);
let duration = get_and_parse_optional_property!(parts, "duration", Text);
let bitrate = get_and_parse_optional_property!(parts, "bitrate", Text);
let xfade = get_and_parse_optional_property!(parts, "xfade", Text);
let mixrampdb = get_and_parse_optional_property!(parts, "mixrampdb", Text);
let mixrampdelay = get_and_parse_optional_property!(parts, "mixrampdelay", Text);
let audio = get_and_parse_optional_property!(parts, "audio", Text);
let updating_db = get_and_parse_optional_property!(parts, "updating_db", Text);
let error = get_and_parse_optional_property!(parts, "error", Text);
let last_loaded_playlist =
get_and_parse_optional_property!(parts, "last_loaded_playlist", Text);
let single = get_and_parse_property!(parts, "single", Text);
let consume = get_and_parse_property!(parts, "consume", Text);
let playlist: u32 = get_and_parse_property!(parts, "playlist", Text);
let playlist_length: u64 = get_and_parse_property!(parts, "playlistlength", Text);
let state: StatusResponseState = get_and_parse_property!(parts, "state", Text);
let song: Option<SongPosition> = get_and_parse_optional_property!(parts, "song", Text);
let song_id: Option<SongId> = get_and_parse_optional_property!(parts, "songid", Text);
let next_song: Option<SongPosition> =
get_and_parse_optional_property!(parts, "nextsong", Text);
let next_song_id: Option<SongId> =
get_and_parse_optional_property!(parts, "nextsongid", Text);
Ok(StatusResponse {
partition,
volume,
repeat,
random,
single,
consume,
playlist,
playlist_length,
state,
song,
song_id,
next_song,
next_song_id,
time,
elapsed,
duration,
bitrate,
xfade,
mixrampdb,
mixrampdelay,
audio,
updating_db,
error,
last_loaded_playlist,
})
let time = match get_optional_property!(parts, "time", Text) {
Some(time) => {
let mut parts = time.split(':');
let elapsed = parts
.next()
.ok_or(ResponseParserError::SyntaxError(0, time))?
.parse()
.map_err(|_| ResponseParserError::InvalidProperty("time", time))?;
let duration = parts
.next()
.ok_or(ResponseParserError::SyntaxError(0, time))?
.parse()
.map_err(|_| ResponseParserError::InvalidProperty("time", time))?;
Some((elapsed, duration))
}
None => None,
};
let elapsed = get_and_parse_optional_property!(parts, "elapsed", Text);
let duration = get_and_parse_optional_property!(parts, "duration", Text);
let bitrate = get_and_parse_optional_property!(parts, "bitrate", Text);
let xfade = get_and_parse_optional_property!(parts, "xfade", Text);
let mixrampdb = get_and_parse_optional_property!(parts, "mixrampdb", Text);
let mixrampdelay = get_and_parse_optional_property!(parts, "mixrampdelay", Text);
let audio = get_and_parse_optional_property!(parts, "audio", Text);
let updating_db = get_and_parse_optional_property!(parts, "updating_db", Text);
let error = get_and_parse_optional_property!(parts, "error", Text);
let last_loaded_playlist =
get_and_parse_optional_property!(parts, "last_loaded_playlist", Text);
Ok(StatusResponse {
partition,
volume,
repeat,
random,
single,
consume,
playlist,
playlist_length,
state,
song,
song_id,
next_song,
next_song_id,
time,
elapsed,
duration,
bitrate,
xfade,
mixrampdb,
mixrampdelay,
audio,
updating_db,
error,
last_loaded_playlist,
})
}
}
pub struct Status;
impl Command for Status {
impl Command<'_, '_> for Status {
type Request = StatusRequest;
type Response = StatusResponse;
const COMMAND: &'static str = "status";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Status, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
parse_status_response(parts)
}
}
#[cfg(test)]
@@ -207,7 +210,7 @@ mod tests {
"# };
assert_eq!(
Status::parse_raw_response(contents),
Status::parse_raw_response(contents.as_bytes()),
Ok(StatusResponse {
partition: "default".into(),
volume: Some(66),

View File

@@ -1,18 +1,44 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
common::SongPosition,
common::types::{SongPosition, Uri},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Add;
impl Command for Add {
type Response = ();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AddRequest {
uri: Uri,
position: Option<SongPosition>,
}
impl CommandRequest<'_> for AddRequest {
const COMMAND: &'static str = "add";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::Add(self.uri, self.position)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Add(uri, position) => Some(AddRequest { uri, position }),
_ => None,
}
}
fn serialize(&self) -> String {
match self.position {
Some(position) => format!("{} {} {}", Self::COMMAND, self.uri, position),
None => format!("{} {}", Self::COMMAND, self.uri),
}
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let uri = match parts.next() {
Some(s) => s,
None => return Err(RequestParserError::UnexpectedEOF),
@@ -28,13 +54,16 @@ impl Command for Add {
debug_assert!(parts.next().is_none());
Ok((Request::Add(uri.to_string(), position), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(AddRequest {
uri: uri.to_string(),
position,
})
}
}
empty_command_response!(Add);
impl Command<'_, '_> for Add {
type Request = AddRequest;
type Response = AddResponse;
}

View File

@@ -1,22 +1,42 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError, get_next_and_parse_property,
},
common::{SongId, SongPosition},
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
common::types::{SongId, SongPosition, Uri},
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_next_and_parse_property},
};
pub struct AddId;
pub struct AddIdResponse {
pub id: SongId,
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AddIdRequest {
pub uri: Uri,
pub position: Option<SongPosition>,
}
impl Command for AddId {
type Response = AddIdResponse;
impl CommandRequest<'_> for AddIdRequest {
const COMMAND: &'static str = "addid";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::AddId(self.uri, self.position)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::AddId(uri, position) => Some(AddIdRequest { uri, position }),
_ => None,
}
}
fn serialize(&self) -> String {
match self.position {
Some(pos) => format!("{} {} {}", Self::COMMAND, self.uri, pos),
None => format!("{} {}", Self::COMMAND, self.uri),
}
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let uri = match parts.next() {
Some(s) => s,
None => return Err(RequestParserError::UnexpectedEOF),
@@ -32,16 +52,39 @@ impl Command for AddId {
debug_assert!(parts.next().is_none());
Ok((Request::AddId(uri.to_string(), position), ""))
Ok(AddIdRequest {
uri: uri.to_string(),
position,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AddIdResponse {
pub id: SongId,
}
impl CommandResponse<'_> for AddIdResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
let parts: Vec<_> = parts.into();
let mut iter = parts.into_iter();
let (key, id) = get_next_and_parse_property!(iter, Text);
debug_assert!(key == "Id");
if key != "Id" {
return Err(ResponseParserError::UnexpectedProperty(key));
}
Ok(AddIdResponse { id })
}
}
impl Command<'_, '_> for AddId {
type Request = AddIdRequest;
type Response = AddIdResponse;
}

View File

@@ -1,17 +1,52 @@
use serde::{Deserialize, Serialize};
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseAttributes, ResponseParserError,
empty_command_response,
},
common::types::{SongId, TagName, TagValue},
request_tokenizer::RequestTokenizer,
};
pub struct AddTagId;
impl Command for AddTagId {
type Response = ();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AddTagIdRequest {
pub songid: SongId,
pub tag_name: TagName,
pub tag_value: TagValue,
}
impl CommandRequest<'_> for AddTagIdRequest {
const COMMAND: &'static str = "addtagid";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::AddTagId(self.songid, self.tag_name, self.tag_value)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::AddTagId(songid, tag_name, tag_value) => Some(AddTagIdRequest {
songid,
tag_name,
tag_value,
}),
_ => None,
}
}
fn serialize(&self) -> String {
format!(
"{} {} {} {}",
Self::COMMAND,
self.songid,
self.tag_name,
self.tag_value
)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let songid = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let songid = songid
.parse()
@@ -29,13 +64,17 @@ impl Command for AddTagId {
debug_assert!(parts.next().is_none());
Ok((Request::AddTagId(songid, tag_name, tag_value), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(AddTagIdRequest {
songid,
tag_name,
tag_value,
})
}
}
empty_command_response!(AddTagId);
impl Command<'_, '_> for AddTagId {
type Request = AddTagIdRequest;
type Response = AddTagIdResponse;
}

View File

@@ -1,22 +1,19 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
use crate::{
commands::{
Command, RequestParserError, ResponseParserError, empty_command_request,
empty_command_response,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Clear;
impl Command for Clear {
type Response = ();
const COMMAND: &'static str = "clear";
empty_command_request!(Clear, "clear");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Clear, ""))
}
empty_command_response!(Clear);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Clear {
type Request = ClearRequest;
type Response = ClearResponse;
}

View File

@@ -1,17 +1,43 @@
use serde::{Deserialize, Serialize};
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseParserError, empty_command_response,
},
common::types::{SongId, TagName},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct ClearTagId;
impl Command for ClearTagId {
type Response = ();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClearTagIdRequest {
pub songid: SongId,
pub tag_name: TagName,
}
impl CommandRequest<'_> for ClearTagIdRequest {
const COMMAND: &'static str = "cleartagid";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::ClearTagId(self.songid, self.tag_name)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::ClearTagId(songid, tag_name) => {
Some(ClearTagIdRequest { songid, tag_name })
}
_ => None,
}
}
fn serialize(&self) -> String {
format!("{} {} {}", Self::COMMAND, self.songid, self.tag_name)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let songid = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let songid = songid
.parse()
@@ -24,13 +50,13 @@ impl Command for ClearTagId {
debug_assert!(parts.next().is_none());
Ok((Request::ClearTagId(songid, tag_name), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(ClearTagIdRequest { songid, tag_name })
}
}
empty_command_response!(ClearTagId);
impl Command<'_, '_> for ClearTagId {
type Request = ClearTagIdRequest;
type Response = ClearTagIdResponse;
}

View File

@@ -1,33 +1,22 @@
use std::str::FromStr;
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
common::OneOrRange,
common::types::OneOrRange,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Delete;
impl Command for Delete {
type Response = ();
const COMMAND: &'static str = "delete";
single_item_command_request!(Delete, "delete", OneOrRange);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let pos_or_range = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let one_or_range = OneOrRange::from_str(pos_or_range)
.map_err(|_| RequestParserError::SyntaxError(0, pos_or_range.to_string()))?;
empty_command_response!(Delete);
debug_assert!(parts.next().is_none());
Ok((Request::Delete(one_or_range), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for Delete {
type Request = DeleteRequest;
type Response = DeleteResponse;
}

View File

@@ -1,31 +1,20 @@
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, RequestParserError, ResponseParserError, empty_command_response,
single_item_command_request,
},
common::types::SongId,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct DeleteId;
impl Command for DeleteId {
type Response = ();
const COMMAND: &'static str = "deleteid";
single_item_command_request!(DeleteId, "deleteid", SongId);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let id = id
.parse()
.map_err(|_| RequestParserError::SyntaxError(0, id.to_string()))?;
empty_command_response!(DeleteId);
debug_assert!(parts.next().is_none());
Ok((Request::DeleteId(id), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
impl Command<'_, '_> for DeleteId {
type Request = DeleteIdRequest;
type Response = DeleteIdResponse;
}

View File

@@ -1,17 +1,41 @@
use serde::{Deserialize, Serialize};
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseAttributes, ResponseParserError,
empty_command_response,
},
common::types::{AbsouluteRelativeSongPosition, OneOrRange},
request_tokenizer::RequestTokenizer,
};
pub struct Move;
impl Command for Move {
type Response = ();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MoveRequest {
pub from_or_range: OneOrRange,
pub to: AbsouluteRelativeSongPosition,
}
impl CommandRequest<'_> for MoveRequest {
const COMMAND: &'static str = "move";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::Move(self.from_or_range, self.to)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Move(from_or_range, to) => Some(MoveRequest { from_or_range, to }),
_ => None,
}
}
fn serialize(&self) -> String {
format!("{} {} {}", Self::COMMAND, self.from_or_range, self.to)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let from_or_range = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let from_or_range = from_or_range
.parse()
@@ -24,13 +48,13 @@ impl Command for Move {
debug_assert!(parts.next().is_none());
Ok((Request::Move(from_or_range, to), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(MoveRequest { from_or_range, to })
}
}
empty_command_response!(Move);
impl Command<'_, '_> for Move {
type Request = MoveRequest;
type Response = MoveResponse;
}

View File

@@ -1,17 +1,41 @@
use serde::{Deserialize, Serialize};
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, CommandRequest, RequestParserError, ResponseAttributes, ResponseParserError,
empty_command_response,
},
common::types::{AbsouluteRelativeSongPosition, SongId},
request_tokenizer::RequestTokenizer,
};
pub struct MoveId;
impl Command for MoveId {
type Response = ();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MoveIdRequest {
pub id: SongId,
pub to: AbsouluteRelativeSongPosition,
}
impl CommandRequest<'_> for MoveIdRequest {
const COMMAND: &'static str = "moveid";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
fn into_request_enum(self) -> crate::Request {
crate::Request::MoveId(self.id, self.to)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::MoveId(id, to) => Some(MoveIdRequest { id, to }),
_ => None,
}
}
fn serialize(&self) -> String {
format!("{} {} {}", Self::COMMAND, self.id, self.to)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let id = id
.parse()
@@ -24,13 +48,13 @@ impl Command for MoveId {
debug_assert!(parts.next().is_none());
Ok((Request::MoveId(id, to), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(MoveIdRequest { id, to })
}
}
empty_command_response!(MoveId);
impl Command<'_, '_> for MoveId {
type Request = MoveIdRequest;
type Response = MoveIdResponse;
}

View File

@@ -1,22 +1,33 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{
Command, CommandResponse, RequestParserError, ResponseParserError, empty_command_request,
single_item_command_request,
},
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
};
pub struct Playlist;
impl Command for Playlist {
type Response = ();
const COMMAND: &'static str = "playlist";
empty_command_request!(Playlist, "playlist");
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Playlist, ""))
pub struct PlaylistResponse();
impl CommandResponse<'_> for PlaylistResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn parse_response(
_parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError<'_>> {
unimplemented!()
}
}
impl Command<'_, '_> for Playlist {
type Request = PlaylistRequest;
type Response = PlaylistResponse;
}

Some files were not shown because too many files have changed in this diff Show More