147 Commits

Author SHA1 Message Date
oysteikt d27f07ce75 WIP: commands: add req/res constructors 2026-06-21 15:49:06 +09:00
oysteikt 81e2f73fda build.rs: fix lalrpop building in direnv
Build and test / check (push) Successful in 56s
Build and test / docs (push) Successful in 1m21s
Build and test / test (push) Successful in 1m41s
Build and test / build (push) Successful in 2m0s
2026-06-21 15:48:59 +09:00
oysteikt 2c0565624d commands: implement Default for empty req/res structs 2026-06-21 15:39:25 +09:00
oysteikt a5e6245957 commands: add ::new constructors for macro generated items 2026-06-21 15:39:25 +09:00
oysteikt 56d7c942db commands: remove toplevel Request/Response enums
This also exposes all the command types as public API
2026-06-21 15:39:23 +09:00
oysteikt e701aeab17 flake.lock: bump, Cargo.{toml,lock}: update inputs
Build and test / check (push) Successful in 1m4s
Build and test / build (push) Successful in 1m7s
Build and test / test (push) Successful in 1m38s
Build and test / docs (push) Successful in 1m40s
2026-04-02 14:06:22 +09:00
oysteikt 644020ea7f flake.lock: bump, Cargo.{toml,lock}: update inputs, fmt, lint
Build and test / build (push) Successful in 1m12s
Build and test / docs (push) Successful in 1m20s
Build and test / check (push) Successful in 1m32s
Build and test / test (push) Successful in 1m47s
2026-02-07 02:25:34 +09:00
oysteikt aafda77896 flake.nix: build package and run tests
Build and test / build (push) Successful in 1m47s
Build and test / test (push) Successful in 1m55s
Build and test / docs (push) Successful in 2m28s
Build and test / check (push) Failing after 39s
2026-01-25 15:06:44 +09:00
oysteikt b4a0b5e27f client: make better constructors/destructors 2026-01-25 15:06:18 +09:00
oysteikt 4dfb4d2150 commands/tag_types_disable: remove u32 import
Build and test / check (push) Successful in 1m4s
Build and test / build (push) Successful in 1m6s
Build and test / docs (push) Successful in 1m10s
Build and test / test (push) Successful in 1m46s
2026-01-10 01:22:17 +09:00
oysteikt e3300f6c8c response_tokenizer: refactor
Build and test / check (push) Successful in 1m5s
Build and test / build (push) Successful in 1m10s
Build and test / docs (push) Successful in 1m11s
Build and test / test (push) Successful in 2m2s
2026-01-10 01:16:30 +09:00
oysteikt a89ad2f93e commands: constructors for all commands
Build and test / build (push) Successful in 1m12s
Build and test / check (push) Successful in 1m22s
Build and test / test (push) Successful in 1m57s
Build and test / docs (push) Successful in 2m16s
2026-01-10 00:12:15 +09:00
oysteikt 7292a940d7 .cargo/config.toml: init with pvv-git registry
Build and test / check (push) Successful in 1m0s
Build and test / build (push) Successful in 1m39s
Build and test / test (push) Successful in 1m36s
Build and test / docs (push) Successful in 2m20s
2026-01-02 21:22:21 +09:00
oysteikt 1be0bdddc8 flake.lock: bump 2025-12-15 09:10:00 +09:00
oysteikt 20803c7573 .gitea/workflows: bump rsync-action from v1 -> v2 2025-12-15 09:08:42 +09:00
oysteikt 46e8a22ddb .gitea/workflows: run on debian-latest 2025-12-08 18:43:05 +09:00
oysteikt 695ef08f62 .gitea/workflows: update actions/checkout: v4 -> v6 2025-12-08 18:42:33 +09:00
oysteikt 4356258312 commands: store SubtypeParserError expected type as &str 2025-12-08 17:48:44 +09:00
oysteikt b1bbd4ebb1 commands: use new error variants for a few more commands 2025-12-08 17:30:47 +09:00
oysteikt f6a0489539 lib: fix doccomment url rendering 2025-12-08 16:27:55 +09:00
oysteikt cedf17d5f9 commands: add request parser error variant for keyword without value 2025-12-08 16:25:33 +09:00
oysteikt 350892fda9 commands: clearly define how arguments are counted in request error 2025-12-08 16:25:04 +09:00
oysteikt febe651158 Clean up misc. module doccomments 2025-12-08 16:15:12 +09:00
oysteikt 39e6b237af filter: document misc 2025-12-08 16:09:18 +09:00
oysteikt 65017ad6b3 .gitea/workflows: don't error on clippy warnings 2025-12-08 15:59:10 +09:00
oysteikt bc915d1ca7 .gitea/workflows: don't run with all features 2025-12-08 15:57:40 +09:00
oysteikt 44b0e6aa85 commands: use new error variants for various commands 2025-12-08 15:54:37 +09:00
oysteikt 670cdc7ba5 Preallocate a few more response parsers 2025-12-08 14:27:42 +09:00
oysteikt 0675d9d984 commands: add and fix variants for RequestParserError 2025-12-08 13:55:25 +09:00
oysteikt 27b1c889df filter: flatten module 2025-12-08 13:32:18 +09:00
oysteikt 8f3dff30a8 cargo fmt + clippy 2025-12-08 13:28:45 +09:00
oysteikt 04e8c1c144 commands: extend parser errors 2025-12-08 13:28:17 +09:00
oysteikt 2582ffac2a Remove a few unused type aliases 2025-12-08 13:11:09 +09:00
oysteikt 1bd2e4f0fb *_tokenizer: add module doccomment 2025-12-08 13:10:31 +09:00
oysteikt 818f89859f commands: force external users to interact with requests and responses through Command trait 2025-12-08 13:07:11 +09:00
oysteikt f2e2eb271a commands: add command executor directly on Command trait 2025-12-08 13:05:30 +09:00
oysteikt 10fb43fa64 examples/mpd-client: add some code 2025-12-08 13:00:46 +09:00
oysteikt d123a53828 commands: strip lifetimes 2025-12-08 12:30:19 +09:00
oysteikt 1311b8118f commands: add newline at end of all command serializers 2025-12-08 05:49:54 +09:00
oysteikt 9f74b219d3 client: init 2025-12-08 05:32:49 +09:00
oysteikt 143dc48019 MpdError: impl thiserror 2025-12-08 05:31:43 +09:00
oysteikt e8f2cd2034 make ResponseParserError self-contained, impl thiserror 2025-12-08 05:31:13 +09:00
oysteikt 72923b1549 common/types: flatten to types 2025-12-08 04:14:32 +09:00
oysteikt 3faf01355f commands: document module 2025-12-08 04:07:20 +09:00
oysteikt 4c07b966b7 response: add some notes about the errors 2025-12-08 01:46:14 +09:00
oysteikt edd9fd0958 LICENSE: init 2025-12-08 01:15:23 +09:00
oysteikt 874fac6ea5 cargo clippy 2025-12-08 01:08:58 +09:00
oysteikt 7e749a4f46 commands/stickernamestypes: add missing derive for response 2025-12-08 01:05:24 +09:00
oysteikt 525b913f70 commands/listplaylist: add note about format 2025-12-08 01:03:23 +09:00
oysteikt 9e3b2d0465 commands/seekcur: fix parser 2025-12-08 01:03:11 +09:00
oysteikt 58f7c01082 commands: fix request enum conversion for unmount 2025-12-08 00:52:27 +09:00
oysteikt b3b7d97fee flake.lock: bump inputs 2025-12-08 00:45:53 +09:00
oysteikt 5823e95eb9 commands: add response types for multiple commands 2025-12-08 00:44:37 +09:00
oysteikt c0cc63503a commands/currentsong: implement response parser 2025-12-08 00:29:00 +09:00
oysteikt b8aaac544d commands/listplaylistinfo: fix response type 2025-12-08 00:28:38 +09:00
oysteikt 2a97914c93 commands: split Command trait into req + res parts 2025-12-05 22:54:01 +09:00
oysteikt 6191e10028 cargo fmt 2025-12-07 21:39:57 +09:00
oysteikt fc5cbe8024 commands/listplaylistinfo: add response type 2025-12-07 21:33:01 +09:00
oysteikt c811094908 commands: implement all database selection responses 2025-12-07 21:33:02 +09:00
oysteikt 267fd0e2e9 types/tag: make orderable 2025-12-07 20:53:45 +09:00
oysteikt b97b650f64 common/types: add db selection print types 2025-12-07 21:21:58 +09:00
oysteikt a2c143a461 commands/listplaylists: implement 2025-12-07 20:53:45 +09:00
oysteikt d811840ea1 commands: parse to Self::Request 2025-12-05 22:00:11 +09:00
oysteikt 925eb1941a filter: export all types from inner module 2025-12-05 21:00:01 +09:00
oysteikt ef753e199a common: don't expose types directly 2025-12-05 20:59:19 +09:00
oysteikt cdb3b2d58c commands: add docs for Command trait 2025-11-25 05:28:58 +09:00
oysteikt 4f8fa26cd4 common/types: add better alias for MountPath 2025-11-25 04:27:20 +09:00
oysteikt 5117ed319d common/types: add better alias for AudioOutputId 2025-11-25 04:22:30 +09:00
oysteikt 69f79197aa common/types: add type for ChannelName 2025-11-25 04:13:32 +09:00
oysteikt f778d85a3c common/types: add better alias for PlaylistVersion 2025-11-25 04:02:03 +09:00
oysteikt e56b9ab6c4 common/types: add type for Sort 2025-11-25 03:56:01 +09:00
oysteikt 59a347f610 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:49:27 +09:00
oysteikt bdbecf3e8f commands: fix request de/serialization for list 2025-11-24 22:00:42 +09:00
oysteikt 162b6f95f1 commands: remove some fixed TODOs 2025-11-24 21:59:42 +09:00
oysteikt e45d8c054b commands: split response tokenizer into separate file 2025-11-24 19:16:09 +09:00
oysteikt d5445466ff filter: implement basic parser 2025-11-24 17:38:37 +09:00
oysteikt 56bdeb3e0a common/types: case insensitive tags 2025-11-24 17:36:48 +09:00
oysteikt f3ad2fc73a Implement a proper request tokenizer 2025-11-24 14:18:30 +09:00
oysteikt 63dfa1b255 WIP: serialize requests 2025-11-21 16:48:55 +09:00
oysteikt 21f98c5808 filter: implement fmt::Display 2025-11-21 18:36:54 +09:00
oysteikt e4e7530902 common/types: implement serialization helpers for Tag 2025-11-21 18:36:18 +09:00
oysteikt 726ea92e1b common/types: implement fmt::Display 2025-11-21 16:59:39 +09:00
oysteikt 0bdfdf149e Cargo.toml: bump deps 2025-11-21 16:15:16 +09:00
oysteikt 07fdf4b631 flake.lock: bump 2025-11-21 16:14:13 +09:00
oysteikt fccb32ad6d commands: implement response parser for lsinfo 2025-11-21 16:13:09 +09:00
oysteikt 8967f38ffa cargo fmt + clippy 2025-11-21 16:02:15 +09:00
oysteikt 72247d3a9d filter: add unit tests 2025-11-21 15:49:27 +09:00
oysteikt 030449128a commands: implement response parser for tagtypes available 2025-11-21 15:16:30 +09:00
oysteikt cbf4e9d382 commands: implement response parser for protocol 2025-11-21 15:14:43 +09:00
oysteikt 79d974f9c0 commands: implement response parser for protocol available 2025-11-21 15:12:21 +09:00
oysteikt 95604aa20d commands: return runtime errors on invalid property names 2025-11-21 15:07:50 +09:00
oysteikt ebfe1311b9 commands: create result struct for readmessages 2025-11-21 14:55:57 +09:00
oysteikt d3e7fac92c commands: precalculate capacity and use iterators 2025-11-21 14:44:17 +09:00
oysteikt 5eb08d852a commands: make better use of expect_property_type! 2025-11-21 14:38:09 +09:00
oysteikt d66c555a26 commands: implement response parser for listmounts 2025-11-21 14:32:06 +09:00
oysteikt a58ed698e7 commands: implement common traits for responses 2025-11-21 14:18:44 +09:00
oysteikt fdd1d358cd commands: implement response parser for decoders 2025-11-21 14:14:25 +09:00
oysteikt 799d37bc0b commands: implement response parser for listneighbors 2025-11-21 14:04:12 +09:00
oysteikt cdad776df0 commands: implement response parser for outputs 2025-11-21 13:55:10 +09:00
oysteikt a261bdbf89 common/types: add debug assertions for range orders 2025-10-13 15:35:56 +09:00
oysteikt dcd8444a6a flake.lock: bump, Cargo.toml: update inputs 2025-10-12 23:01:17 +09:00
oysteikt 29f4e0519e commands: fix clippy warnings about confusing elided lifetimes 2025-10-12 22:59:50 +09:00
oysteikt 6d9e9cd468 flake.lock: bump, Cargo.toml: update inputs 2025-09-20 18:05:21 +02:00
oysteikt dcfe30e0fd .gitignore: ignore Cargo.lock
This is okay because this is a library
2025-09-20 18:03:35 +02:00
oysteikt 708849e87f flake.lock: bump 2025-08-03 05:01:37 +02:00
oysteikt ae0fbaaa15 .gitea/workflows: update gitea-web target host 2025-08-03 04:50:01 +02:00
oysteikt 8e10db40bd Cargo.toml: update deps 2025-07-11 20:37:16 +02:00
oysteikt aba16b3216 flake.lock: bump 2025-07-11 20:36:58 +02:00
oysteikt fe840015f4 flake.nix: add cargo-edit to devshell 2025-07-11 20:36:50 +02:00
oysteikt a1cea09979 flake.lock: bump 2025-05-07 10:43:24 +02:00
oysteikt de1cdd59ac Cargo.lock: bump 2025-05-05 10:15:00 +02:00
oysteikt 1e352d4413 flake.lock: bump 2025-05-05 10:13:44 +02:00
oysteikt 88e3deebc8 Rust edition 2024 2025-02-26 16:39:34 +01:00
oysteikt 98d1b1eebb common/types: move stuff from requests to types 2025-02-23 19:46:28 +01:00
oysteikt 05ae68de0b commands: deduplicate logic in macros, add more macros 2025-02-23 19:16:58 +01:00
oysteikt f6c76fb006 commands: implement some more response parsers 2025-02-23 16:41:15 +01:00
oysteikt 77d6ce919e Cargo fmt 2025-02-23 16:40:06 +01:00
oysteikt 11598a5827 commands: move ResponseAttributes parser out of Command 2025-02-23 16:33:34 +01:00
oysteikt 90a467287f flake.lock: bump 2025-02-23 16:30:39 +01:00
oysteikt 28c90b4802 common: move types into separate files 2025-02-23 16:30:21 +01:00
oysteikt 71af030116 Move project from Projects to Grzegorz 2025-01-06 16:32:13 +01:00
oysteikt 0b5fb4b571 commands: fix some syntax errors reporting literals 2024-12-14 00:19:25 +01:00
oysteikt c04c14e710 Implement some more commands 2024-12-14 00:14:02 +01:00
oysteikt 8dc6bb0382 commands: add missing debug asserts 2024-12-13 19:31:40 +01:00
oysteikt 98e6a05f9f commands: add some TODOs for assumptions made about syntax 2024-12-13 19:31:12 +01:00
oysteikt 7c26e2c409 response: remove leftover response types, add error codes 2024-12-13 18:29:54 +01:00
oysteikt 1e6279b7df Implement some more commands 2024-12-13 18:20:03 +01:00
oysteikt ace026af6b commands: add a few TODOs 2024-12-13 17:08:59 +01:00
oysteikt c62dfc58c9 format 2024-12-13 17:08:15 +01:00
oysteikt b03c0fac3c commands: verify key uniqueness for ResponseAttributes -> HashMap 2024-12-13 17:06:28 +01:00
oysteikt b4dfdb9b8b commands: use real Filter type 2024-12-13 17:05:08 +01:00
oysteikt ecbc645784 Move repo to Projects 2024-12-13 17:02:58 +01:00
oysteikt 642a46a7fc examples/mpd-client: format 2024-12-13 16:59:40 +01:00
oysteikt f6981052c8 .gitea/build-and-test: init 2024-12-10 21:48:20 +01:00
oysteikt 0ff66fb9d9 .envrc: init 2024-12-05 18:10:11 +01:00
oysteikt 7c91dda409 Add more commands 2024-12-01 20:06:01 +01:00
oysteikt d2ef02971f Add existing command parsers to the main request parser 2024-12-01 19:16:44 +01:00
oysteikt a7da84bc9c Implement some more commands 2024-11-30 17:12:49 +01:00
oysteikt b84cf514c2 prefer unimplemented! for unimplemented functions 2024-11-30 03:36:00 +01:00
oysteikt 28610b5235 commands: add derives for a few response types 2024-11-30 03:31:52 +01:00
oysteikt 94b42b52bc commands: handle some more response parsing 2024-11-30 03:28:22 +01:00
oysteikt 1b22b05dc6 commands/stats: parse_response 2024-11-30 02:28:29 +01:00
oysteikt d59b1bfb40 commands: make macros usable without use statements 2024-11-30 02:28:10 +01:00
oysteikt 48f7fc1faa commands: use different datastructure for response attrs 2024-11-30 02:18:06 +01:00
oysteikt 6c984cf322 flake.lock: bump 2024-11-30 01:57:57 +01:00
oysteikt a1ef15f41b Continued development 2024-11-30 01:57:45 +01:00
oysteikt 8b0d33b941 Initial commit 2024-11-05 22:47:35 +01:00
188 changed files with 10349 additions and 4630 deletions
+2
View File
@@ -0,0 +1,2 @@
[registries]
pvv-git = { index = "sparse+https://git.pvv.ntnu.no/api/packages/Grzegorz/cargo/" }
+18 -18
View File
@@ -7,20 +7,20 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: debian-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build
run: cargo build --all-features --verbose --release
run: cargo build --verbose --release
check:
runs-on: ubuntu-latest
runs-on: debian-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -31,12 +31,13 @@ jobs:
run: cargo fmt -- --check
- name: Check clippy
run: cargo clippy --all-features -- --deny warnings
# run: cargo clippy -- --deny warnings
run: cargo clippy
test:
runs-on: ubuntu-latest
runs-on: debian-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: cargo-bins/cargo-binstall@main
- name: Install rust toolchain
@@ -49,7 +50,7 @@ jobs:
- name: Run tests
run: |
cargo nextest run --all-features --release --no-fail-fast
cargo nextest run --release --no-fail-fast
env:
RUST_LOG: "trace"
RUSTFLAGS: "-Cinstrument-coverage"
@@ -83,13 +84,13 @@ 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
runs-on: debian-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v6
- name: Install latest nightly toolchain
uses: actions-rs/toolchain@v1
@@ -98,15 +99,14 @@ jobs:
override: true
- name: Build docs
run: cargo doc --all-features --document-private-items --release
run: cargo doc --document-private-items --release
- name: Transfer files
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v1
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v2
with:
source: target/doc/
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"
+1 -1
View File
@@ -1,3 +1,3 @@
/target
result
result-*
result-*
Generated
+822 -17
View File
@@ -2,26 +2,495 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "ascii-canvas"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891"
dependencies = [
"term",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cc"
version = "1.2.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "empidee"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"futures-util",
"indoc",
"lalrpop",
"lalrpop-util",
"paste",
"pretty_assertions",
"serde",
"thiserror",
"tokio",
]
[[package]]
name = "ena"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1"
dependencies = [
"log",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-io"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-macro"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-io",
"futures-macro",
"futures-task",
"memchr",
"pin-project-lite",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "indoc"
version = "2.0.6"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
dependencies = [
"rustversion",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "js-sys"
version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "keccak"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
dependencies = [
"cpufeatures",
]
[[package]]
name = "lalrpop"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501"
dependencies = [
"ascii-canvas",
"bit-set",
"ena",
"itertools",
"lalrpop-util",
"petgraph",
"pico-args",
"regex",
"regex-syntax",
"sha3",
"string_cache",
"term",
"unicode-xid",
"walkdir",
]
[[package]]
name = "lalrpop-util"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733"
dependencies = [
"regex-automata",
"rustversion",
]
[[package]]
name = "libc"
version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mio"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"wasi",
"windows-sys",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "petgraph"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "pretty_assertions"
@@ -35,36 +504,105 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.95"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.219"
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -72,10 +610,66 @@ dependencies = [
]
[[package]]
name = "syn"
version = "2.0.101"
name = "sha3"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
dependencies = [
"digest",
"keccak",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "siphasher"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "string_cache"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
dependencies = [
"new_debug_unreachable",
"parking_lot",
"phf_shared",
"precomputed-hash",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
@@ -83,10 +677,221 @@ dependencies = [
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
name = "term"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1"
dependencies = [
"windows-sys",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
dependencies = [
"bytes",
"libc",
"mio",
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasm-bindgen"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "yansi"
+19 -2
View File
@@ -4,6 +4,7 @@ version = "0.1.0"
authors = [
"Øystein Tveit <oysteikt@pvv.ntnu.no>"
]
license = "MIT"
description = "A rust implementation of the mpd protocol, both client and serverside"
repository = "https://git.pvv.ntnu.no/Grzegorz/empidee"
documentation = "https://pages.pvv.ntnu.no/Grzegorz/empidee/main/docs/empidee/"
@@ -11,8 +12,24 @@ edition = "2024"
rust-version = "1.85.0"
[dependencies]
serde = { version = "1.0.210", features = ["derive"] }
chrono = { version = "0.4.44", features = ["serde"] }
futures-util = { version = "0.3.32", optional = true, features = ["io"] }
lalrpop-util = { version = "0.22.2", features = ["lexer"] }
paste = "1.0.15"
serde = { version = "1.0.228", features = ["derive"] }
thiserror = "2.0.18"
tokio = { version = "1.50.0", optional = true, features = ["io-util"] }
[features]
default = ["tokio"]
futures = ["dep:futures-util"]
tokio = ["dep:tokio"]
[dev-dependencies]
indoc = "2.0.5"
anyhow = "1.0.102"
indoc = "2.0.7"
pretty_assertions = "1.4.1"
tokio = { version = "1.50.0", features = ["macros", "net", "rt"] }
[build-dependencies]
lalrpop = "0.22.2"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Programvareverkstedet <projects@pvv.ntnu.no>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+9
View File
@@ -0,0 +1,9 @@
fn main() {
// let debug_mode = std::env::var("PROFILE").unwrap() == "debug";
lalrpop::Configuration::new()
//.emit_comments(debug_mode)
// .generate_in_source_tree()
.process()
.unwrap();
}
+15 -2
View File
@@ -1,3 +1,16 @@
fn main() {
todo!()
use empidee::MpdClient;
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let socket = tokio::net::TcpSocket::new_v4()?;
let mut stream = socket.connect("127.0.0.1:6600".parse()?).await?;
let mut client = MpdClient::new(&mut stream).await?;
println!(
"Connected to MPD server: {}",
client.get_mpd_version().unwrap_or("unknown")
);
client.play(None).await?;
Ok(())
}
Generated
+6 -6
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1746461020,
"narHash": "sha256-7+pG1I9jvxNlmln4YgnlW4o+w0TZX24k688mibiFDUE=",
"lastModified": 1775036866,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3730d8a308f94996a9ba7c7138ede69c1b9ac4ae",
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"type": "github"
},
"original": {
@@ -29,11 +29,11 @@
]
},
"locked": {
"lastModified": 1746585402,
"narHash": "sha256-Pf+ufu6bYNA1+KQKHnGMNEfTwpD9ZIcAeLoE2yPWIP0=",
"lastModified": 1775099554,
"narHash": "sha256-3xBsGnGDLOFtnPZ1D3j2LU19wpAlYefRKTlkv648rU0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "72dd969389583664f87aa348b3458f2813693617",
"rev": "8d6387ed6d8e6e6672fd3ed4b61b59d44b124d99",
"type": "github"
},
"original": {
+42
View File
@@ -36,10 +36,52 @@
default = pkgs.mkShell {
nativeBuildInputs = [
toolchain
pkgs.cargo-edit
];
RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";
};
});
packages = forAllSystems (system: pkgs: toolchain: let
rustPlatform = pkgs.makeRustPlatform {
cargo = toolchain;
rustc = toolchain;
};
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
src = lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [
./.cargo
./Cargo.toml
./Cargo.lock
./LICENSE
./README.md
./build.rs
./examples
./rustfmt.toml
./src
];
};
in {
default = self.packages.${system}.example-binaries;
example-binaries = rustPlatform.buildRustPackage {
pname = "empidee-example-bins";
inherit (cargoToml.package) version;
inherit src;
cargoLock.lockFile = ./Cargo.lock;
cargoBuildFlags = [ "--examples" ];
# TODO: avoid the binary variant with the hash at the end
postInstall = ''
find "$releaseDir"/examples -type f -executable -exec install -Dt "$out/bin" {} \;
'';
doCheck = true;
useNextest = true;
};
});
};
}
+138
View File
@@ -0,0 +1,138 @@
//! A high-level client for interacting with an Mpd server.
//!
//! The client provides methods for common operations such as playing, pausing, and
//! managing the playlist, and returns the expected response types directly
//! from its methods.
use crate::{commands::*, types::SongPosition};
#[cfg(feature = "futures")]
use futures_util::{
AsyncBufReadExt,
io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter},
};
#[cfg(feature = "tokio")]
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufStream};
use thiserror::Error;
pub struct MpdClient<'a, T>
where
T: AsyncWrite + AsyncRead + Unpin,
{
stream: BufStream<&'a mut T>,
mpd_version: Option<String>,
}
#[derive(Error, Debug)]
pub enum MpdClientError {
#[error("Connection error: {0}")]
ConnectionError(#[from] std::io::Error),
#[error("Failed to parse MPD response: {0}")]
ResponseParseError(#[from] crate::commands::ResponseParserError),
#[error("MPD returned an error: {0}")]
MpdError(#[from] crate::response::MpdError),
}
impl<'a, T> MpdClient<'a, T>
where
T: AsyncWrite + AsyncRead + Unpin,
{
pub async fn new(connection: &'a mut T) -> Result<Self, MpdClientError> {
let mut client = MpdClient {
stream: BufStream::new(connection),
mpd_version: None,
};
client.read_initial_mpd_version().await?;
Ok(client)
}
pub async fn wrap_existing(connection: &'a mut T, mpd_version: Option<String>) -> Self {
MpdClient {
stream: BufStream::new(connection),
mpd_version,
}
}
pub fn into_connection(self) -> &'a mut T {
self.stream.into_inner()
}
pub fn get_mpd_version(&self) -> Option<&str> {
self.mpd_version.as_deref()
}
async fn read_initial_mpd_version(&mut self) -> Result<(), MpdClientError> {
let mut version_line = String::new();
self.stream
.read_line(&mut version_line)
.await
.map_err(MpdClientError::ConnectionError)?;
self.mpd_version = Some(version_line.trim().to_string());
Ok(())
}
async fn read_response(&mut self) -> Result<Vec<u8>, MpdClientError> {
let mut response = Vec::new();
loop {
let mut line = Vec::new();
let bytes_read = self
.stream
.read_until(b'\n', &mut line)
.await
.map_err(MpdClientError::ConnectionError)?;
if bytes_read == 0 {
break; // EOF reached
}
response.extend_from_slice(&line);
if line == b"OK\n" || line.starts_with(b"ACK ") {
break; // End of response
}
}
Ok(response)
}
pub async fn execute<C>(&mut self, request: C::Request) -> Result<C::Response, MpdClientError>
where
C: Command,
{
let payload = request.serialize();
self.stream
.write_all(payload.as_bytes())
.await
.map_err(MpdClientError::ConnectionError)?;
self.stream
.flush()
.await
.map_err(MpdClientError::ConnectionError)?;
let response_bytes = self.read_response().await?;
let response = C::Response::parse_raw(&response_bytes)?;
Ok(response)
}
pub async fn play(
&mut self,
position: Option<SongPosition>,
) -> Result<PlayResponse, MpdClientError> {
let request = <Play as Command>::Request::new(position);
self.execute::<Play>(request).await
}
}
+690 -342
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -1,11 +1,11 @@
pub mod disableoutput;
pub mod enableoutput;
pub mod outputs;
pub mod outputset;
pub mod toggleoutput;
mod disableoutput;
mod enableoutput;
mod outputs;
mod outputset;
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::*;
@@ -1,26 +1,15 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::AudioOutputId,
};
pub struct DisableOutput;
single_item_command_request!(DisableOutput, "disableoutput", AudioOutputId);
empty_command_response!(DisableOutput);
impl Command for DisableOutput {
type Response = ();
const COMMAND: &'static str = "disableoutput";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let output_id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
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(())
}
type Request = DisableOutputRequest;
type Response = DisableOutputResponse;
}
@@ -1,26 +1,15 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::AudioOutputId,
};
pub struct EnableOutput;
single_item_command_request!(EnableOutput, "enableoutput", AudioOutputId);
empty_command_response!(EnableOutput);
impl Command for EnableOutput {
type Response = ();
const COMMAND: &'static str = "enableoutput";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let output_id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
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(())
}
type Request = EnableOutputRequest;
type Response = EnableOutputResponse;
}
+165 -13
View File
@@ -2,35 +2,187 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
use crate::{
commands::{Command, CommandResponse, ResponseParserError, empty_command_request},
response_tokenizer::{ResponseAttributes, expect_property_type},
types::AudioOutputId,
};
pub struct Outputs;
empty_command_request!(Outputs, "outputs");
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OutputsResponse(Vec<Output>);
impl OutputsResponse {
pub fn new(outputs: Vec<Output>) -> Self {
OutputsResponse(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,
pub attribute: HashMap<String, String>,
}
pub type OutputsResponse = Vec<Output>;
impl Output {
pub fn new(
id: AudioOutputId,
name: String,
plugin: String,
enabled: bool,
attribute: HashMap<String, String>,
) -> Self {
Output {
id,
name,
plugin,
enabled,
attribute,
}
}
}
impl CommandResponse for OutputsResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts = parts.into_vec()?;
let result_len = parts.iter().filter(|(k, _)| *k == "outputid").count();
let mut outputs = Vec::with_capacity(result_len);
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_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.to_string()))?,
);
}
"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.to_string()))?;
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()))?
.to_string();
let attr_value = parts
.next()
.ok_or(ResponseParserError::SyntaxError(0, value.to_string()))?
.to_string();
attributes.insert(attr_key, attr_value);
}
_ => {
return Err(ResponseParserError::UnexpectedProperty(k.to_string()));
}
}
}
// 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(OutputsResponse(outputs))
}
}
impl Command for Outputs {
type Request = OutputsRequest;
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, ""))
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use super::*;
fn parse_response(
_parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
#[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(OutputsResponse(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
},
},
])),
);
}
}
+63 -28
View File
@@ -1,35 +1,70 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::AudioOutputId,
};
pub struct OutputSet;
impl Command for OutputSet {
type Response = ();
const COMMAND: &'static str = "outputset";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OutputSetRequest {
pub output_id: AudioOutputId,
pub attribute_name: String,
pub attribute_value: String,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let output_id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
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(())
impl OutputSetRequest {
pub fn new(output_id: AudioOutputId, attribute_name: String, attribute_value: String) -> Self {
Self {
output_id,
attribute_name,
attribute_value,
}
}
}
impl CommandRequest for OutputSetRequest {
const COMMAND: &'static str = "outputset";
const MIN_ARGS: u32 = 3;
const MAX_ARGS: Option<u32> = Some(3);
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(Self::missing_arguments_error(0))?;
let output_id = output_id
.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 1,
expected_type: "AudioOutputId",
raw_input: output_id.to_string(),
})?;
let attribute_name = parts.next().ok_or(Self::missing_arguments_error(1))?;
let attribute_value = parts.next().ok_or(Self::missing_arguments_error(2))?;
Self::throw_if_too_many_arguments(parts)?;
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;
}
@@ -1,26 +1,15 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::AudioOutputId,
};
pub struct ToggleOutput;
single_item_command_request!(ToggleOutput, "toggleoutput", AudioOutputId);
empty_command_response!(ToggleOutput);
impl Command for ToggleOutput {
type Response = ();
const COMMAND: &'static str = "toggleoutput";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let output_id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
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(())
}
type Request = ToggleOutputRequest;
type Response = ToggleOutputResponse;
}
+10 -10
View File
@@ -1,11 +1,11 @@
pub mod channels;
pub mod readmessages;
pub mod sendmessage;
pub mod subscribe;
pub mod unsubscribe;
mod channels;
mod readmessages;
mod sendmessage;
mod subscribe;
mod unsubscribe;
pub use channels::Channels;
pub use readmessages::ReadMessages;
pub use sendmessage::SendMessage;
pub use subscribe::Subscribe;
pub use unsubscribe::Unsubscribe;
pub use channels::*;
pub use readmessages::*;
pub use sendmessage::*;
pub use subscribe::*;
pub use unsubscribe::*;
+29 -19
View File
@@ -1,36 +1,37 @@
use serde::{Deserialize, Serialize};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
expect_property_type,
use crate::{
commands::{Command, CommandResponse, ResponseParserError, empty_command_request},
response_tokenizer::{ResponseAttributes, expect_property_type},
types::ChannelName,
};
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 ChannelsResponse {
pub fn new(channels: Vec<ChannelName>) -> Self {
ChannelsResponse { channels }
}
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: Vec<_> = parts.into();
impl CommandResponse for ChannelsResponse {
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.to_string()))?;
channel_names.push(channel_name);
}
Ok(ChannelsResponse {
@@ -39,6 +40,11 @@ impl Command for Channels {
}
}
impl Command for Channels {
type Request = ChannelsRequest;
type Response = ChannelsResponse;
}
#[cfg(test)]
mod tests {
use super::*;
@@ -53,11 +59,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(),
]
}
);
}
+49 -32
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, CommandResponse, ResponseParserError, empty_command_request},
response_tokenizer::{ResponseAttributes, expect_property_type},
types::ChannelName,
};
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>);
impl ReadMessagesResponse {
pub fn new(entries: Vec<ReadMessagesResponseEntry>) -> Self {
ReadMessagesResponse(entries)
}
}
impl Command for ReadMessages {
type Response = ReadMessagesResponse;
const COMMAND: &'static str = "readmessages";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReadMessagesResponseEntry {
channel: ChannelName,
message: String,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ReadMessages, ""))
impl ReadMessagesResponseEntry {
pub fn new(channel: ChannelName, message: String) -> Self {
ReadMessagesResponseEntry { channel, message }
}
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: Vec<_> = parts.into();
impl CommandResponse for ReadMessagesResponse {
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);
@@ -34,23 +42,28 @@ impl Command for ReadMessages {
let (ckey, cvalue) = channel_message_pair[0];
let (mkey, mvalue) = channel_message_pair[1];
if ckey != "channel" {
return Err(ResponseParserError::UnexpectedProperty(ckey));
}
if mkey != "message" {
return Err(ResponseParserError::UnexpectedProperty(mkey));
}
debug_assert!(ckey == "channel");
debug_assert!(mkey == "message");
let channel = expect_property_type!(Some(cvalue), "channel", Text);
let channel = channel
.parse()
.map_err(|_| ResponseParserError::SyntaxError(0, channel.to_string()))?;
let channel = expect_property_type!(Some(cvalue), "channel", Text).to_string();
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;
@@ -66,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(),
},
]))
);
}
}
+43 -18
View File
@@ -1,29 +1,54 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::ChannelName,
};
pub struct SendMessage;
impl Command for SendMessage {
type Response = ();
const COMMAND: &'static str = "sendmessage";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SendMessageRequest {
pub channel: ChannelName,
pub message: String,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let channel = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
impl SendMessageRequest {
pub fn new(channel: ChannelName, message: String) -> Self {
Self { channel, message }
}
}
impl CommandRequest for SendMessageRequest {
const COMMAND: &'static str = "sendmessage";
const MIN_ARGS: u32 = 2;
const MAX_ARGS: Option<u32> = 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(Self::missing_arguments_error(0))?;
let channel = channel
.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "ChannelName",
raw_input: channel.to_string(),
})?;
// 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;
}
+9 -20
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},
types::ChannelName,
};
pub struct Subscribe;
single_item_command_request!(Subscribe, "subscribe", ChannelName);
empty_command_response!(Subscribe);
impl Command for Subscribe {
type Response = ();
const COMMAND: &'static str = "subscribe";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let channel_name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
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(())
}
type Request = SubscribeRequest;
type Response = SubscribeResponse;
}
+9 -20
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},
types::ChannelName,
};
pub struct Unsubscribe;
single_item_command_request!(Unsubscribe, "unsubscribe", ChannelName);
empty_command_response!(Unsubscribe);
impl Command for Unsubscribe {
type Response = ();
const COMMAND: &'static str = "unsubscribe";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let channel_name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
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(())
}
type Request = UnsubscribeRequest;
type Response = UnsubscribeResponse;
}
+36 -36
View File
@@ -1,37 +1,37 @@
pub mod binary_limit;
pub mod close;
pub mod kill;
pub mod password;
pub mod ping;
pub mod protocol;
pub mod protocol_all;
pub mod protocol_available;
pub mod protocol_clear;
pub mod protocol_disable;
pub mod protocol_enable;
pub mod tag_types;
pub mod tag_types_all;
pub mod tag_types_available;
pub mod tag_types_clear;
pub mod tag_types_disable;
pub mod tag_types_enable;
pub mod tag_types_reset;
mod binary_limit;
mod close;
mod kill;
mod password;
mod ping;
mod protocol;
mod protocol_all;
mod protocol_available;
mod protocol_clear;
mod protocol_disable;
mod protocol_enable;
mod tag_types;
mod tag_types_all;
mod tag_types_available;
mod tag_types_clear;
mod tag_types_disable;
mod tag_types_enable;
mod tag_types_reset;
pub use binary_limit::BinaryLimit;
pub use close::Close;
pub use kill::Kill;
pub use password::Password;
pub use ping::Ping;
pub use protocol::Protocol;
pub use protocol_all::ProtocolAll;
pub use protocol_available::ProtocolAvailable;
pub use protocol_clear::ProtocolClear;
pub use protocol_disable::ProtocolDisable;
pub use protocol_enable::ProtocolEnable;
pub use tag_types::TagTypes;
pub use tag_types_all::TagTypesAll;
pub use tag_types_available::TagTypesAvailable;
pub use tag_types_clear::TagTypesClear;
pub use tag_types_disable::TagTypesDisable;
pub use tag_types_enable::TagTypesEnable;
pub use tag_types_reset::TagTypesReset;
pub use binary_limit::*;
pub use close::*;
pub use kill::*;
pub use password::*;
pub use ping::*;
pub use protocol::*;
pub use protocol_all::*;
pub use protocol_available::*;
pub use protocol_clear::*;
pub use protocol_disable::*;
pub use protocol_enable::*;
pub use tag_types::*;
pub use tag_types_all::*;
pub use tag_types_available::*;
pub use tag_types_clear::*;
pub use tag_types_disable::*;
pub use tag_types_enable::*;
pub use tag_types_reset::*;
@@ -1,27 +1,12 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
use crate::commands::{Command, empty_command_response, single_item_command_request};
pub struct BinaryLimit;
single_item_command_request!(BinaryLimit, "binarylimit", u64);
empty_command_response!(BinaryLimit);
impl Command for BinaryLimit {
type Response = ();
const COMMAND: &'static str = "binarylimit";
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), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = BinaryLimitRequest;
type Response = BinaryLimitResponse;
}
+7 -17
View File
@@ -1,22 +1,12 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
use crate::commands::{Command, empty_command_request, empty_command_response};
pub struct Close;
empty_command_request!(Close, "close");
empty_command_response!(Close);
impl Command for Close {
type Response = ();
const COMMAND: &'static str = "close";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Close, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = CloseRequest;
type Response = CloseResponse;
}
+7 -17
View File
@@ -1,22 +1,12 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
use crate::commands::{Command, empty_command_request, empty_command_response};
pub struct Kill;
empty_command_request!(Kill, "kill");
empty_command_response!(Kill);
impl Command for Kill {
type Response = ();
const COMMAND: &'static str = "kill";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Kill, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = KillRequest;
type Response = KillResponse;
}
+7 -22
View File
@@ -1,27 +1,12 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
use crate::commands::{Command, empty_command_response, single_item_command_request};
pub struct Password;
single_item_command_request!(Password, "password", String);
empty_command_response!(Password);
impl Command for Password {
type Response = ();
const COMMAND: &'static str = "password";
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), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = PasswordRequest;
type Response = PasswordResponse;
}
+7 -17
View File
@@ -1,22 +1,12 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
use crate::commands::{Command, empty_command_request, empty_command_response};
pub struct Ping;
empty_command_request!(Ping, "ping");
empty_command_response!(Ping);
impl Command for Ping {
type Response = ();
const COMMAND: &'static str = "ping";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Ping, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = PingRequest;
type Response = PingResponse;
}
+8 -15
View File
@@ -1,22 +1,15 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
response_tokenizer::expect_property_type,
};
pub struct Protocol;
empty_command_request!(Protocol, "protocol");
multi_item_command_response!(Protocol, "feature", String);
impl Command for Protocol {
type Response = ();
const COMMAND: &'static str = "protocol";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Protocol, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
type Request = ProtocolRequest;
type Response = ProtocolResponse;
}
@@ -1,23 +1,12 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
};
use crate::commands::{Command, empty_command_request, empty_command_response};
pub struct ProtocolAll;
empty_command_request!(ProtocolAll, "protocol all");
empty_command_response!(ProtocolAll);
impl Command for ProtocolAll {
type Response = ();
const COMMAND: &'static str = "protocol all";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ProtocolAll, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = ProtocolAllRequest;
type Response = ProtocolAllResponse;
}
@@ -1,22 +1,15 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
response_tokenizer::expect_property_type,
};
pub struct ProtocolAvailable;
empty_command_request!(ProtocolAvailable, "protocol available");
multi_item_command_response!(ProtocolAvailable, "feature", String);
impl Command for ProtocolAvailable {
type Response = ();
const COMMAND: &'static str = "protocol available";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ProtocolAvailable, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
type Request = ProtocolAvailableRequest;
type Response = ProtocolAvailableResponse;
}
@@ -1,23 +1,12 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
};
use crate::commands::{Command, empty_command_request, empty_command_response};
pub struct ProtocolClear;
empty_command_request!(ProtocolClear, "protocol clear");
empty_command_response!(ProtocolClear);
impl Command for ProtocolClear {
type Response = ();
const COMMAND: &'static str = "protocol clear";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ProtocolClear, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = ProtocolClearRequest;
type Response = ProtocolClearResponse;
}
@@ -1,35 +1,59 @@
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::Feature,
};
pub struct ProtocolDisable;
impl Command for ProtocolDisable {
type Response = ();
const COMMAND: &'static str = "protocol disable";
pub struct ProtocolDisableRequest(Vec<Feature>);
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
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());
}
Ok((Request::ProtocolDisable(features), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
impl ProtocolDisableRequest {
pub fn new(features: Vec<Feature>) -> Self {
ProtocolDisableRequest(features)
}
}
impl CommandRequest for ProtocolDisableRequest {
const COMMAND: &'static str = "protocol disable";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = 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(Self::missing_arguments_error(0));
}
let features = parts
.enumerate()
.map(|(i, f)| {
f.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: i.try_into().unwrap_or(u32::MAX),
expected_type: "Feature",
raw_input: f.to_owned(),
})
})
.collect::<Result<Vec<Feature>, RequestParserError>>()?;
Ok(ProtocolDisableRequest(features))
}
}
empty_command_response!(ProtocolDisable);
impl Command for ProtocolDisable {
type Request = ProtocolDisableRequest;
type Response = ProtocolDisableResponse;
}
@@ -1,35 +1,59 @@
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::Feature,
};
pub struct ProtocolEnable;
impl Command for ProtocolEnable {
type Response = ();
const COMMAND: &'static str = "protocol enable";
pub struct ProtocolEnableRequest(Vec<Feature>);
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
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());
}
Ok((Request::ProtocolEnable(features), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
impl ProtocolEnableRequest {
pub fn new(features: Vec<Feature>) -> Self {
ProtocolEnableRequest(features)
}
}
impl CommandRequest for ProtocolEnableRequest {
const COMMAND: &'static str = "protocol enable";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = 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(Self::missing_arguments_error(0));
}
let features = parts
.enumerate()
.map(|(i, f)| {
f.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: i.try_into().unwrap_or(u32::MAX),
expected_type: "Feature",
raw_input: f.to_owned(),
})
})
.collect::<Result<Vec<Feature>, RequestParserError>>()?;
Ok(ProtocolEnableRequest(features))
}
}
empty_command_response!(ProtocolEnable);
impl Command for ProtocolEnable {
type Request = ProtocolEnableRequest;
type Response = ProtocolEnableResponse;
}
+7 -25
View File
@@ -1,33 +1,15 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
expect_property_type,
use crate::{
commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
response_tokenizer::expect_property_type,
};
pub struct TagTypes;
pub type TagTypesResponse = Vec<String>;
empty_command_request!(TagTypes, "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 = expect_property_type!(Some(value), "tagtype", Text).to_string();
tagtypes.push(tagtype);
}
Ok(tagtypes)
}
}
@@ -1,23 +1,12 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
};
use crate::commands::{Command, empty_command_request, empty_command_response};
pub struct TagTypesAll;
empty_command_request!(TagTypesAll, "tagtypes all");
empty_command_response!(TagTypesAll);
impl Command for TagTypesAll {
type Response = ();
const COMMAND: &'static str = "tagtypes all";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::TagTypesAll, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = TagTypesAllRequest;
type Response = TagTypesAllResponse;
}
@@ -1,22 +1,15 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
response_tokenizer::expect_property_type,
};
pub struct TagTypesAvailable;
empty_command_request!(TagTypesAvailable, "tagtypes available");
multi_item_command_response!(TagTypesAvailable, "tagtype", String);
impl Command for TagTypesAvailable {
type Response = ();
const COMMAND: &'static str = "tagtypes available";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::TagTypesAvailable, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
type Request = TagTypesAvailableRequest;
type Response = TagTypesAvailableResponse;
}
@@ -1,23 +1,12 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
};
use crate::commands::{Command, empty_command_request, empty_command_response};
pub struct TagTypesClear;
empty_command_request!(TagTypesClear, "tagtypes clear");
empty_command_response!(TagTypesClear);
impl Command for TagTypesClear {
type Response = ();
const COMMAND: &'static str = "tagtypes clear";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::TagTypesClear, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = TagTypesClearRequest;
type Response = TagTypesClearResponse;
}
@@ -1,35 +1,61 @@
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::TagName,
};
pub struct TagTypesDisable;
impl Command for TagTypesDisable {
type Response = ();
const COMMAND: &'static str = "tagtypes disable";
pub struct TagTypesDisableRequest(Vec<TagName>);
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
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());
}
Ok((Request::TagTypesDisable(tag_types), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
impl TagTypesDisableRequest {
pub fn new(tag_types: Vec<TagName>) -> Self {
TagTypesDisableRequest(tag_types)
}
}
impl CommandRequest for TagTypesDisableRequest {
const COMMAND: &'static str = "tagtypes disable";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = 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(Self::missing_arguments_error(0));
}
let tag_types = parts
.enumerate()
.map(|(i, s)| {
s.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: i.try_into().unwrap_or(u32::MAX),
expected_type: "TagName",
raw_input: s.to_owned(),
})
})
.collect::<Result<Vec<TagName>, RequestParserError>>()?;
Ok(TagTypesDisableRequest(tag_types))
}
}
empty_command_response!(TagTypesDisable);
impl Command for TagTypesDisable {
type Request = TagTypesDisableRequest;
type Response = TagTypesDisableResponse;
}
@@ -1,35 +1,61 @@
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::TagName,
};
pub struct TagTypesEnable;
impl Command for TagTypesEnable {
type Response = ();
const COMMAND: &'static str = "tagtypes enable";
pub struct TagTypesEnableRequest(Vec<TagName>);
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
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());
}
Ok((Request::TagTypesEnable(tag_types), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
impl TagTypesEnableRequest {
pub fn new(tag_types: Vec<TagName>) -> Self {
TagTypesEnableRequest(tag_types)
}
}
impl CommandRequest for TagTypesEnableRequest {
const COMMAND: &'static str = "tagtypes enable";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = 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(Self::missing_arguments_error(0));
}
let tag_types = parts
.enumerate()
.map(|(i, s)| {
s.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: i.try_into().unwrap_or(u32::MAX),
expected_type: "TagName",
raw_input: s.to_owned(),
})
})
.collect::<Result<Vec<TagName>, RequestParserError>>()?;
Ok(TagTypesEnableRequest(tag_types))
}
}
empty_command_response!(TagTypesEnable);
impl Command for TagTypesEnable {
type Request = TagTypesEnableRequest;
type Response = TagTypesEnableResponse;
}
@@ -1,35 +1,61 @@
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::TagName,
};
pub struct TagTypesReset;
impl Command for TagTypesReset {
type Response = ();
const COMMAND: &'static str = "tagtypes reset";
pub struct TagTypesResetRequest(Vec<TagName>);
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
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());
}
Ok((Request::TagTypesReset(tag_types), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
impl TagTypesResetRequest {
pub fn new(tag_types: Vec<TagName>) -> Self {
TagTypesResetRequest(tag_types)
}
}
impl CommandRequest for TagTypesResetRequest {
const COMMAND: &'static str = "tagtypes reset";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = 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(Self::missing_arguments_error(0));
}
let tag_types = parts
.enumerate()
.map(|(i, s)| {
s.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: i.try_into().unwrap_or(u32::MAX),
expected_type: "TagName",
raw_input: s.to_owned(),
})
})
.collect::<Result<Vec<TagName>, RequestParserError>>()?;
Ok(TagTypesResetRequest(tag_types))
}
}
empty_command_response!(TagTypesReset);
impl Command for TagTypesReset {
type Request = TagTypesResetRequest;
type Response = TagTypesResetResponse;
}
+18 -18
View File
@@ -1,19 +1,19 @@
pub mod next;
pub mod pause;
pub mod play;
pub mod playid;
pub mod previous;
pub mod seek;
pub mod seekcur;
pub mod seekid;
pub mod stop;
mod next;
mod pause;
mod play;
mod playid;
mod previous;
mod seek;
mod seekcur;
mod seekid;
mod stop;
pub use next::Next;
pub use pause::Pause;
pub use play::Play;
pub use playid::PlayId;
pub use previous::Previous;
pub use seek::Seek;
pub use seekcur::SeekCur;
pub use seekid::SeekId;
pub use stop::Stop;
pub use next::*;
pub use pause::*;
pub use play::*;
pub use playid::*;
pub use previous::*;
pub use seek::*;
pub use seekcur::*;
pub use seekid::*;
pub use stop::*;
+7 -17
View File
@@ -1,22 +1,12 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
use crate::commands::{Command, empty_command_request, empty_command_response};
pub struct Next;
empty_command_request!(Next, "next");
empty_command_response!(Next);
impl Command for Next {
type Response = ();
const COMMAND: &'static str = "next";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Next, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = NextRequest;
type Response = NextResponse;
}
+45 -24
View File
@@ -1,31 +1,52 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
};
pub struct Pause;
impl Command for Pause {
type Response = ();
const COMMAND: &'static str = "pause";
pub struct PauseRequest(Option<bool>);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let result = match parts.next() {
Some("0") => Ok((Request::Pause(Some(false)), "")),
Some("1") => Ok((Request::Pause(Some(true)), "")),
Some(s) => Err(RequestParserError::SyntaxError(0, s.to_string())),
None => Ok((Request::Pause(None), "")),
};
debug_assert!(parts.next().is_none());
result
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
impl PauseRequest {
pub fn new(state: Option<bool>) -> Self {
PauseRequest(state)
}
}
impl CommandRequest for PauseRequest {
const COMMAND: &'static str = "pause";
const MIN_ARGS: u32 = 0;
const MAX_ARGS: Option<u32> = Some(1);
fn serialize(&self) -> String {
match self.0 {
Some(true) => format!("{} 1\n", Self::COMMAND),
Some(false) => format!("{} 0\n", Self::COMMAND),
None => Self::COMMAND.to_string() + "\n",
}
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let result = match parts.next() {
Some("0") => Ok(Some(false)),
Some("1") => Ok(Some(true)),
Some(s) => Err(RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "Option<bool>",
raw_input: s.to_owned(),
}),
None => Ok(None),
};
Self::throw_if_too_many_arguments(parts)?;
result.map(PauseRequest)
}
}
empty_command_response!(Pause);
impl Command for Pause {
type Request = PauseRequest;
type Response = PauseResponse;
}
+8 -27
View File
@@ -1,34 +1,15 @@
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::SongPosition,
commands::{Command, empty_command_response, single_optional_item_command_request},
types::SongPosition,
};
pub struct Play;
single_optional_item_command_request!(Play, "play", SongPosition);
empty_command_response!(Play);
impl Command for Play {
type Response = ();
const COMMAND: &'static str = "play";
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),
};
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(())
}
type Request = PlayRequest;
type Response = PlayResponse;
}
+8 -27
View File
@@ -1,34 +1,15 @@
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::SongId,
commands::{Command, empty_command_response, single_optional_item_command_request},
types::SongId,
};
pub struct PlayId;
single_optional_item_command_request!(PlayId, "playid", SongId);
empty_command_response!(PlayId);
impl Command for PlayId {
type Response = ();
const COMMAND: &'static str = "playid";
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),
};
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(())
}
type Request = PlayIdRequest;
type Response = PlayIdResponse;
}
+7 -17
View File
@@ -1,22 +1,12 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
use crate::commands::{Command, empty_command_request, empty_command_response};
pub struct Previous;
empty_command_request!(Previous, "previous");
empty_command_response!(Previous);
impl Command for Previous {
type Response = ();
const COMMAND: &'static str = "previous";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Previous, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = PreviousRequest;
type Response = PreviousResponse;
}
+53 -26
View File
@@ -1,41 +1,68 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::{SongPosition, TimeWithFractions},
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::{SongPosition, TimeWithFractions},
};
pub struct Seek;
impl Command for Seek {
type Response = ();
const COMMAND: &'static str = "seek";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SeekRequest {
pub songpos: SongPosition,
pub time: TimeWithFractions,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
impl SeekRequest {
pub fn new(songpos: SongPosition, time: TimeWithFractions) -> Self {
Self { songpos, time }
}
}
impl CommandRequest for SeekRequest {
const COMMAND: &'static str = "seek";
const MIN_ARGS: u32 = 2;
const MAX_ARGS: Option<u32> = Some(2);
fn serialize(&self) -> String {
format!("{} {} {}\n", Self::COMMAND, self.songpos, self.time)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let songpos = match parts.next() {
Some(s) => s
.parse::<SongPosition>()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
Some(s) => {
s.parse::<SongPosition>()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "SongPosition",
raw_input: s.to_owned(),
})?
}
None => return Err(Self::missing_arguments_error(0)),
};
let time = match parts.next() {
Some(t) => t
.parse::<TimeWithFractions>()
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
Some(t) => t.parse::<TimeWithFractions>().map_err(|_| {
RequestParserError::SubtypeParserError {
argument_index: 1,
expected_type: "TimeWithFractions",
raw_input: t.to_owned(),
}
})?,
None => return Err(Self::missing_arguments_error(1)),
};
debug_assert!(parts.next().is_none());
Self::throw_if_too_many_arguments(parts)?;
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;
}
+66 -28
View File
@@ -1,53 +1,91 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::{SeekMode, TimeWithFractions},
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::{SeekMode, TimeWithFractions},
};
pub struct SeekCur;
impl Command for SeekCur {
type Response = ();
const COMMAND: &'static str = "seekcur";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SeekCurRequest {
pub mode: SeekMode,
pub time: TimeWithFractions,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
impl SeekCurRequest {
pub fn new(mode: SeekMode, time: TimeWithFractions) -> Self {
Self { mode, time }
}
}
impl CommandRequest for SeekCurRequest {
const COMMAND: &'static str = "seekcur";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(1);
fn serialize(&self) -> String {
let time_str = match self.mode {
SeekMode::Absolute => format!("{}", self.time),
SeekMode::Relative => format!("+{}", self.time),
SeekMode::RelativeReverse => format!("-{}", self.time),
};
format!("{} {}\n", 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),
None => return Err(Self::missing_arguments_error(0)),
};
Self::throw_if_too_many_arguments(parts)?;
// TODO: DRY
let (mode, time) = match time_raw {
t if t.starts_with('+') => (
SeekMode::Relative,
t[1..]
.parse::<TimeWithFractions>()
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?,
t[1..].parse::<TimeWithFractions>().map_err(|_| {
RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "TimeWithFractions",
raw_input: t[1..].to_owned(),
}
})?,
),
t if t.starts_with('-') => (
SeekMode::Relative,
-t[1..]
.parse::<TimeWithFractions>()
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?,
SeekMode::RelativeReverse,
t[1..].parse::<TimeWithFractions>().map_err(|_| {
RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "TimeWithFractions",
raw_input: t[1..].to_owned(),
}
})?,
),
t => (
SeekMode::Absolute,
t.parse::<TimeWithFractions>()
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?,
t.parse::<TimeWithFractions>().map_err(|_| {
RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "TimeWithFractions",
raw_input: t.to_owned(),
}
})?,
),
};
debug_assert!(parts.next().is_none());
debug_assert!(time >= 0.0);
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;
}
+49 -24
View File
@@ -1,41 +1,66 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::{SongId, TimeWithFractions},
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::{SongId, TimeWithFractions},
};
pub struct SeekId;
impl Command for SeekId {
type Response = ();
const COMMAND: &'static str = "seekid";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SeekIdRequest {
pub songid: SongId,
pub time: TimeWithFractions,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
impl SeekIdRequest {
pub fn new(songid: SongId, time: TimeWithFractions) -> Self {
Self { songid, time }
}
}
impl CommandRequest for SeekIdRequest {
const COMMAND: &'static str = "seekid";
const MIN_ARGS: u32 = 2;
const MAX_ARGS: Option<u32> = Some(2);
fn serialize(&self) -> String {
format!("{} {} {}\n", Self::COMMAND, self.songid, self.time)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let songid = match parts.next() {
Some(s) => s
.parse::<SongId>()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "SongId",
raw_input: s.to_owned(),
})?,
None => return Err(Self::missing_arguments_error(0)),
};
let time = match parts.next() {
Some(t) => t
.parse::<TimeWithFractions>()
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
Some(t) => t.parse::<TimeWithFractions>().map_err(|_| {
RequestParserError::SubtypeParserError {
argument_index: 1,
expected_type: "TimeWithFractions",
raw_input: t.to_owned(),
}
})?,
None => return Err(Self::missing_arguments_error(1)),
};
debug_assert!(parts.next().is_none());
Self::throw_if_too_many_arguments(parts)?;
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;
}
+7 -17
View File
@@ -1,22 +1,12 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
use crate::commands::{Command, empty_command_request, empty_command_response};
pub struct Stop;
empty_command_request!(Stop, "stop");
empty_command_response!(Stop);
impl Command for Stop {
type Response = ();
const COMMAND: &'static str = "stop";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Stop, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = StopRequest;
type Response = StopResponse;
}
+8 -8
View File
@@ -1,9 +1,9 @@
pub mod listmounts;
pub mod listneighbors;
pub mod mount;
pub mod unmount;
mod listmounts;
mod listneighbors;
mod mount;
mod unmount;
pub use listmounts::ListMounts;
pub use listneighbors::ListNeighbors;
pub use mount::Mount;
pub use unmount::Unmount;
pub use listmounts::*;
pub use listneighbors::*;
pub use mount::*;
pub use unmount::*;
+29 -12
View File
@@ -1,22 +1,39 @@
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
response_tokenizer::expect_property_type,
};
pub struct ListMounts;
empty_command_request!(ListMounts, "listmounts");
multi_item_command_response!(ListMounts, "mount", String);
impl Command for ListMounts {
type Response = Vec<(String, String)>;
const COMMAND: &'static str = "listmounts";
type Request = ListMountsRequest;
type Response = ListMountsResponse;
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ListMounts, ""))
}
#[cfg(test)]
mod tests {
use indoc::indoc;
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
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(),
]))
);
}
}
@@ -1,22 +1,50 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
commands::{Command, CommandResponse, ResponseParserError, empty_command_request},
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, ""))
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ListNeighborsResponse(HashMap<String, String>);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
impl ListNeighborsResponse {
pub fn new(map: HashMap<String, String>) -> Self {
ListNeighborsResponse(map)
}
}
impl CommandResponse for ListNeighborsResponse {
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;
}
+50 -22
View File
@@ -1,36 +1,64 @@
use serde::{Deserialize, Serialize};
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::{MountPath, Uri},
};
pub struct Mount;
impl Command for Mount {
type Response = ();
const COMMAND: &'static str = "mount";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MountRequest {
pub path: MountPath,
pub uri: Uri,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let path = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
impl MountRequest {
pub fn new(path: MountPath, uri: Uri) -> Self {
MountRequest { path, uri }
}
}
impl CommandRequest for MountRequest {
const COMMAND: &'static str = "mount";
const MIN_ARGS: u32 = 2;
const MAX_ARGS: Option<u32> = Some(2);
fn serialize(&self) -> String {
debug_assert!(self.path.to_str().is_some());
format!(
"{} {} {}\n",
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(Self::missing_arguments_error(0))?;
let path = path
.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "MountPath",
raw_input: path.to_string(),
})?;
let uri = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.ok_or(Self::missing_arguments_error(1))?
.to_string();
debug_assert!(parts.next().is_none());
Self::throw_if_too_many_arguments(parts)?;
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;
}
+44 -21
View File
@@ -1,31 +1,54 @@
use crate::{
Request,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
Command, CommandRequest, RequestParserError, RequestTokenizer, empty_command_response,
},
types::MountPath,
};
pub struct Unmount;
impl Command for Unmount {
type Response = ();
const COMMAND: &'static str = "unmount";
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct UnmountRequest(MountPath);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let path = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
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(())
impl UnmountRequest {
pub fn new(path: MountPath) -> Self {
UnmountRequest(path)
}
}
impl CommandRequest for UnmountRequest {
const COMMAND: &'static str = "unmount";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(1);
fn serialize(&self) -> String {
debug_assert!(self.0.to_str().is_some());
format!(
"{} {}\n",
Self::COMMAND,
self.0.to_str().unwrap_or("<invalid path>")
)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let path = parts.next().ok_or(Self::missing_arguments_error(0))?;
let path =
path.parse::<MountPath>()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "MountPath",
raw_input: path.to_string(),
})?;
Self::throw_if_too_many_arguments(parts)?;
Ok(UnmountRequest(path))
}
}
empty_command_response!(Unmount);
impl Command for Unmount {
type Request = UnmountRequest;
type Response = UnmountResponse;
}
+36 -36
View File
@@ -1,37 +1,37 @@
pub mod albumart;
pub mod count;
pub mod find;
pub mod findadd;
pub mod getfingerprint;
pub mod list;
pub mod listall;
pub mod listallinfo;
pub mod listfiles;
pub mod lsinfo;
pub mod readcomments;
pub mod readpicture;
pub mod rescan;
pub mod search;
pub mod searchadd;
pub mod searchaddpl;
pub mod searchcount;
pub mod update;
mod albumart;
mod count;
mod find;
mod findadd;
mod getfingerprint;
mod list;
mod listall;
mod listallinfo;
mod listfiles;
mod lsinfo;
mod readcomments;
mod readpicture;
mod rescan;
mod search;
mod searchadd;
mod searchaddpl;
mod searchcount;
mod update;
pub use albumart::AlbumArt;
pub use count::Count;
pub use find::Find;
pub use findadd::FindAdd;
pub use getfingerprint::GetFingerprint;
pub use list::List;
pub use listall::ListAll;
pub use listallinfo::ListAllInfo;
pub use listfiles::ListFiles;
pub use lsinfo::LsInfo;
pub use readcomments::ReadComments;
pub use readpicture::ReadPicture;
pub use rescan::Rescan;
pub use search::Search;
pub use searchadd::SearchAdd;
pub use searchaddpl::SearchAddPl;
pub use searchcount::SearchCount;
pub use update::Update;
pub use albumart::*;
pub use count::*;
pub use find::*;
pub use findadd::*;
pub use getfingerprint::*;
pub use list::*;
pub use listall::*;
pub use listallinfo::*;
pub use listfiles::*;
pub use lsinfo::*;
pub use readcomments::*;
pub use readpicture::*;
pub use rescan::*;
pub use search::*;
pub use searchadd::*;
pub use searchaddpl::*;
pub use searchcount::*;
pub use update::*;
+58 -21
View File
@@ -1,46 +1,78 @@
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},
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_and_parse_property, get_property},
types::{Offset, Uri},
};
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;
const COMMAND: &'static str = "albumart";
impl AlbumArtRequest {
pub fn new(uri: Uri, offset: Offset) -> Self {
AlbumArtRequest { uri, offset }
}
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
impl CommandRequest for AlbumArtRequest {
const COMMAND: &'static str = "albumart";
const MIN_ARGS: u32 = 2;
const MAX_ARGS: Option<u32> = Some(2);
fn serialize(&self) -> String {
format!("{} {} {}\n", 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),
None => return Err(Self::missing_arguments_error(0)),
};
let offset = match parts.next() {
Some(s) => s
.parse::<Offset>()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 1,
expected_type: "Offset",
raw_input: s.to_string(),
})?,
None => return Err(Self::missing_arguments_error(1)),
};
debug_assert!(parts.next().is_none());
Self::throw_if_too_many_arguments(parts)?;
Ok((Request::AlbumArt(uri.to_string(), offset), ""))
Ok(AlbumArtRequest {
uri: uri.to_string(),
offset,
})
}
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AlbumArtResponse {
pub size: usize,
pub binary: Vec<u8>,
}
impl AlbumArtResponse {
pub fn new(size: usize, binary: Vec<u8>) -> Self {
AlbumArtResponse { size, binary }
}
}
impl CommandResponse for AlbumArtResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
let size = get_and_parse_property!(parts, "size", Text);
@@ -49,3 +81,8 @@ impl Command for AlbumArt {
Ok(AlbumArtResponse { size, binary })
}
}
impl Command for AlbumArt {
type Request = AlbumArtRequest;
type Response = AlbumArtResponse;
}
+71 -21
View File
@@ -1,48 +1,93 @@
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},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_and_parse_property},
types::GroupType,
};
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;
const COMMAND: &'static str = "count";
impl CountRequest {
pub fn new(filter: Filter, group: Option<GroupType>) -> Self {
CountRequest { filter, group }
}
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
impl CommandRequest for CountRequest {
const COMMAND: &'static str = "count";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(2);
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.push('\n');
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(Self::missing_arguments_error(0)),
};
let group = if let Some("group") = parts.next() {
let group = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let group = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
argument_index: 1,
keyword: "group",
})?;
Some(
group
.parse()
.map_err(|_| RequestParserError::SyntaxError(1, group.to_owned()))?,
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 1,
expected_type: "GroupType",
raw_input: group.to_owned(),
})?,
)
} else {
None
};
debug_assert!(parts.next().is_none());
Self::throw_if_too_many_arguments(parts)?;
Ok((Request::Count(filter, group), ""))
Ok(CountRequest { filter, group })
}
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CountResponse {
pub songs: usize,
pub playtime: u64,
}
impl CountResponse {
pub fn new(songs: usize, playtime: u64) -> Self {
CountResponse { songs, playtime }
}
}
impl CommandResponse for CountResponse {
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 +95,8 @@ impl Command for Count {
Ok(CountResponse { songs, playtime })
}
}
impl Command for Count {
type Request = CountRequest;
type Response = CountResponse;
}
+109 -24
View File
@@ -1,51 +1,136 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
types::{DbSelectionPrintResponse, DbSongInfo, Sort, WindowRange},
};
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 FindRequest {
pub fn new(filter: Filter, sort: Option<Sort>, window: Option<WindowRange>) -> Self {
Self {
filter,
sort,
window,
}
}
}
impl CommandRequest for FindRequest {
const COMMAND: &'static str = "find";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(3);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
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.push('\n');
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(Self::missing_arguments_error(0)),
};
let mut argument_index_counter = 0;
let mut sort_or_window = parts.next();
let mut sort = None;
if let Some("sort") = sort_or_window {
argument_index_counter += 1;
let s = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "sort",
argument_index: argument_index_counter,
})?;
sort = Some(
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
s.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "Sort",
raw_input: s.to_string(),
})?,
);
sort_or_window = parts.next();
}
let mut window = None;
if let Some("window") = sort_or_window {
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
window = Some(
w.parse()
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?,
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
);
}
debug_assert!(parts.next().is_none());
Self::throw_if_too_many_arguments(parts)?;
Ok((Request::Find(filter, sort, window), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
Ok(FindRequest {
filter,
sort,
window,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FindResponse(Vec<DbSongInfo>);
impl FindResponse {
pub fn new(items: Vec<DbSongInfo>) -> Self {
FindResponse(items)
}
}
impl CommandResponse for FindResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
DbSelectionPrintResponse::parse(parts)?
.into_iter()
.map(|i| match i {
DbSelectionPrintResponse::Song(db_song_info) => Ok(db_song_info),
DbSelectionPrintResponse::Directory(_db_directory_info) => Err(
ResponseParserError::UnexpectedProperty("directory".to_string()),
),
DbSelectionPrintResponse::Playlist(_db_playlist_info) => Err(
ResponseParserError::UnexpectedProperty("playlist".to_string()),
),
})
.collect::<Result<Vec<DbSongInfo>, ResponseParserError>>()
.map(FindResponse)
}
}
impl Command for Find {
type Request = FindRequest;
type Response = FindResponse;
}
+107 -26
View File
@@ -1,60 +1,141 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
filter::Filter,
request_tokenizer::RequestTokenizer,
types::{SongPosition, Sort, WindowRange},
};
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 FindAddRequest {
pub fn new(
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
) -> Self {
Self {
filter,
sort,
window,
position,
}
}
}
impl CommandRequest for FindAddRequest {
const COMMAND: &'static str = "findadd";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(4);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
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.push('\n');
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(Self::missing_arguments_error(0)),
};
let mut argument_index_counter = 0;
let mut sort_or_window_or_position = parts.next();
let mut sort = None;
if let Some("sort") = sort_or_window_or_position {
argument_index_counter += 1;
let s = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "sort",
argument_index: argument_index_counter,
})?;
sort = Some(
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
s.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "Sort",
raw_input: s.to_string(),
})?,
);
sort_or_window_or_position = parts.next();
}
let mut window = None;
if let Some("window") = sort_or_window_or_position {
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
window = Some(
w.parse()
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?,
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
);
sort_or_window_or_position = parts.next();
}
let mut position = None;
if let Some("position") = sort_or_window_or_position {
let p = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
argument_index_counter += 1;
let p = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "position",
argument_index: argument_index_counter,
})?;
position = Some(
p.parse()
.map_err(|_| RequestParserError::SyntaxError(0, p.to_string()))?,
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "SongPosition",
raw_input: p.to_string(),
})?,
);
}
debug_assert!(parts.next().is_none());
Self::throw_if_too_many_arguments(parts)?;
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;
}
+21 -20
View File
@@ -1,38 +1,39 @@
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, ResponseParserError, single_item_command_request},
response_tokenizer::{ResponseAttributes, get_and_parse_property},
types::Uri,
};
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 GetFingerprintResponse {
pub fn new(chromaprint: String) -> Self {
Self { chromaprint }
}
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
impl CommandResponse for GetFingerprintResponse {
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;
}
+147 -38
View File
@@ -1,58 +1,167 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError, expect_property_type,
},
filter::parse_filter,
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
types::{GroupType, TagName, WindowRange},
};
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 ListRequest {
pub fn new(
tagname: TagName,
filter: Option<Filter>,
groups: Vec<GroupType>,
window: Option<WindowRange>,
) -> Self {
Self {
tagname,
filter,
groups,
window,
}
}
}
impl CommandRequest for ListRequest {
const COMMAND: &'static str = "list";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = None;
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()))?;
// TODO: This should be optional
let filter = parse_filter(&mut parts)?;
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),
};
debug_assert!(parts.next().is_none());
Ok((Request::List(tagtype, filter, group), ""))
for group in &self.groups {
cmd.push_str(&format!(" group {}", group));
}
if let Some(window) = &self.window {
cmd.push_str(&format!(" window {}", window));
}
cmd.push('\n');
cmd
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let tagname = parts.next().ok_or(Self::missing_arguments_error(0))?;
let tagname = tagname
.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "TagName",
raw_input: tagname.to_owned(),
})?;
let mut filter = None;
let mut groups = Vec::new();
let mut window = None;
let mut next = parts.next();
let mut argument_index_counter = 0;
if let Some(f) = next
&& f != "group"
&& f != "window"
{
argument_index_counter += 1;
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"
{
argument_index_counter += 1;
let group = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "group",
argument_index: argument_index_counter,
})?;
let parsed_group =
group
.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 2,
expected_type: "GroupType",
raw_input: 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::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
let parsed_window =
window_str
.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 3,
expected_type: "WindowRange",
raw_input: window_str.to_owned(),
})?;
window = Some(parsed_window);
}
Self::throw_if_too_many_arguments(parts)?;
Ok(ListRequest {
tagname,
filter,
groups,
window,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ListResponse(Vec<String>);
impl ListResponse {
pub fn new(items: Vec<String>) -> Self {
ListResponse(items)
}
}
impl CommandResponse for ListResponse {
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;
}
+147 -20
View File
@@ -1,34 +1,161 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::ResponseAttributes,
types::{DbSelectionPrintResponse, Uri},
};
pub struct ListAll;
single_optional_item_command_request!(ListAll, "listall", Uri);
// TODO: This is supposed to be a tree-like structure, with directories containing files and playlists
pub type ListAllResponse = Vec<String>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ListAllResponse(Vec<DbSelectionPrintResponse>);
impl ListAllResponse {
pub fn new(items: Vec<DbSelectionPrintResponse>) -> Self {
ListAllResponse(items)
}
}
impl CommandResponse for ListAllResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let result = DbSelectionPrintResponse::parse(parts)?;
Ok(ListAllResponse(result))
}
}
impl Command for ListAll {
type Request = ListAllRequest;
type Response = ListAllResponse;
const COMMAND: &'static str = "listall";
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = parts
.next()
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))
})
.transpose()?;
#[cfg(test)]
mod tests {
use std::path::PathBuf;
debug_assert!(parts.next().is_none());
use crate::types::{DbDirectoryInfo, DbPlaylistInfo, DbSongInfo};
Ok((Request::ListAll(uri), ""))
}
use super::*;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_parse_response() {
let response = indoc! {"
directory: albums
directory: albums/a
file: albums/a/song1.mp3
file: albums/a/song2.mp3
file: albums/a/song3.mp3
playlist: albums/a/album a.m3u8
directory: albums/b
file: albums/b/song1.mp3
file: albums/b/song2.mp3
file: albums/b/song3.mp3
playlist: albums/b/album b.m3u8
OK
"};
let result = ListAll::parse_raw_response(response.as_bytes());
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
assert_eq!(
result,
Ok(ListAllResponse(vec![
DbSelectionPrintResponse::Directory(DbDirectoryInfo {
directory: PathBuf::from("albums"),
last_modified: None
}),
DbSelectionPrintResponse::Directory(DbDirectoryInfo {
directory: PathBuf::from("albums/a"),
last_modified: None
}),
DbSelectionPrintResponse::Song(DbSongInfo {
file: PathBuf::from("albums/a/song1.mp3"),
range: None,
last_modified: None,
added: None,
format: None,
tags: vec![],
time: None,
duration: None,
playlist: None
}),
DbSelectionPrintResponse::Song(DbSongInfo {
file: PathBuf::from("albums/a/song2.mp3"),
range: None,
last_modified: None,
added: None,
format: None,
tags: vec![],
time: None,
duration: None,
playlist: None
}),
DbSelectionPrintResponse::Song(DbSongInfo {
file: PathBuf::from("albums/a/song3.mp3"),
range: None,
last_modified: None,
added: None,
format: None,
tags: vec![],
time: None,
duration: None,
playlist: None
}),
DbSelectionPrintResponse::Playlist(DbPlaylistInfo {
playlist: PathBuf::from("albums/a/album a.m3u8"),
last_modified: None
}),
DbSelectionPrintResponse::Directory(DbDirectoryInfo {
directory: PathBuf::from("albums/b"),
last_modified: None
}),
DbSelectionPrintResponse::Song(DbSongInfo {
file: PathBuf::from("albums/b/song1.mp3"),
range: None,
last_modified: None,
added: None,
format: None,
tags: vec![],
time: None,
duration: None,
playlist: None
}),
DbSelectionPrintResponse::Song(DbSongInfo {
file: PathBuf::from("albums/b/song2.mp3"),
range: None,
last_modified: None,
added: None,
format: None,
tags: vec![],
time: None,
duration: None,
playlist: None
}),
DbSelectionPrintResponse::Song(DbSongInfo {
file: PathBuf::from("albums/b/song3.mp3"),
range: None,
last_modified: None,
added: None,
format: None,
tags: vec![],
time: None,
duration: None,
playlist: None
}),
DbSelectionPrintResponse::Playlist(DbPlaylistInfo {
playlist: PathBuf::from("albums/b/album b.m3u8"),
last_modified: None
})
]))
)
}
}
+104 -21
View File
@@ -1,35 +1,118 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::ResponseAttributes,
types::{DbSelectionPrintResponse, Uri},
};
pub struct ListAllInfo;
single_optional_item_command_request!(ListAllInfo, "listallinfo", Uri);
// TODO: This is supposed to be a tree-like structure, with directories containing files and playlists
// in addition to the metadata of each entry
pub type ListAllInfoResponse = Vec<String>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ListAllInfoResponse(Vec<DbSelectionPrintResponse>);
impl ListAllInfoResponse {
pub fn new(items: Vec<DbSelectionPrintResponse>) -> Self {
ListAllInfoResponse(items)
}
}
impl CommandResponse for ListAllInfoResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let result = DbSelectionPrintResponse::parse(parts)?;
Ok(ListAllInfoResponse(result))
}
}
impl Command for ListAllInfo {
type Request = ListAllInfoRequest;
type Response = ListAllInfoResponse;
const COMMAND: &'static str = "listallinfo";
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = parts
.next()
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))
})
.transpose()?;
#[cfg(test)]
mod tests {
use std::path::PathBuf;
debug_assert!(parts.next().is_none());
use crate::types::{DbDirectoryInfo, DbPlaylistInfo, DbSongInfo, Tag};
Ok((Request::ListAllInfo(uri), ""))
}
use super::*;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_parse_response() {
let response = indoc! {"
directory: albums
Last-Modified: 2024-10-01T10:00:00Z
directory: albums/a
Last-Modified: 2024-10-01T10:00:00Z
file: albums/a/song1.mp3
Last-Modified: 2022-12-31T09:00:00Z
Added: 2021-12-31T09:00:00Z
Format: 44100:16:2
Title: Song A
Artist: Artist A
Album: Album A
AlbumArtist: Artist A
Composer: Artist A
Genre: Pop
Track: 1
Disc: 1
Date: 2020-01-01
Time: 360
duration: 360.123
playlist: albums/a/album a.m3u8
Last-Modified: 2022-12-31T09:00:00Z
OK
"};
let result = ListAllInfo::parse_raw_response(response.as_bytes());
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
assert_eq!(
result,
Ok(ListAllInfoResponse(vec![
DbSelectionPrintResponse::Directory(DbDirectoryInfo {
directory: PathBuf::from("albums"),
last_modified: Some("2024-10-01T10:00:00Z".to_string()),
}),
DbSelectionPrintResponse::Directory(DbDirectoryInfo {
directory: PathBuf::from("albums/a"),
last_modified: Some("2024-10-01T10:00:00Z".to_string()),
}),
DbSelectionPrintResponse::Song(DbSongInfo {
file: PathBuf::from("albums/a/song1.mp3"),
range: None,
last_modified: Some("2022-12-31T09:00:00Z".to_string()),
added: Some("2021-12-31T09:00:00Z".to_string()),
format: Some("44100:16:2".to_string()),
tags: vec![
Tag::Album("Album A".to_string()),
Tag::AlbumArtist("Artist A".to_string()),
Tag::Artist("Artist A".to_string()),
Tag::Composer("Artist A".to_string()),
Tag::Date("2020-01-01".to_string()),
Tag::Disc("1".to_string()),
Tag::Genre("Pop".to_string()),
Tag::Title("Song A".to_string()),
Tag::Track("1".to_string()),
],
time: Some(360),
duration: Some(360.123),
playlist: None,
}),
DbSelectionPrintResponse::Playlist(DbPlaylistInfo {
playlist: PathBuf::from("albums/a/album a.m3u8"),
last_modified: Some("2022-12-31T09:00:00Z".to_string())
})
])),
)
}
}
+37 -26
View File
@@ -1,34 +1,45 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::ResponseAttributes,
types::{DbDirectoryInfo, DbSelectionPrintResponse, Uri},
};
pub struct ListFiles;
// TODO: fix this type
pub type ListFilesResponse = Vec<String>;
single_optional_item_command_request!(ListFiles, "listfiles", Uri);
impl Command for ListFiles {
type Response = ListFilesResponse;
const COMMAND: &'static str = "listfiles";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ListFilesResponse(Vec<DbDirectoryInfo>);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = parts
.next()
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))
})
.transpose()?;
debug_assert!(parts.next().is_none());
Ok((Request::ListFiles(uri), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
impl ListFilesResponse {
pub fn new(items: Vec<DbDirectoryInfo>) -> Self {
ListFilesResponse(items)
}
}
impl CommandResponse for ListFilesResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
DbSelectionPrintResponse::parse(parts)?
.into_iter()
.map(|i| match i {
DbSelectionPrintResponse::Directory(db_directory_info) => Ok(db_directory_info),
DbSelectionPrintResponse::Song(_db_song_info) => {
Err(ResponseParserError::UnexpectedProperty("file".to_string()))
}
DbSelectionPrintResponse::Playlist(_db_playlist_info) => Err(
ResponseParserError::UnexpectedProperty("playlist".to_string()),
),
})
.collect::<Result<Vec<_>, ResponseParserError>>()
.map(ListFilesResponse)
}
}
impl Command for ListFiles {
type Request = ListFilesRequest;
type Response = ListFilesResponse;
}
+83 -24
View File
@@ -1,34 +1,93 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::ResponseAttributes,
types::{DbSelectionPrintResponse, Uri},
};
pub struct LsInfo;
// TODO: fix this type
pub type LsInfoResponse = Vec<String>;
single_optional_item_command_request!(LsInfo, "lsinfo", Uri);
impl Command for LsInfo {
type Response = LsInfoResponse;
const COMMAND: &'static str = "lsinfo";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LsInfoResponse(Vec<DbSelectionPrintResponse>);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = parts
.next()
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))
})
.transpose()?;
debug_assert!(parts.next().is_none());
Ok((Request::LsInfo(uri), ""))
impl LsInfoResponse {
pub fn new(items: Vec<DbSelectionPrintResponse>) -> Self {
LsInfoResponse(items)
}
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
impl CommandResponse for LsInfoResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let result = DbSelectionPrintResponse::parse(parts)?;
Ok(LsInfoResponse(result))
}
}
impl Command for LsInfo {
type Request = LsInfoRequest;
type Response = LsInfoResponse;
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::types::{DbDirectoryInfo, DbPlaylistInfo};
use super::*;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_parse_response() {
let response = indoc! {"
directory: albums
Last-Modified: 2024-10-01T10:00:00Z
directory: albums/a
Last-Modified: 2024-10-01T10:00:00Z
playlist: albums/a/album a.m3u8
Last-Modified: 2022-12-31T09:00:00Z
directory: albums/b
Last-Modified: 2023-10-01T10:00:00Z
playlist: albums/b/album b.m3u8
Last-Modified: 2021-12-31T09:00:00Z
OK
"};
let result = LsInfo::parse_raw_response(response.as_bytes());
assert_eq!(
result,
Ok(LsInfoResponse(vec![
DbSelectionPrintResponse::Directory(DbDirectoryInfo {
directory: PathBuf::from("albums"),
last_modified: Some("2024-10-01T10:00:00Z".to_string())
}),
DbSelectionPrintResponse::Directory(DbDirectoryInfo {
directory: PathBuf::from("albums/a"),
last_modified: Some("2024-10-01T10:00:00Z".to_string())
}),
DbSelectionPrintResponse::Playlist(DbPlaylistInfo {
playlist: PathBuf::from("albums/a/album a.m3u8"),
last_modified: Some("2022-12-31T09:00:00Z".to_string())
}),
DbSelectionPrintResponse::Directory(DbDirectoryInfo {
directory: PathBuf::from("albums/b"),
last_modified: Some("2023-10-01T10:00:00Z".to_string())
}),
DbSelectionPrintResponse::Playlist(DbPlaylistInfo {
playlist: PathBuf::from("albums/b/album b.m3u8"),
last_modified: Some("2021-12-31T09:00:00Z".to_string())
})
])),
);
}
}
+28 -27
View File
@@ -1,44 +1,45 @@
use std::collections::HashMap;
use crate::commands::{
Command, GenericResponseValue, Request, RequestParserError, RequestParserResult,
ResponseAttributes, ResponseParserError, expect_property_type,
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandResponse, ResponseParserError, single_item_command_request},
response_tokenizer::{GenericResponseValue, ResponseAttributes},
types::Uri,
};
pub struct ReadComments;
pub type ReadCommentsResponse = HashMap<String, String>;
single_item_command_request!(ReadComments, "readcomments", Uri);
impl Command for ReadComments {
type Response = ReadCommentsResponse;
const COMMAND: &'static str = "readcomments";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReadCommentsResponse(HashMap<String, String>);
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::ReadComments(uri), ""))
impl ReadCommentsResponse {
pub fn new(comments: HashMap<String, String>) -> Self {
ReadCommentsResponse(comments)
}
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
impl CommandResponse for ReadCommentsResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
let comments = parts
.into_iter()
.map(|(k, v)| {
Ok((
k.to_string(),
expect_property_type!(Some(v), k, Text).to_string(),
))
.iter()
.map(|(k, v)| match v {
GenericResponseValue::Text(s) => Ok((k.to_string(), s.to_string())),
GenericResponseValue::Binary(_) => {
Err(ResponseParserError::SyntaxError(1, k.to_string()))
}
})
.collect::<Result<HashMap<_, _>, ResponseParserError>>()?;
Ok(comments)
Ok(ReadCommentsResponse(comments))
}
}
impl Command for ReadComments {
type Request = ReadCommentsRequest;
type Response = ReadCommentsResponse;
}
+79 -35
View File
@@ -1,51 +1,90 @@
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, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
request_tokenizer::RequestTokenizer,
response_tokenizer::{
ResponseAttributes, get_and_parse_property, get_optional_property, get_property,
},
common::Offset,
types::{Offset, Uri},
};
pub struct ReadPicture;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReadPictureRequest {
pub uri: Uri,
pub offset: Offset,
}
impl ReadPictureRequest {
pub fn new(uri: Uri, offset: Offset) -> Self {
Self { uri, offset }
}
}
impl CommandRequest for ReadPictureRequest {
const COMMAND: &'static str = "readpicture";
const MIN_ARGS: u32 = 2;
const MAX_ARGS: Option<u32> = Some(2);
fn serialize(&self) -> String {
format!("{} {} {}\n", 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(Self::missing_arguments_error(0)),
};
let offset = match parts.next() {
Some(s) => s
.parse::<Offset>()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 1,
expected_type: "Offset",
raw_input: s.to_owned(),
})?,
None => return Err(Self::missing_arguments_error(1)),
};
Self::throw_if_too_many_arguments(parts)?;
Ok(ReadPictureRequest {
uri: uri.to_string(),
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 {
type Response = Option<ReadPictureResponse>;
const COMMAND: &'static str = "readpicture";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = match parts.next() {
Some(s) => s,
None => return Err(RequestParserError::UnexpectedEOF),
};
let offset = match parts.next() {
Some(s) => s
.parse::<Offset>()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
debug_assert!(parts.next().is_none());
Ok((Request::ReadPicture(uri.to_string(), offset), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
if parts.is_empty() {
return Ok(None);
impl ReadPictureResponse {
pub fn new(size: usize, binary: Vec<u8>, mimetype: Option<String>) -> Self {
Self {
size,
binary,
mimetype,
}
}
}
impl CommandResponse for ReadPictureResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
// TODO: is empty response possible?
// if parts.is_empty() {
// return Err(ResponseParserError::UnexpectedEOF);
// }
let size = get_and_parse_property!(parts, "size", Text);
@@ -53,10 +92,15 @@ impl Command for ReadPicture {
let mimetype = get_optional_property!(parts, "mimetype", Text).map(|s| s.to_string());
Ok(Some(ReadPictureResponse {
Ok(ReadPictureResponse {
size,
binary,
mimetype,
}))
})
}
}
impl Command for ReadPicture {
type Request = ReadPictureRequest;
type Response = ReadPictureResponse;
}
+23 -17
View File
@@ -1,35 +1,41 @@
use std::collections::HashMap;
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
get_and_parse_property,
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::{ResponseAttributes, get_and_parse_property},
types::Uri,
};
pub struct Rescan;
single_optional_item_command_request!(Rescan, "rescan", Uri);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RescanResponse {
pub updating_db: usize,
}
impl Command for Rescan {
type Response = RescanResponse;
const COMMAND: &'static str = "rescan";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = parts.next().map(|s| s.to_string());
debug_assert!(parts.next().is_none());
Ok((Request::Rescan(uri), ""))
impl RescanResponse {
pub fn new(updating_db: usize) -> Self {
RescanResponse { updating_db }
}
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
impl CommandResponse for RescanResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
let updating_db = get_and_parse_property!(parts, "updating_db", Text);
Ok(RescanResponse { updating_db })
}
}
impl Command for Rescan {
type Request = RescanRequest;
type Response = RescanResponse;
}
+109 -24
View File
@@ -1,51 +1,136 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
types::{DbSelectionPrintResponse, DbSongInfo, Sort, WindowRange},
};
pub struct Search;
pub struct SearchResponse {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
}
impl Command for Search {
type Response = SearchResponse;
impl SearchRequest {
pub fn new(filter: Filter, sort: Option<Sort>, window: Option<WindowRange>) -> Self {
Self {
filter,
sort,
window,
}
}
}
impl CommandRequest for SearchRequest {
const COMMAND: &'static str = "search";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(3);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
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.push('\n');
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(Self::missing_arguments_error(0)),
};
let mut argument_index_counter = 0;
let mut sort_or_window = parts.next();
let mut sort = None;
if let Some("sort") = sort_or_window {
argument_index_counter += 1;
let s = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "sort",
argument_index: argument_index_counter,
})?;
sort = Some(
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
s.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "Sort",
raw_input: s.to_string(),
})?,
);
sort_or_window = parts.next();
}
let mut window = None;
if let Some("window") = sort_or_window {
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
window = Some(
w.parse()
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?,
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
);
}
debug_assert!(parts.next().is_none());
Self::throw_if_too_many_arguments(parts)?;
Ok((Request::Search(filter, sort, window), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
Ok(SearchRequest {
filter,
sort,
window,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchResponse(Vec<DbSongInfo>);
impl SearchResponse {
pub fn new(items: Vec<DbSongInfo>) -> Self {
SearchResponse(items)
}
}
impl CommandResponse for SearchResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
DbSelectionPrintResponse::parse(parts)?
.into_iter()
.map(|i| match i {
DbSelectionPrintResponse::Song(db_song_info) => Ok(db_song_info),
DbSelectionPrintResponse::Directory(_db_directory_info) => Err(
ResponseParserError::UnexpectedProperty("directory".to_string()),
),
DbSelectionPrintResponse::Playlist(_db_playlist_info) => Err(
ResponseParserError::UnexpectedProperty("playlist".to_string()),
),
})
.collect::<Result<Vec<DbSongInfo>, ResponseParserError>>()
.map(SearchResponse)
}
}
impl Command for Search {
type Request = SearchRequest;
type Response = SearchResponse;
}
+107 -26
View File
@@ -1,60 +1,141 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
filter::Filter,
request_tokenizer::RequestTokenizer,
types::{SongPosition, Sort, WindowRange},
};
pub struct SearchAdd;
impl Command for SearchAdd {
type Response = ();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchAddRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
}
impl SearchAddRequest {
pub fn new(
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
) -> Self {
Self {
filter,
sort,
window,
position,
}
}
}
impl CommandRequest for SearchAddRequest {
const COMMAND: &'static str = "searchadd";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(4);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
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.push('\n');
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(Self::missing_arguments_error(0)),
};
let mut argument_index_counter = 0;
let mut sort_or_window_or_position = parts.next();
let mut sort = None;
if let Some("sort") = sort_or_window_or_position {
argument_index_counter += 1;
let s = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "sort",
argument_index: argument_index_counter,
})?;
sort = Some(
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
s.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "Sort",
raw_input: s.to_string(),
})?,
);
sort_or_window_or_position = parts.next();
}
let mut window = None;
if let Some("window") = sort_or_window_or_position {
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
window = Some(
w.parse()
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?,
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
);
sort_or_window_or_position = parts.next();
}
let mut position = None;
if let Some("position") = sort_or_window_or_position {
let p = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
argument_index_counter += 1;
let p = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "position",
argument_index: argument_index_counter,
})?;
position = Some(
p.parse()
.map_err(|_| RequestParserError::SyntaxError(0, p.to_string()))?,
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "SongPosition",
raw_input: p.to_string(),
})?,
);
}
debug_assert!(parts.next().is_none());
Self::throw_if_too_many_arguments(parts)?;
Ok((Request::SearchAdd(filter, sort, window, position), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(SearchAddRequest {
filter,
sort,
window,
position,
})
}
}
empty_command_response!(SearchAdd);
impl Command for SearchAdd {
type Request = SearchAddRequest;
type Response = SearchAddResponse;
}
+113 -31
View File
@@ -1,68 +1,150 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
filter::Filter,
request_tokenizer::RequestTokenizer,
types::{PlaylistName, SongPosition, Sort, WindowRange},
};
pub struct SearchAddPl;
impl Command for SearchAddPl {
type Response = ();
const COMMAND: &'static str = "searchaddpl";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchAddPlRequest {
playlist_name: PlaylistName,
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
impl SearchAddPlRequest {
pub fn new(
playlist_name: PlaylistName,
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
) -> Self {
Self {
playlist_name,
filter,
sort,
window,
position,
}
}
}
impl CommandRequest for SearchAddPlRequest {
const COMMAND: &'static str = "searchaddpl";
const MIN_ARGS: u32 = 2;
const MAX_ARGS: Option<u32> = Some(5);
fn serialize(&self) -> String {
let mut cmd = format!("{} {} {}", Self::COMMAND, self.playlist_name, 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.push('\n');
cmd
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let playlist_name = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.ok_or(Self::missing_arguments_error(0))?
.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(Self::missing_arguments_error(1)),
};
let mut argument_index_counter = 1;
let mut sort_or_window_or_position = parts.next();
let mut sort = None;
if let Some("sort") = sort_or_window_or_position {
argument_index_counter += 1;
let s = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "sort",
argument_index: argument_index_counter,
})?;
sort = Some(
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
s.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "Sort",
raw_input: s.to_string(),
})?,
);
sort_or_window_or_position = parts.next();
}
let mut window = None;
if let Some("window") = sort_or_window_or_position {
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
window = Some(
w.parse()
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?,
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
);
sort_or_window_or_position = parts.next();
}
let mut position = None;
if let Some("position") = sort_or_window_or_position {
let p = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
argument_index_counter += 1;
let p = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "position",
argument_index: argument_index_counter,
})?;
position = Some(
p.parse()
.map_err(|_| RequestParserError::SyntaxError(0, p.to_string()))?,
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "SongPosition",
raw_input: p.to_string(),
})?,
);
}
debug_assert!(parts.next().is_none());
Self::throw_if_too_many_arguments(parts)?;
Ok((
Request::SearchAddPl(playlist_name, filter, sort, window, position),
"",
))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
Ok(SearchAddPlRequest {
playlist_name,
filter,
sort,
window,
position,
})
}
}
empty_command_response!(SearchAddPl);
impl Command for SearchAddPl {
type Request = SearchAddPlRequest;
type Response = SearchAddPlResponse;
}
+71 -21
View File
@@ -1,47 +1,92 @@
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},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_and_parse_property},
types::{GroupType, Seconds},
};
pub struct SearchCount;
pub struct SearchCountResponse {
pub songs: usize,
pub playtime: u64,
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchCountRequest {
filter: Filter,
group: Option<GroupType>,
}
impl Command for SearchCount {
type Response = SearchCountResponse;
const COMMAND: &'static str = "searchcount";
impl SearchCountRequest {
pub fn new(filter: Filter, group: Option<GroupType>) -> Self {
Self { filter, group }
}
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
impl CommandRequest for SearchCountRequest {
const COMMAND: &'static str = "searchcount";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(2);
fn serialize(&self) -> String {
let mut cmd = format!("{} {}", Self::COMMAND, self.filter);
if let Some(group) = &self.group {
cmd.push_str(&format!(" group {}", group));
}
cmd.push('\n');
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(Self::missing_arguments_error(0)),
};
let group = if let Some("group") = parts.next() {
let group = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let group = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "group",
argument_index: 1,
})?;
Some(
group
.parse()
.map_err(|_| RequestParserError::SyntaxError(1, group.to_owned()))?,
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 1,
expected_type: "Group",
raw_input: group.to_string(),
})?,
)
} else {
None
};
debug_assert!(parts.next().is_none());
Self::throw_if_too_many_arguments(parts)?;
Ok((Request::SearchCount(filter, group), ""))
Ok(SearchCountRequest { filter, group })
}
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchCountResponse {
pub songs: usize,
pub playtime: Seconds,
}
impl SearchCountResponse {
pub fn new(songs: usize, playtime: Seconds) -> Self {
Self { songs, playtime }
}
}
impl CommandResponse for SearchCountResponse {
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);
@@ -49,3 +94,8 @@ impl Command for SearchCount {
Ok(SearchCountResponse { songs, playtime })
}
}
impl Command for SearchCount {
type Request = SearchCountRequest;
type Response = SearchCountResponse;
}
+23 -17
View File
@@ -1,35 +1,41 @@
use std::collections::HashMap;
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
get_and_parse_property,
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::{ResponseAttributes, get_and_parse_property},
types::Uri,
};
pub struct Update;
single_optional_item_command_request!(Update, "update", Uri);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UpdateResponse {
updating_db: usize,
}
impl Command for Update {
type Response = UpdateResponse;
const COMMAND: &'static str = "update";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = parts.next().map(|s| s.to_string());
debug_assert!(parts.next().is_none());
Ok((Request::Update(uri), ""))
impl UpdateResponse {
pub fn new(updating_db: usize) -> Self {
UpdateResponse { updating_db }
}
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
impl CommandResponse for UpdateResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
let updating_db = get_and_parse_property!(parts, "updating_db", Text);
Ok(UpdateResponse { updating_db })
}
}
impl Command for Update {
type Request = UpdateRequest;
type Response = UpdateResponse;
}
+10 -10
View File
@@ -1,11 +1,11 @@
pub mod delpartition;
pub mod listpartitions;
pub mod moveoutput;
pub mod newpartition;
pub mod partition;
mod delpartition;
mod listpartitions;
mod moveoutput;
mod newpartition;
mod partition;
pub use delpartition::DelPartition;
pub use listpartitions::ListPartitions;
pub use moveoutput::MoveOutput;
pub use newpartition::NewPartition;
pub use partition::Partition;
pub use delpartition::*;
pub use listpartitions::*;
pub use moveoutput::*;
pub use newpartition::*;
pub use partition::*;
@@ -1,29 +1,15 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::PartitionName,
};
pub struct DelPartition;
single_item_command_request!(DelPartition, "delpartition", PartitionName);
empty_command_response!(DelPartition);
impl Command for DelPartition {
type Response = ();
const COMMAND: &'static str = "delpartition";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let partition = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
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(())
}
type Request = DelPartitionRequest;
type Response = DelPartitionResponse;
}
@@ -1,33 +1,16 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
expect_property_type,
use crate::{
commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
response_tokenizer::expect_property_type,
types::PartitionName,
};
pub struct ListPartitions;
pub type ListPartitionsResponse = Vec<String>;
empty_command_request!(ListPartitions, "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)
}
}
+7 -24
View File
@@ -1,29 +1,12 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
use crate::commands::{Command, empty_command_response, single_item_command_request};
pub struct MoveOutput;
single_item_command_request!(MoveOutput, "moveoutput", String);
empty_command_response!(MoveOutput);
impl Command for MoveOutput {
type Response = ();
const COMMAND: &'static str = "moveoutput";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let output_name = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
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(())
}
type Request = MoveOutputRequest;
type Response = MoveOutputResponse;
}
@@ -1,29 +1,15 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::PartitionName,
};
pub struct NewPartition;
single_item_command_request!(NewPartition, "newpartition", PartitionName);
empty_command_response!(NewPartition);
impl Command for NewPartition {
type Response = ();
const COMMAND: &'static str = "newpartition";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let partition = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
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(())
}
type Request = NewPartitionRequest;
type Response = NewPartitionResponse;
}
+9 -23
View File
@@ -1,29 +1,15 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::PartitionName,
};
pub struct Partition;
single_item_command_request!(Partition, "partition", PartitionName);
empty_command_response!(Partition);
impl Command for Partition {
type Response = ();
const COMMAND: &'static str = "partition";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let partition = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
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(())
}
type Request = PartitionRequest;
type Response = PartitionResponse;
}
+24 -24
View File
@@ -1,25 +1,25 @@
pub mod consume;
pub mod crossfade;
pub mod getvol;
pub mod mixrampdb;
pub mod mixrampdelay;
pub mod random;
pub mod repeat;
pub mod replay_gain_mode;
pub mod replay_gain_status;
pub mod setvol;
pub mod single;
pub mod volume;
mod consume;
mod crossfade;
mod getvol;
mod mixrampdb;
mod mixrampdelay;
mod random;
mod repeat;
mod replay_gain_mode;
mod replay_gain_status;
mod setvol;
mod single;
mod volume;
pub use consume::Consume;
pub use crossfade::Crossfade;
pub use getvol::GetVol;
pub use mixrampdb::MixRampDb;
pub use mixrampdelay::MixRampDelay;
pub use random::Random;
pub use repeat::Repeat;
pub use replay_gain_mode::ReplayGainMode;
pub use replay_gain_status::ReplayGainStatus;
pub use setvol::SetVol;
pub use single::Single;
pub use volume::Volume;
pub use consume::*;
pub use crossfade::*;
pub use getvol::*;
pub use mixrampdb::*;
pub use mixrampdelay::*;
pub use random::*;
pub use repeat::*;
pub use replay_gain_mode::*;
pub use replay_gain_status::*;
pub use setvol::*;
pub use single::*;
pub use volume::*;
+9 -26
View File
@@ -1,32 +1,15 @@
use std::str::FromStr;
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::BoolOrOneshot,
};
pub struct Consume;
single_item_command_request!(Consume, "consume", BoolOrOneshot);
empty_command_response!(Consume);
impl Command for Consume {
type Response = ();
const COMMAND: &'static str = "consume";
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),
};
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(())
}
type Request = ConsumeRequest;
type Response = ConsumeResponse;
}
+8 -27
View File
@@ -1,34 +1,15 @@
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::Seconds,
commands::{Command, empty_command_response, single_item_command_request},
types::Seconds,
};
pub struct Crossfade;
single_item_command_request!(Crossfade, "crossfade", Seconds);
empty_command_response!(Crossfade);
impl Command for Crossfade {
type Response = ();
const COMMAND: &'static str = "crossfade";
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::Crossfade(seconds), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = CrossfadeRequest;
type Response = CrossfadeResponse;
}
+8 -23
View File
@@ -1,30 +1,15 @@
use std::collections::HashMap;
use crate::{
commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
get_and_parse_property,
},
common::VolumeValue,
commands::{Command, empty_command_request, single_item_command_response},
types::VolumeValue,
};
pub struct GetVol;
empty_command_request!(GetVol, "getvol");
single_item_command_response!(GetVol, "volume", VolumeValue);
impl Command for GetVol {
type Response = VolumeValue;
const COMMAND: &'static str = "getvol";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::GetVol, ""))
}
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)
}
type Request = GetVolRequest;
type Response = GetVolResponse;
}
+7 -26
View File
@@ -1,31 +1,12 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
use crate::commands::{Command, empty_command_response, single_item_command_request};
pub struct MixRampDb;
single_item_command_request!(MixRampDb, "mixrampdb", f32);
empty_command_response!(MixRampDb);
impl Command for MixRampDb {
type Response = ();
const COMMAND: &'static str = "mixrampdb";
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),
};
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(())
}
type Request = MixRampDbRequest;
type Response = MixRampDbResponse;
}
+8 -27
View File
@@ -1,34 +1,15 @@
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::Seconds,
commands::{Command, empty_command_response, single_item_command_request},
types::Seconds,
};
pub struct MixRampDelay;
single_item_command_request!(MixRampDelay, "mixrampdelay", Seconds);
empty_command_response!(MixRampDelay);
impl Command for MixRampDelay {
type Response = ();
const COMMAND: &'static str = "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(())
}
type Request = MixRampDelayRequest;
type Response = MixRampDelayResponse;
}
+38 -18
View File
@@ -1,31 +1,51 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
};
pub struct Random;
impl Command for Random {
type Response = ();
const COMMAND: &'static str = "random";
pub struct RandomRequest(bool);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
impl RandomRequest {
pub fn new(state: bool) -> Self {
Self(state)
}
}
impl CommandRequest for RandomRequest {
const COMMAND: &'static str = "random";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(1);
fn serialize(&self) -> String {
let state = if self.0 { "1" } else { "0" };
format!("{} {}\n", Self::COMMAND, state)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let state = match parts.next() {
Some("0") => false,
Some("1") => true,
Some(s) => return Err(RequestParserError::SyntaxError(0, s.to_owned())),
None => return Err(RequestParserError::UnexpectedEOF),
Some(s) => {
return Err(RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "bool",
raw_input: s.to_owned(),
});
}
None => return Err(Self::missing_arguments_error(0)),
};
debug_assert!(parts.next().is_none());
Self::throw_if_too_many_arguments(parts)?;
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;
}
+38 -18
View File
@@ -1,31 +1,51 @@
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
};
pub struct Repeat;
impl Command for Repeat {
type Response = ();
const COMMAND: &'static str = "repeat";
pub struct RepeatRequest(bool);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
impl RepeatRequest {
pub fn new(state: bool) -> Self {
RepeatRequest(state)
}
}
impl CommandRequest for RepeatRequest {
const COMMAND: &'static str = "repeat";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(1);
fn serialize(&self) -> String {
let state = if self.0 { "1" } else { "0" };
format!("{} {}\n", Self::COMMAND, state)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let state = match parts.next() {
Some("0") => false,
Some("1") => true,
Some(s) => return Err(RequestParserError::SyntaxError(0, s.to_owned())),
None => return Err(RequestParserError::UnexpectedEOF),
Some(s) => {
return Err(RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "bool",
raw_input: s.to_owned(),
});
}
None => return Err(Self::missing_arguments_error(0)),
};
debug_assert!(parts.next().is_none());
Self::throw_if_too_many_arguments(parts)?;
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;
}
@@ -1,35 +1,15 @@
use std::str::FromStr;
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::ReplayGainModeMode,
commands::{Command, empty_command_response, single_item_command_request},
types::ReplayGainModeMode,
};
pub struct ReplayGainMode;
single_item_command_request!(ReplayGainMode, "replay_gain_mode", ReplayGainModeMode);
empty_command_response!(ReplayGainMode);
impl Command for ReplayGainMode {
type Response = ();
const COMMAND: &'static str = "replay_gain_mode";
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),
};
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(())
}
type Request = ReplayGainModeRequest;
type Response = ReplayGainModeResponse;
}
@@ -3,39 +3,43 @@ use std::{collections::HashMap, str::FromStr};
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
get_property,
},
common::ReplayGainModeMode,
commands::{Command, CommandResponse, ResponseParserError, empty_command_request},
response_tokenizer::{ResponseAttributes, get_property},
types::ReplayGainModeMode,
};
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 ReplayGainStatusResponse {
pub fn new(replay_gain_mode: ReplayGainModeMode) -> Self {
Self { replay_gain_mode }
}
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
impl CommandResponse for ReplayGainStatusResponse {
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 {
replay_gain_mode: ReplayGainModeMode::from_str(replay_gain_mode).map_err(|_| {
ResponseParserError::InvalidProperty("replay_gain_mode", replay_gain_mode)
ResponseParserError::InvalidProperty(
"replay_gain_mode".to_string(),
replay_gain_mode.to_string(),
)
})?,
})
}
}
impl Command for ReplayGainStatus {
type Request = ReplayGainStatusRequest;
type Response = ReplayGainStatusResponse;
}
+8 -28
View File
@@ -1,35 +1,15 @@
use std::str::FromStr;
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::VolumeValue,
commands::{Command, empty_command_response, single_item_command_request},
types::VolumeValue,
};
pub struct SetVol;
single_item_command_request!(SetVol, "setvol", VolumeValue);
empty_command_response!(SetVol);
impl Command for SetVol {
type Response = ();
const COMMAND: &'static str = "setvol";
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),
};
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(())
}
type Request = SetVolRequest;
type Response = SetVolResponse;
}
+9 -26
View File
@@ -1,32 +1,15 @@
use std::str::FromStr;
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::BoolOrOneshot,
};
pub struct Single;
single_item_command_request!(Single, "single", BoolOrOneshot);
empty_command_response!(Single);
impl Command for Single {
type Response = ();
const COMMAND: &'static str = "single";
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),
};
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(())
}
type Request = SingleRequest;
type Response = SingleResponse;
}
+8 -28
View File
@@ -1,35 +1,15 @@
use std::str::FromStr;
use crate::{
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::VolumeValue,
commands::{Command, empty_command_response, single_item_command_request},
types::VolumeValue,
};
pub struct Volume;
single_item_command_request!(Volume, "volume", VolumeValue);
empty_command_response!(Volume);
impl Command for Volume {
type Response = ();
const COMMAND: &'static str = "volume";
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),
};
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(())
}
type Request = VolumeRequest;
type Response = VolumeResponse;
}
+10 -10
View File
@@ -1,11 +1,11 @@
pub mod clearerror;
pub mod currentsong;
pub mod idle;
pub mod stats;
pub mod status;
mod clearerror;
mod currentsong;
mod idle;
mod stats;
mod status;
pub use clearerror::ClearError;
pub use currentsong::CurrentSong;
pub use idle::Idle;
pub use stats::Stats;
pub use status::Status;
pub use clearerror::*;
pub use currentsong::*;
pub use idle::*;
pub use stats::*;
pub use status::*;
+7 -18
View File
@@ -1,24 +1,13 @@
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
use crate::commands::{Command, empty_command_request, empty_command_response};
/// Clears the current error message in status (this is also accomplished by any command that starts playback)
pub struct ClearError;
empty_command_request!(ClearError, "clearerror");
empty_command_response!(ClearError);
impl Command for ClearError {
type Response = ();
const COMMAND: &'static str = "clearerror";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ClearError, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
type Request = ClearErrorRequest;
type Response = ClearErrorResponse;
}
+57 -17
View File
@@ -1,28 +1,68 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
use crate::{
commands::{Command, CommandResponse, ResponseParserError, empty_command_request},
response_tokenizer::{
ResponseAttributes, get_and_parse_optional_property, get_and_parse_property,
},
types::{DbSongInfo, Priority, SongId, SongPosition},
};
/// Displays the song info of the current song (same song that is identified in status)
pub struct CurrentSong;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CurrentSongResponse {}
empty_command_request!(CurrentSong, "currentsong");
impl Command for CurrentSong {
type Response = CurrentSongResponse;
const COMMAND: &'static str = "currentsong";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CurrentSongResponse {
position: SongPosition,
id: SongId,
priority: Option<Priority>,
song_info: DbSongInfo,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::CurrentSong, ""))
}
fn parse_response(
_parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
impl CurrentSongResponse {
pub fn new(
position: SongPosition,
id: SongId,
priority: Option<Priority>,
song_info: DbSongInfo,
) -> Self {
Self {
position,
id,
priority,
song_info,
}
}
}
impl CommandResponse for CurrentSongResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let mut parts: HashMap<_, _> = parts.into_map()?;
let position: SongPosition = get_and_parse_property!(parts, "Pos", Text);
let id: SongId = get_and_parse_property!(parts, "Id", Text);
let priority: Option<Priority> = get_and_parse_optional_property!(parts, "Prio", Text);
parts.remove("Pos");
parts.remove("Id");
parts.remove("Prio");
let song_info = DbSongInfo::parse_map(parts)?;
Ok(CurrentSongResponse {
position,
id,
priority,
song_info,
})
}
}
impl Command for CurrentSong {
type Request = CurrentSongRequest;
type Response = CurrentSongResponse;
}
+49 -29
View File
@@ -1,37 +1,57 @@
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, empty_command_response},
request_tokenizer::RequestTokenizer,
types::SubSystem,
};
pub struct Idle;
impl Command for Idle {
type Response = ();
const COMMAND: &'static str = "idle";
pub struct IdleRequest(Option<Vec<SubSystem>>);
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)), ""))
});
debug_assert!(parts.next().is_none());
result
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
impl IdleRequest {
pub fn new(subsystems: Option<Vec<SubSystem>>) -> Self {
IdleRequest(subsystems)
}
}
impl CommandRequest for IdleRequest {
const COMMAND: &'static str = "idle";
const MIN_ARGS: u32 = 0;
const MAX_ARGS: Option<u32> = None;
fn serialize(&self) -> String {
match &self.0 {
Some(subsystems) => {
let subsystems_str = subsystems
.iter()
.map(|subsystem| subsystem.to_string())
.collect::<Vec<_>>()
.join(",");
format!("{} {}\n", Self::COMMAND, subsystems_str)
}
None => Self::COMMAND.to_string() + "\n",
}
}
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))
});
result.map(IdleRequest)
}
}
empty_command_response!(Idle);
impl Command for Idle {
type Request = IdleRequest;
type Response = IdleResponse;
}

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