24 Commits

Author SHA1 Message Date
oysteikt abacb54c8d commands: add missing debug asserts
Build and test / build (push) Failing after 16s
Build and test / test (push) Failing after 15s
Build and test / check (push) Failing after 59s
Build and test / docs (push) Successful in 1m17s
2024-12-13 19:31:50 +01:00
oysteikt 86183e56ad commands: add some TODOs for assumptions made about syntax 2024-12-13 19:31:48 +01:00
oysteikt 6a9cd00275 response: remove leftover response types, add error codes
Build and test / check (push) Failing after 1m2s
Build and test / build (push) Successful in 1m3s
Build and test / test (push) Successful in 1m37s
Build and test / docs (push) Successful in 1m16s
2024-12-13 18:30:16 +01:00
oysteikt 9cb92741a4 Implement some more commands
Build and test / check (push) Failing after 13s
Build and test / test (push) Failing after 14s
Build and test / build (push) Successful in 57s
Build and test / docs (push) Successful in 1m12s
2024-12-13 18:20:03 +01:00
oysteikt cea3d3da04 commands: add a few TODOs
Build and test / build (push) Successful in 57s
Build and test / check (push) Failing after 57s
Build and test / docs (push) Successful in 1m8s
Build and test / test (push) Successful in 1m27s
2024-12-13 17:16:16 +01:00
oysteikt 87fa93fb1c format 2024-12-13 17:16:16 +01:00
oysteikt cf0db472e8 commands: verify key uniqueness for ResponseAttributes -> HashMap 2024-12-13 17:16:16 +01:00
oysteikt 4f6392e376 commands: use real Filter type 2024-12-13 17:16:16 +01:00
oysteikt 2fb97a9964 Move repo to Projects 2024-12-13 17:16:15 +01:00
oysteikt 48beaa7732 examples/mpd-client: format 2024-12-13 16:59:40 +01:00
oysteikt 2bd53462d7 .gitea/build-and-test: init
Build and test / check (push) Failing after 5m36s
Build and test / build (push) Successful in 10m23s
Build and test / test (push) Failing after 6m16s
Build and test / docs (push) Successful in 10m25s
2024-12-10 21:48:20 +01:00
oysteikt edeb0e3b78 .envrc: init 2024-12-05 18:10:11 +01:00
oysteikt 2ee6bbc582 Add more commands 2024-12-02 21:00:22 +01:00
oysteikt 3e512092bd Add existing command parsers to the main request parser 2024-12-01 19:16:44 +01:00
oysteikt 08104b3537 Implement some more commands 2024-12-01 18:31:34 +01:00
oysteikt 1561ae2e80 prefer unimplemented! for unimplemented functions 2024-11-30 03:36:00 +01:00
oysteikt d45502a43e commands: add derives for a few response types 2024-11-30 03:31:52 +01:00
oysteikt 4fada75fe9 commands: handle some more response parsing 2024-11-30 03:28:22 +01:00
oysteikt 184f9fc215 commands/stats: parse_response 2024-11-30 02:28:29 +01:00
oysteikt 74074bf9c7 commands: make macros usable without use statements 2024-11-30 02:28:10 +01:00
oysteikt 84f061a79f commands: use different datastructure for response attrs 2024-11-30 02:18:06 +01:00
oysteikt 472be6c083 flake.lock: bump 2024-11-30 01:57:57 +01:00
oysteikt 49e070a41d Continued development 2024-11-30 01:57:45 +01:00
oysteikt 8293c6e6e5 Initial commit 2024-11-05 22:47:35 +01:00
187 changed files with 4361 additions and 10723 deletions
-2
View File
@@ -1,2 +0,0 @@
[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: debian-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build
run: cargo build --verbose --release
run: cargo build --all-features --verbose --release
check:
runs-on: debian-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -31,13 +31,12 @@ jobs:
run: cargo fmt -- --check
- name: Check clippy
# run: cargo clippy -- --deny warnings
run: cargo clippy
run: cargo clippy --all-features -- --deny warnings
test:
runs-on: debian-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: cargo-bins/cargo-binstall@main
- name: Install rust toolchain
@@ -50,7 +49,7 @@ jobs:
- name: Run tests
run: |
cargo nextest run --release --no-fail-fast
cargo nextest run --all-features --release --no-fail-fast
env:
RUST_LOG: "trace"
RUSTFLAGS: "-Cinstrument-coverage"
@@ -84,13 +83,13 @@ jobs:
target: ${{ gitea.ref_name }}/coverage/
username: gitea-web
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }}
host: pages.pvv.ntnu.no
known-hosts: "pages.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH2QjfFB+city1SYqltkVqWACfo1j37k+oQQfj13mtgg"
host: bekkalokk.pvv.ntnu.no
known-hosts: "bekkalokk.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEI6VSaDrMG8+flg4/AeHlAFIen8RUzWh6URQKqFegSx"
docs:
runs-on: debian-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v3
- name: Install latest nightly toolchain
uses: actions-rs/toolchain@v1
@@ -99,14 +98,15 @@ jobs:
override: true
- name: Build docs
run: cargo doc --document-private-items --release
run: cargo doc --all-features --document-private-items --release
- name: Transfer files
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v2
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v1
with:
source: target/doc/
target: ${{ gitea.ref_name }}/docs/
username: gitea-web
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }}
host: pages.pvv.ntnu.no
known-hosts: "pages.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH2QjfFB+city1SYqltkVqWACfo1j37k+oQQfj13mtgg"
host: bekkalokk.pvv.ntnu.no
known-hosts: "bekkalokk.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEI6VSaDrMG8+flg4/AeHlAFIen8RUzWh6URQKqFegSx"
+1 -1
View File
@@ -1,3 +1,3 @@
/target
result
result-*
result-*
Generated
+15 -820
View File
@@ -1,142 +1,6 @@
# This file is automatically @generated by Cargo.
# 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",
]
version = 3
[[package]]
name = "diff"
@@ -144,353 +8,20 @@ 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.7"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
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"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "pretty_assertions"
@@ -504,394 +35,58 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.106"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
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"
version = "1.0.210"
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"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sha3"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
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"
version = "2.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "term"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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"
version = "1.0.13"
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",
]
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "yansi"
+6 -23
View File
@@ -4,32 +4,15 @@ 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/"
edition = "2024"
rust-version = "1.85.0"
repository = "https://git.pvv.ntnu.no/Projects/empidee"
documentation = "https://pages.pvv.ntnu.no/Projects/empidee/main/docs/empidee/"
edition = "2021"
rust-version = "1.83.0"
[dependencies]
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"]
serde = { version = "1.0.210", features = ["derive"] }
[dev-dependencies]
anyhow = "1.0.102"
indoc = "2.0.7"
indoc = "2.0.5"
pretty_assertions = "1.4.1"
tokio = { version = "1.50.0", features = ["macros", "net", "rt"] }
[build-dependencies]
lalrpop = "0.22.2"
-21
View File
@@ -1,21 +0,0 @@
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.
+2 -2
View File
@@ -1,5 +1,5 @@
[![Coverage](https://pages.pvv.ntnu.no/Grzegorz/empidee/main/coverage/badges/for_the_badge.svg)](https://pages.pvv.ntnu.no/Grzegorz/empidee/main/coverage/src/)
[![Docs](https://img.shields.io/badge/docs-blue?style=for-the-badge&logo=rust)](https://pages.pvv.ntnu.no/Grzegorz/empidee/main/docs/empidee/)
[![Coverage](https://pages.pvv.ntnu.no/Projects/empidee/main/coverage/badges/for_the_badge.svg)](https://pages.pvv.ntnu.no/Projects/empidee/main/coverage/src/)
[![Docs](https://img.shields.io/badge/docs-blue?style=for-the-badge&logo=rust)](https://pages.pvv.ntnu.no/Projects/empidee/main/docs/empidee/)
# empidee
-9
View File
@@ -1,9 +0,0 @@
fn main() {
// let debug_mode = std::env::var("PROFILE").unwrap() == "debug";
lalrpop::Configuration::new()
//.emit_comments(debug_mode)
// .generate_in_source_tree()
.process()
.unwrap();
}
+2 -15
View File
@@ -1,16 +1,3 @@
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(())
fn main() {
todo!()
}
Generated
+6 -6
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1775036866,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"lastModified": 1732521221,
"narHash": "sha256-2ThgXBUXAE1oFsVATK1ZX9IjPcS4nKFOAjhPNKuiMn0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"rev": "4633a7c72337ea8fd23a4f2ba3972865e3ec685d",
"type": "github"
},
"original": {
@@ -29,11 +29,11 @@
]
},
"locked": {
"lastModified": 1775099554,
"narHash": "sha256-3xBsGnGDLOFtnPZ1D3j2LU19wpAlYefRKTlkv648rU0=",
"lastModified": 1732847586,
"narHash": "sha256-SnHHSBNZ+aj8mRzYxb6yXBl9ei3K3j5agC/D8Vjw/no=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "8d6387ed6d8e6e6672fd3ed4b61b59d44b124d99",
"rev": "97210ddff72fe139815f4c1ac5da74b5b0dde2d7",
"type": "github"
},
"original": {
-42
View File
@@ -36,52 +36,10 @@
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;
};
});
};
}
-1
View File
@@ -1 +0,0 @@
style_edition = "2024"
-138
View File
@@ -1,138 +0,0 @@
//! 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
}
}
+257 -658
View File
@@ -1,16 +1,9 @@
//! The basic building blocks, definitions and helpers used to define an Mpd command.
//!
//! An mpd command consists of a pair of serializers and parsers for both the request
//! and the corresponding response, as well as the command name used to identify the command.
//!
//! Each command is modelled as a struct implementing the [`Command`] trait,
//! which in turn uses the [`CommandRequest`] and [`CommandResponse`] traits
//! to define the request and response types respectively.
use std::{
collections::{HashMap, HashSet},
str::SplitWhitespace,
};
use crate::{request_tokenizer::RequestTokenizer, response_tokenizer::ResponseAttributes};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::Request;
mod audio_output_devices;
mod client_to_client;
@@ -23,7 +16,6 @@ mod playback_options;
mod querying_mpd_status;
mod queue;
mod reflection;
mod stickers;
mod stored_playlists;
pub use audio_output_devices::*;
@@ -37,679 +29,286 @@ pub use playback_options::*;
pub use querying_mpd_status::*;
pub use queue::*;
pub use reflection::*;
pub use stickers::*;
pub use stored_playlists::*;
/// A trait modelling a single MPD command request.
pub trait CommandRequest
where
Self: Sized,
{
/// The command name used within the protocol
pub trait Command {
type Response;
// The command name used within the protocol
const COMMAND: &'static str;
// TODO: add these for ease of throwing parsing errors
/// The minimum number of arguments this command takes
const MIN_ARGS: u32;
// TODO: `parse_request` should be using a more custom splitter, that can handle
// quoted strings and escape characters. This is what mpd uses to provide arguments
// with spaces and whatnot.
/// The maximum number of arguments this command takes.
///
/// Note that in the case of keyworded arguments, such as
/// `group <groupname>`, `sort <sorting>`, etc., these are
/// counted as a single argument despite being two tokens.
const MAX_ARGS: Option<u32>;
// A function to parse the remaining parts of the command, split by whitespace
fn parse_request(parts: SplitWhitespace<'_>) -> RequestParserResult<'_>;
/// Helper function to create a [`RequestParserError::TooManyArguments`] error
fn too_many_arguments_error(found: u32) -> RequestParserError {
RequestParserError::TooManyArguments {
expected_min: Self::MIN_ARGS,
expected_max: Self::MAX_ARGS,
found,
}
}
/// Helper function to throw a [`RequestParserError::TooManyArguments`] error
fn throw_if_too_many_arguments(parts: RequestTokenizer<'_>) -> Result<(), RequestParserError> {
let remaining_args = parts.count().try_into().unwrap_or(u32::MAX);
if remaining_args != 0 {
return Err(Self::too_many_arguments_error(
remaining_args.saturating_add(Self::MAX_ARGS.unwrap()),
));
}
Ok(())
}
/// Helper function to create a [`RequestParserError::MissingArguments`] error
fn missing_arguments_error(found: u32) -> RequestParserError {
RequestParserError::MissingArguments {
expected_min: Self::MIN_ARGS,
expected_max: Self::MAX_ARGS,
found,
}
}
/// Serializes the request into a String.
fn serialize(&self) -> String;
/// Parses the request from its tokenized parts.
/// See also [`parse_raw`].
fn parse(parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError>;
/// Parses the request from its raw string representation.
///
/// This assumes the raw string starts with the command name, e.g.
/// `command_name arg1 "arg2 arg3"`
fn parse_raw(raw: &str) -> Result<Self, RequestParserError> {
let (line, _rest) = raw
fn parse_raw_request(raw: &str) -> RequestParserResult<'_> {
let (line, rest) = raw
.split_once('\n')
.ok_or(RequestParserError::MissingNewline)?;
.ok_or(RequestParserError::UnexpectedEOF)?;
let mut parts = line.split_whitespace();
if line.is_empty() {
return Err(RequestParserError::EmptyLine);
}
let command_name = parts
.next()
.ok_or(RequestParserError::SyntaxError(0, line.to_string()))?
.trim();
let mut tokenized = RequestTokenizer::new(line);
debug_assert!(command_name == Self::COMMAND);
let command_name_token_length = Self::COMMAND.split_ascii_whitespace().count();
let mut command_name = Vec::with_capacity(command_name_token_length);
for _ in 0..command_name_token_length {
let token = tokenized
Self::parse_request(parts).map(|(req, _)| (req, rest))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError<'_>>;
fn parse_raw_response(raw: &str) -> Result<Self::Response, ResponseParserError<'_>> {
let mut parts = Vec::new();
let mut lines = raw.lines();
loop {
let line = lines.next().ok_or(ResponseParserError::UnexpectedEOF)?;
if line.is_empty() {
println!("Warning: empty line in response");
continue;
}
if line == "OK" {
break;
}
let mut keyval = line.splitn(2, ": ");
let key = keyval
.next()
.ok_or(RequestParserError::SyntaxError(0, line.to_string()))?;
command_name.push(token);
}
let command_name = command_name.join(" ");
.ok_or(ResponseParserError::SyntaxError(0, line))?;
if command_name != Self::COMMAND {
return Err(RequestParserError::SyntaxError(0, line.to_string()));
// TODO: handle binary data, also verify binarylimit
let value = keyval
.next()
.ok_or(ResponseParserError::SyntaxError(0, line))?;
parts.push((key, GenericResponseValue::Text(value)));
}
Self::parse(tokenized)
Self::parse_response(parts.into())
}
}
/// A trait modelling a single MPD command response.
pub trait CommandResponse
where
Self: Sized,
{
// /// Serializes the response into a Vec<u8>.
// fn serialize(&self) -> Vec<u8>;
pub type RequestParserResult<'a> = Result<(Request, &'a str), RequestParserError>;
/// Parses the response from its tokenized parts.
/// See also [`parse_raw`].
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError>;
/// Parses the response from its raw byte representation.
fn parse_raw(raw: &[u8]) -> Result<Self, ResponseParserError> {
Self::parse(ResponseAttributes::new_from_bytes(raw))
}
}
/// A trait modelling the request/response pair of a single MPD command.
pub trait Command {
/// The request sent from the client to the server
type Request: CommandRequest;
/// The response sent from the server to the client
type Response: CommandResponse;
/// The command name used within the protocol
const COMMAND: &'static str = Self::Request::COMMAND;
/// Parse the request from its tokenized parts. See also [`parse_raw_request`].
fn parse_request(parts: RequestTokenizer) -> Result<Self::Request, RequestParserError> {
Self::Request::parse(parts)
}
/// Parse the raw request string into a request.
/// This assumes the raw string starts with the command name, e.g. `command_name arg1 "arg2 arg3"`
fn parse_raw_request(raw: &str) -> Result<Self::Request, RequestParserError> {
Self::Request::parse_raw(raw)
}
// /// Serialize the response into a string.
// fn serialize_response(&self, response: Self::Response) -> String {
/// Parse the response from its tokenized parts. See also [`parse_raw_response`].
fn parse_response(parts: ResponseAttributes) -> Result<Self::Response, ResponseParserError> {
Self::Response::parse(parts)
}
/// Parse the raw response string into a response.
fn parse_raw_response(raw: &[u8]) -> Result<Self::Response, ResponseParserError> {
Self::Response::parse_raw(raw)
}
}
// Request/response implementation helpers
macro_rules! empty_command_request {
($name:ident, $command_name:expr) => {
paste::paste! {
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct [<$name Request>];
}
impl std::default::Default for paste::paste! { [<$name Request>] } {
fn default() -> Self {
paste::paste! { [<$name Request>] }
}
}
impl crate::commands::CommandRequest for paste::paste! { [<$name Request>] } {
const COMMAND: &'static str = $command_name;
const MIN_ARGS: u32 = 0;
const MAX_ARGS: Option<u32> = Some(0);
fn serialize(&self) -> String {
Self::COMMAND.to_string() + "\n"
}
fn parse(
parts: crate::commands::RequestTokenizer<'_>,
) -> Result<Self, crate::commands::RequestParserError> {
Self::throw_if_too_many_arguments(parts)?;
Ok(paste::paste! { [<$name Request>] })
}
}
};
}
macro_rules! empty_command_response {
($name:ident) => {
paste::paste! {
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct [<$name Response>];
}
impl std::default::Default for paste::paste! { [<$name Response>] } {
fn default() -> Self {
paste::paste! { [<$name Response>] }
}
}
impl crate::commands::CommandResponse for paste::paste! { [<$name Response>] } {
fn parse(
_parts: crate::commands::ResponseAttributes<'_>,
) -> Result<Self, crate::commands::ResponseParserError> {
debug_assert!(_parts.is_empty());
Ok(paste::paste! { [<$name Response>] })
}
}
};
}
macro_rules! single_item_command_request {
($name:ident, $command_name:expr, $item_type:ty) => {
paste::paste! {
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct [<$name Request>] (pub $item_type);
}
impl paste::paste! { [<$name Request>] } {
pub fn new(item: $item_type) -> Self {
paste::paste! {
crate::commands::[<$name Request>](item)
}
}
}
impl crate::commands::CommandRequest for paste::paste! { [<$name Request>] } {
const COMMAND: &'static str = $command_name;
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(1);
fn serialize(&self) -> String {
format!("{} {}\n", Self::COMMAND, self.0)
}
fn parse(
mut parts: crate::commands::RequestTokenizer<'_>,
) -> Result<Self, crate::commands::RequestParserError> {
let item_token = parts.next().ok_or(Self::missing_arguments_error(0))?;
let item = item_token.parse::<$item_type>().map_err(|_| {
crate::commands::RequestParserError::SubtypeParserError {
argument_index: 1,
expected_type: stringify!($item_type),
raw_input: item_token.to_owned(),
}
})?;
Self::throw_if_too_many_arguments(parts)?;
Ok(paste::paste! { [<$name Request>] ( item ) })
}
}
};
}
macro_rules! single_optional_item_command_request {
($name:ident, $command_name:expr, $item_type:ty) => {
paste::paste! {
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct [<$name Request>] (pub Option<$item_type>);
}
impl paste::paste! { [<$name Request>] } {
pub fn new(item: Option<$item_type>) -> Self {
paste::paste! {
crate::commands::[<$name Request>](item)
}
}
}
impl crate::commands::CommandRequest for paste::paste! { [<$name Request>] } {
const COMMAND: &'static str = $command_name;
const MIN_ARGS: u32 = 0;
const MAX_ARGS: Option<u32> = Some(1);
fn serialize(&self) -> String {
match &self.0 {
Some(item) => format!("{} {}\n", Self::COMMAND, item),
None => Self::COMMAND.to_string() + "\n",
}
}
fn parse(
mut parts: crate::commands::RequestTokenizer<'_>,
) -> Result<Self, crate::commands::RequestParserError> {
let item = parts
.next()
.map(|s| {
s.parse().map_err(|_| {
crate::commands::RequestParserError::SubtypeParserError {
argument_index: 1,
expected_type: stringify!($item_type),
raw_input: s.to_owned(),
}
})
})
.transpose()?;
Self::throw_if_too_many_arguments(parts)?;
Ok(paste::paste! { [<$name Request>] ( item ) })
}
}
};
}
macro_rules! single_item_command_response {
($name:ident, $item_name:expr, $item_type:ty) => {
paste::paste! {
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct [<$name Response>] (pub $item_type);
}
impl paste::paste! { [<$name Response>] } {
pub fn new(item: $item_type) -> Self {
paste::paste! {
crate::commands::[<$name Response>](item)
}
}
}
impl crate::commands::CommandResponse for paste::paste! { [<$name Response>] } {
fn parse(
parts: crate::commands::ResponseAttributes<'_>,
) -> Result<Self, crate::commands::ResponseParserError> {
let map = parts.into_map()?;
debug_assert!(map.len() == 1, "Expected only one property in response");
let item_token = map.get($item_name).ok_or(
crate::commands::ResponseParserError::MissingProperty($item_name.to_string()),
)?;
let item_ = crate::response_tokenizer::expect_property_type!(
Some(item_token),
$item_name,
Text
);
let item = item_.parse::<$item_type>().map_err(|_| {
crate::commands::ResponseParserError::InvalidProperty(
$item_name.to_string(),
item_.to_string(),
)
})?;
Ok(paste::paste! { [<$name Response>] ( item ) })
}
}
};
}
macro_rules! multi_item_command_response {
($name:ident, $item_name:expr, $item_type:ty) => {
paste::paste! {
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct [<$name Response>] (pub Vec<$item_type>);
}
impl paste::paste! { [<$name Response>] } {
pub fn new(items: Vec<$item_type>) -> Self {
paste::paste! {
crate::commands::[<$name Response>](items)
}
}
}
impl crate::commands::CommandResponse for paste::paste! { [<$name Response>] } {
fn parse(
parts: crate::commands::ResponseAttributes<'_>,
) -> Result<Self, crate::commands::ResponseParserError> {
// TODO: use lazy vec
let parts_: Vec<_> = parts.into_vec()?;
if let Some((k, _)) = parts_.iter().find(|(k, _)| *k != $item_name) {
return Err(ResponseParserError::UnexpectedProperty(k.to_string()));
}
let mut items = Vec::with_capacity(parts_.len());
let mut iter = parts_.into_iter();
while let Some(value) = iter.next() {
let unwrapped_value = expect_property_type!(Some(value.1), $item_name, Text);
let parsed_value = unwrapped_value.parse::<$item_type>().map_err(|_| {
crate::commands::ResponseParserError::InvalidProperty(
$item_name.to_string(),
unwrapped_value.to_string(),
)
})?;
items.push(parsed_value);
}
Ok(paste::paste! { [<$name Response>] ( items ) })
}
}
};
}
pub(crate) use empty_command_request;
pub(crate) use empty_command_response;
pub(crate) use multi_item_command_response;
pub(crate) use single_item_command_request;
pub(crate) use single_item_command_response;
pub(crate) use single_optional_item_command_request;
#[derive(Error, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq)]
pub enum RequestParserError {
#[error("Found empty line while parsing request")]
EmptyLine,
// TODO: remove this, replaced by various other errors
#[error("Could not parse the request due to a syntax error at position {0}: {1}")]
SyntaxError(u64, String),
// TODO: can we store the parser error as well?
#[error(
"Could not parse argument {argument_index} of the request (expected type: {expected_type}, raw input: '{raw_input}')"
)]
SubtypeParserError {
/// The index of the argument that failed to parse
///
/// Note that in the case of keyworded arguments, such as
/// `group <groupname>`, `sort <sorting>`, etc., these are
/// counted as a single argument despite being two tokens.
argument_index: u32,
/// The expected type of the argument
expected_type: &'static str,
/// The raw input that failed to parse
raw_input: String,
},
#[error(
"Too many arguments were provided in the request (expected between {expected_min} and {expected_max:?}, found {found})"
)]
TooManyArguments {
/// The minimum number of arguments that were expected
expected_min: u32,
/// The maximum number of arguments that were expected
///
/// This is `None` if the amount of arguments is unbounded.
expected_max: Option<u32>,
/// The number of arguments that were found
///
/// Note that in the case of keyworded arguments, such as
/// `group <groupname>`, `sort <sorting>`, etc., these are
/// counted as a single argument despite being two tokens.
found: u32,
},
#[error(
"Not enough arguments were provided in the request (expected between {expected_min} and {expected_max:?}, found {found})"
)]
MissingArguments {
/// The minimum number of arguments that were expected
expected_min: u32,
/// The maximum number of arguments that were expected.
///
/// This is `None` if the amount of arguments is unbounded.
expected_max: Option<u32>,
/// The number of arguments that were found
///
/// Note that in the case of keyworded arguments, such as
/// `group <groupname>`, `sort <sorting>`, etc., these are
/// counted as a single argument despite being two tokens.
found: u32,
},
#[error("Keyword argument {keyword} at position {argument_index} is missing its value")]
MissingKeywordValue {
/// The unexpected keyword that was found
keyword: &'static str,
/// The index of the argument that was missing it's value
///
/// Note that in the case of keyworded arguments, such as
/// `group <groupname>`, `sort <sorting>`, etc., these are
/// counted as a single argument despite being two tokens.
argument_index: u32,
},
#[error("A command list was expected to be closed, but the end was not found")]
MissingCommandListEnd,
#[error("A command list was found inside another command list at line {line}")]
NestedCommandList {
/// The line where the nested command list was found
line: u32,
},
#[error("An unexpected command list end was found")]
UnexpectedCommandListEnd,
// TODO: remove this, replaced by EmptyLine + MissingArguments
#[error("Request ended early, while more arguments were expected")]
MissingCommandListEnd(u64),
NestedCommandList(u64),
UnexpectedCommandListEnd(u64),
UnexpectedEOF,
#[error("Request is missing terminating newline")]
MissingNewline,
}
// TODO: should these be renamed to fit the mpd docs?
// "Attribute" instead of "Property"?
#[derive(Error, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ResponseParserError {
#[error("A property was expected to be present in the response, but was not found: {0}")]
MissingProperty(String),
// TODO: change name to UnexpectedPropertyEncoding
#[error(
"An expected property was found in the response, but its encoding was not as expected: {0}: {1}"
)]
UnexpectedPropertyType(String, String),
#[error("A property was found in the response that was not expected: {0}")]
UnexpectedProperty(String),
#[error("A property was found multiple times in the response, but was only expected once: {0}")]
DuplicateProperty(String),
#[error("The property value is parsable, but the value is invalid or nonsensical: {0}: {1}")]
InvalidProperty(String, String),
#[error("Could not parse the response due to a syntax error at position {0}: {1}")]
SyntaxError(u64, String),
#[error("Response ended early, while more properties were expected")]
#[derive(Debug, Clone, PartialEq)]
pub enum ResponseParserError<'a> {
MissingProperty(&'a str),
UnexpectedPropertyType(&'a str, &'a str),
UnexpectedProperty(&'a str),
InvalidProperty(&'a str, &'a str),
SyntaxError(u64, &'a str),
UnexpectedEOF,
// #[error("Response is missing terminating newline")]
// MissingNewline,
MissingNewline,
}
pub type GenericResponseResult<'a> = Result<GenericResponse<'a>, &'a str>;
pub type GenericResponse<'a> = HashMap<&'a str, GenericResponseValue<'a>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GenericResponseValue<'a> {
Text(&'a str),
Binary(&'a [u8]),
// Many(Vec<GenericResponseValue<'a>>),
}
pub struct ResponseAttributes<'a>(Vec<(&'a str, GenericResponseValue<'a>)>);
// impl ResponseAttributes<'_> {
// pub fn
// pub fn get<'a>(&self, key: &str) -> Option<&GenericResponseValue<'a>> {
// self.0.iter().find_map(|(k, v)| if k == &key { Some(v) } else { None })
// }
// }
impl ResponseAttributes<'_> {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl<'a> From<HashMap<&'a str, GenericResponseValue<'a>>> for ResponseAttributes<'a> {
fn from(map: HashMap<&'a str, GenericResponseValue<'a>>) -> Self {
Self(map.into_iter().collect())
}
}
impl<'a> From<ResponseAttributes<'a>> for HashMap<&'a str, GenericResponseValue<'a>> {
fn from(val: ResponseAttributes<'a>) -> Self {
debug_assert!({
let mut uniq = HashSet::new();
val.0.iter().all(move |x| uniq.insert(*x))
});
val.0.into_iter().collect()
}
}
impl<'a> From<ResponseAttributes<'a>> for Vec<(&'a str, GenericResponseValue<'a>)> {
fn from(val: ResponseAttributes<'a>) -> Self {
val.0
}
}
impl<'a> From<Vec<(&'a str, GenericResponseValue<'a>)>> for ResponseAttributes<'a> {
fn from(val: Vec<(&'a str, GenericResponseValue<'a>)>) -> Self {
Self(val)
}
}
// TODO: There should probably be a helper that lets you extract and verify one, two or maybe
// three properties without having to allocate a hashmap to get a nice API. We can retrieve
// the properties by name with a loop on the inner vec.
/*******************/
/* Parsing Helpers */
/*******************/
pub const COMMAND_NAMES: &[&str] = &[
// Audio output devices
DisableOutput::COMMAND,
EnableOutput::COMMAND,
Outputs::COMMAND,
OutputSet::COMMAND,
ToggleOutput::COMMAND,
// Client to client
Channels::COMMAND,
ReadMessages::COMMAND,
SendMessage::COMMAND,
Subscribe::COMMAND,
Unsubscribe::COMMAND,
// Connection settings
BinaryLimit::COMMAND,
Close::COMMAND,
Kill::COMMAND,
Password::COMMAND,
Ping::COMMAND,
Protocol::COMMAND,
ProtocolAll::COMMAND,
ProtocolAvailable::COMMAND,
ProtocolClear::COMMAND,
ProtocolDisable::COMMAND,
ProtocolEnable::COMMAND,
TagTypes::COMMAND,
TagTypesAll::COMMAND,
TagTypesAvailable::COMMAND,
TagTypesClear::COMMAND,
TagTypesDisable::COMMAND,
TagTypesEnable::COMMAND,
TagTypesReset::COMMAND,
// Controlling playback
Next::COMMAND,
Pause::COMMAND,
Play::COMMAND,
PlayId::COMMAND,
Previous::COMMAND,
Seek::COMMAND,
SeekCur::COMMAND,
SeekId::COMMAND,
Stop::COMMAND,
// Mounts and neighbors
ListMounts::COMMAND,
ListNeighbors::COMMAND,
Mount::COMMAND,
Unmount::COMMAND,
// Music database
AlbumArt::COMMAND,
Count::COMMAND,
Find::COMMAND,
FindAdd::COMMAND,
GetFingerprint::COMMAND,
List::COMMAND,
ListAll::COMMAND,
ListAllInfo::COMMAND,
ListFiles::COMMAND,
LsInfo::COMMAND,
ReadComments::COMMAND,
ReadPicture::COMMAND,
Rescan::COMMAND,
Search::COMMAND,
SearchAdd::COMMAND,
SearchAddPl::COMMAND,
SearchCount::COMMAND,
Update::COMMAND,
// Partition commands
DelPartition::COMMAND,
ListPartitions::COMMAND,
MoveOutput::COMMAND,
NewPartition::COMMAND,
Partition::COMMAND,
// Playback options
Consume::COMMAND,
Crossfade::COMMAND,
GetVol::COMMAND,
MixRampDb::COMMAND,
MixRampDelay::COMMAND,
Random::COMMAND,
Repeat::COMMAND,
ReplayGainMode::COMMAND,
ReplayGainStatus::COMMAND,
SetVol::COMMAND,
Single::COMMAND,
Volume::COMMAND,
// Querying mpd status
ClearError::COMMAND,
CurrentSong::COMMAND,
Idle::COMMAND,
Stats::COMMAND,
Status::COMMAND,
// Queue
Add::COMMAND,
AddId::COMMAND,
AddTagId::COMMAND,
Clear::COMMAND,
ClearTagId::COMMAND,
Delete::COMMAND,
DeleteId::COMMAND,
Move::COMMAND,
MoveId::COMMAND,
Playlist::COMMAND,
PlaylistFind::COMMAND,
PlaylistId::COMMAND,
PlaylistInfo::COMMAND,
PlaylistSearch::COMMAND,
PlChanges::COMMAND,
PlChangesPosId::COMMAND,
Prio::COMMAND,
PrioId::COMMAND,
RangeId::COMMAND,
Shuffle::COMMAND,
Swap::COMMAND,
SwapId::COMMAND,
// Reflection
Commands::COMMAND,
Config::COMMAND,
Decoders::COMMAND,
NotCommands::COMMAND,
UrlHandlers::COMMAND,
// Stickers
StickerDec::COMMAND,
StickerDelete::COMMAND,
StickerFind::COMMAND,
StickerGet::COMMAND,
StickerInc::COMMAND,
StickerList::COMMAND,
StickerSet::COMMAND,
StickerNames::COMMAND,
StickerNamesTypes::COMMAND,
StickerTypes::COMMAND,
// Stored playlists
ListPlaylist::COMMAND,
ListPlaylistInfo::COMMAND,
ListPlaylists::COMMAND,
Load::COMMAND,
PlaylistAdd::COMMAND,
PlaylistClear::COMMAND,
PlaylistDelete::COMMAND,
PlaylistLength::COMMAND,
PlaylistMove::COMMAND,
Rename::COMMAND,
Rm::COMMAND,
Save::COMMAND,
SearchPlaylist::COMMAND,
];
macro_rules! get_property {
($parts:expr, $name:literal, $variant:ident) => {
match $parts.get($name) {
Some(crate::commands::GenericResponseValue::$variant(value)) => *value,
Some(value) => {
let actual_type = match value {
crate::commands::GenericResponseValue::Text(_) => "Text",
crate::commands::GenericResponseValue::Binary(_) => "Binary",
};
return Err(
crate::commands::ResponseParserError::UnexpectedPropertyType(
$name,
actual_type,
),
);
}
None => return Err(crate::commands::ResponseParserError::MissingProperty($name)),
}
};
}
macro_rules! get_optional_property {
($parts:expr, $name:literal, $variant:ident) => {
match $parts.get($name) {
Some(crate::commands::GenericResponseValue::$variant(value)) => Some(*value),
Some(value) => {
let actual_type = match value {
crate::commands::GenericResponseValue::Text(_) => "Text",
crate::commands::GenericResponseValue::Binary(_) => "Binary",
};
return Err(
crate::commands::ResponseParserError::UnexpectedPropertyType(
$name,
actual_type,
),
);
}
None => None,
}
};
}
macro_rules! get_and_parse_property {
($parts:ident, $name:literal, $variant:ident) => {
match $parts.get($name) {
Some(crate::commands::GenericResponseValue::$variant(value)) => (*value)
.parse()
.map_err(|_| crate::commands::ResponseParserError::InvalidProperty($name, value))?,
Some(value) => {
let actual_type = match value {
crate::commands::GenericResponseValue::Text(_) => "Text",
crate::commands::GenericResponseValue::Binary(_) => "Binary",
};
return Err(
crate::commands::ResponseParserError::UnexpectedPropertyType(
$name,
actual_type,
),
);
}
None => return Err(crate::commands::ResponseParserError::MissingProperty($name)),
}
};
}
macro_rules! get_and_parse_optional_property {
($parts:ident, $name:literal, $variant:ident) => {
match $parts.get($name) {
Some(crate::commands::GenericResponseValue::$variant(value)) => {
Some((*value).parse().map_err(|_| {
crate::commands::ResponseParserError::InvalidProperty($name, value)
})?)
}
Some(value) => {
let actual_type = match value {
crate::commands::GenericResponseValue::Text(_) => "Text",
crate::commands::GenericResponseValue::Binary(_) => "Binary",
};
return Err(
crate::commands::ResponseParserError::UnexpectedPropertyType(
$name,
actual_type,
),
);
}
None => None,
}
};
}
pub(crate) use get_and_parse_optional_property;
pub(crate) use get_and_parse_property;
pub(crate) use get_optional_property;
pub(crate) use get_property;
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(debug_assertions)]
fn test_valid_hashmap_uniqueness_assert() {
let valid_maplike_attrs: ResponseAttributes = vec![
("a", GenericResponseValue::Text("1")),
("A", GenericResponseValue::Text("2")),
("A ", GenericResponseValue::Text("3")),
("b", GenericResponseValue::Text("4")),
]
.into();
let map: HashMap<_, _> = valid_maplike_attrs.into();
assert_eq!(map.len(), 4);
}
#[test]
#[cfg(debug_assertions)]
#[should_panic]
fn test_invalid_hashmap_uniqueness_assert() {
let invalid_maplike_attrs: ResponseAttributes = vec![
("a", GenericResponseValue::Text("1")),
("b", GenericResponseValue::Text("2")),
("c", GenericResponseValue::Text("3")),
("a", GenericResponseValue::Text("4")),
]
.into();
let map: HashMap<_, _> = invalid_maplike_attrs.into();
assert_eq!(map.len(), 4);
}
}
+10 -10
View File
@@ -1,11 +1,11 @@
mod disableoutput;
mod enableoutput;
mod outputs;
mod outputset;
mod toggleoutput;
pub mod disableoutput;
pub mod enableoutput;
pub mod outputs;
pub mod outputset;
pub mod toggleoutput;
pub use disableoutput::*;
pub use enableoutput::*;
pub use outputs::*;
pub use outputset::*;
pub use toggleoutput::*;
pub use disableoutput::DisableOutput;
pub use enableoutput::EnableOutput;
pub use outputs::Outputs;
pub use outputset::OutputSet;
pub use toggleoutput::ToggleOutput;
@@ -1,15 +1,26 @@
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::AudioOutputId,
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct DisableOutput;
single_item_command_request!(DisableOutput, "disableoutput", AudioOutputId);
empty_command_response!(DisableOutput);
impl Command for DisableOutput {
type Request = DisableOutputRequest;
type Response = DisableOutputResponse;
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(())
}
}
@@ -1,15 +1,26 @@
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::AudioOutputId,
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct EnableOutput;
single_item_command_request!(EnableOutput, "enableoutput", AudioOutputId);
empty_command_response!(EnableOutput);
impl Command for EnableOutput {
type Request = EnableOutputRequest;
type Response = EnableOutputResponse;
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(())
}
}
+13 -165
View File
@@ -2,187 +2,35 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandResponse, ResponseParserError, empty_command_request},
response_tokenizer::{ResponseAttributes, expect_property_type},
types::AudioOutputId,
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
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: AudioOutputId,
pub id: u64,
pub name: String,
pub plugin: String,
pub enabled: bool,
pub attribute: HashMap<String, String>,
}
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))
}
}
pub type OutputsResponse = Vec<Output>;
impl Command for Outputs {
type Request = OutputsRequest;
type Response = OutputsResponse;
}
const COMMAND: &'static str = "outputs";
#[cfg(test)]
mod tests {
use indoc::indoc;
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::Outputs, ""))
}
use super::*;
#[test]
fn test_parse_response() {
let input = indoc! {"
outputid: 0
outputname: PipeWire Sound Server
plugin: pipewire
outputenabled: 1
outputid: 1
outputname: Visualizer feed
plugin: fifo
outputenabled: 1
attribute: fifo_path=/tmp/empidee-visualizer.fifo
OK
"};
let result = Outputs::parse_raw_response(input.as_bytes());
assert_eq!(
result,
Ok(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
},
},
])),
);
fn parse_response(
_parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
}
+29 -64
View File
@@ -1,70 +1,35 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::AudioOutputId,
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct OutputSet;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OutputSetRequest {
pub output_id: AudioOutputId,
pub attribute_name: String,
pub attribute_value: String,
}
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;
type Response = ();
const COMMAND: &'static str = "outputset";
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(())
}
}
@@ -1,15 +1,26 @@
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::AudioOutputId,
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct ToggleOutput;
single_item_command_request!(ToggleOutput, "toggleoutput", AudioOutputId);
empty_command_response!(ToggleOutput);
impl Command for ToggleOutput {
type Request = ToggleOutputRequest;
type Response = ToggleOutputResponse;
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(())
}
}
+10 -10
View File
@@ -1,11 +1,11 @@
mod channels;
mod readmessages;
mod sendmessage;
mod subscribe;
mod unsubscribe;
pub mod channels;
pub mod readmessages;
pub mod sendmessage;
pub mod subscribe;
pub mod unsubscribe;
pub use channels::*;
pub use readmessages::*;
pub use sendmessage::*;
pub use subscribe::*;
pub use unsubscribe::*;
pub use channels::Channels;
pub use readmessages::ReadMessages;
pub use sendmessage::SendMessage;
pub use subscribe::Subscribe;
pub use unsubscribe::Unsubscribe;
+27 -30
View File
@@ -1,37 +1,43 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandResponse, ResponseParserError, empty_command_request},
response_tokenizer::{ResponseAttributes, expect_property_type},
types::ChannelName,
use crate::commands::{
Command, GenericResponseValue, Request, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct Channels;
empty_command_request!(Channels, "channels");
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChannelsResponse {
pub channels: Vec<ChannelName>,
pub channels: Vec<String>,
}
impl ChannelsResponse {
pub fn new(channels: Vec<ChannelName>) -> Self {
ChannelsResponse { channels }
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 CommandResponse for ChannelsResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: Vec<_> = parts.into_vec()?;
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: Vec<_> = parts.into();
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);
let channel_name = channel_name
.parse()
.map_err(|_| ResponseParserError::SyntaxError(0, channel_name.to_string()))?;
channel_names.push(channel_name);
let channel_name = match value {
GenericResponseValue::Text(s) => s,
GenericResponseValue::Binary(_) => {
return Err(ResponseParserError::UnexpectedPropertyType(
"channels", "Binary",
));
}
};
channel_names.push(channel_name.to_string());
}
Ok(ChannelsResponse {
@@ -40,11 +46,6 @@ impl CommandResponse for ChannelsResponse {
}
}
impl Command for Channels {
type Request = ChannelsRequest;
type Response = ChannelsResponse;
}
#[cfg(test)]
mod tests {
use super::*;
@@ -59,15 +60,11 @@ mod tests {
channels: baz
OK
"};
let response = Channels::parse_raw_response(response.as_bytes()).unwrap();
let response = Channels::parse_raw_response(response).unwrap();
assert_eq!(
response,
ChannelsResponse {
channels: vec![
"foo".parse().unwrap(),
"bar".parse().unwrap(),
"baz".parse().unwrap(),
]
channels: vec!["foo".to_string(), "bar".to_string(), "baz".to_string()]
}
);
}
+43 -50
View File
@@ -1,39 +1,31 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandResponse, ResponseParserError, empty_command_request},
response_tokenizer::{ResponseAttributes, expect_property_type},
types::ChannelName,
use crate::commands::{
Command, GenericResponseValue, Request, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct ReadMessages;
empty_command_request!(ReadMessages, "readmessages");
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReadMessagesResponse(Vec<ReadMessagesResponseEntry>);
pub struct ReadMessagesResponse {
pub messages: Vec<(String, String)>,
}
impl ReadMessagesResponse {
pub fn new(entries: Vec<ReadMessagesResponseEntry>) -> Self {
ReadMessagesResponse(entries)
impl Command for ReadMessages {
type Response = ReadMessagesResponse;
const COMMAND: &'static str = "readmessages";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ReadMessages, ""))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReadMessagesResponseEntry {
channel: ChannelName,
message: String,
}
impl ReadMessagesResponseEntry {
pub fn new(channel: ChannelName, message: String) -> Self {
ReadMessagesResponseEntry { channel, message }
}
}
impl CommandResponse for ReadMessagesResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: Vec<_> = parts.into_vec()?;
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: Vec<_> = parts.into();
debug_assert!(parts.len() % 2 == 0);
let mut messages = Vec::with_capacity(parts.len() / 2);
@@ -41,29 +33,34 @@ impl CommandResponse for ReadMessagesResponse {
for channel_message_pair in parts.chunks_exact(2) {
let (ckey, cvalue) = channel_message_pair[0];
let (mkey, mvalue) = channel_message_pair[1];
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 = match cvalue {
GenericResponseValue::Text(s) => s.to_string(),
GenericResponseValue::Binary(_) => {
return Err(ResponseParserError::UnexpectedPropertyType(
"channel", "Binary",
))
}
};
let message = expect_property_type!(Some(mvalue), "message", Text).to_string();
let message = match mvalue {
GenericResponseValue::Text(s) => s.to_string(),
GenericResponseValue::Binary(_) => {
return Err(ResponseParserError::UnexpectedPropertyType(
"message", "Binary",
))
}
};
messages.push(ReadMessagesResponseEntry { channel, message });
messages.push((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;
@@ -79,19 +76,15 @@ mod tests {
message: message2
OK
"};
let result = ReadMessages::parse_raw_response(input.as_bytes());
let result = ReadMessages::parse_raw_response(input);
assert_eq!(
result,
Ok(ReadMessagesResponse(vec![
ReadMessagesResponseEntry {
channel: "channel1".parse().unwrap(),
message: "message1".to_string(),
},
ReadMessagesResponseEntry {
channel: "channel2".parse().unwrap(),
message: "message2".to_string(),
},
]))
Ok(ReadMessagesResponse {
messages: vec![
("channel1".to_string(), "message1".to_string()),
("channel2".to_string(), "message2".to_string()),
]
})
);
}
}
+17 -42
View File
@@ -1,54 +1,29 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::ChannelName,
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct SendMessage;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SendMessageRequest {
pub channel: ChannelName,
pub message: String,
}
impl SendMessageRequest {
pub fn new(channel: ChannelName, message: String) -> Self {
Self { channel, message }
}
}
impl CommandRequest for SendMessageRequest {
impl Command for SendMessage {
type Response = ();
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(),
})?;
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let channel = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
// TODO: SplitWhitespace::remainder() is unstable, use when stable
let message = parts.collect::<Vec<_>>().join(" ");
Ok(SendMessageRequest { channel, message })
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(())
}
}
empty_command_response!(SendMessage);
impl Command for SendMessage {
type Request = SendMessageRequest;
type Response = SendMessageResponse;
}
+20 -9
View File
@@ -1,15 +1,26 @@
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::ChannelName,
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct Subscribe;
single_item_command_request!(Subscribe, "subscribe", ChannelName);
empty_command_response!(Subscribe);
impl Command for Subscribe {
type Request = SubscribeRequest;
type Response = SubscribeResponse;
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(())
}
}
+20 -9
View File
@@ -1,15 +1,26 @@
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::ChannelName,
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct Unsubscribe;
single_item_command_request!(Unsubscribe, "unsubscribe", ChannelName);
empty_command_response!(Unsubscribe);
impl Command for Unsubscribe {
type Request = UnsubscribeRequest;
type Response = UnsubscribeResponse;
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(())
}
}
+36 -36
View File
@@ -1,37 +1,37 @@
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 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;
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::*;
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;
@@ -1,12 +1,27 @@
use crate::commands::{Command, empty_command_response, single_item_command_request};
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct BinaryLimit;
single_item_command_request!(BinaryLimit, "binarylimit", u64);
empty_command_response!(BinaryLimit);
impl Command for BinaryLimit {
type Request = BinaryLimitRequest;
type Response = BinaryLimitResponse;
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(())
}
}
+17 -7
View File
@@ -1,12 +1,22 @@
use crate::commands::{Command, empty_command_request, empty_command_response};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
pub struct Close;
empty_command_request!(Close, "close");
empty_command_response!(Close);
impl Command for Close {
type Request = CloseRequest;
type Response = CloseResponse;
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(())
}
}
+17 -7
View File
@@ -1,12 +1,22 @@
use crate::commands::{Command, empty_command_request, empty_command_response};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
pub struct Kill;
empty_command_request!(Kill, "kill");
empty_command_response!(Kill);
impl Command for Kill {
type Request = KillRequest;
type Response = KillResponse;
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(())
}
}
+22 -7
View File
@@ -1,12 +1,27 @@
use crate::commands::{Command, empty_command_response, single_item_command_request};
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct Password;
single_item_command_request!(Password, "password", String);
empty_command_response!(Password);
impl Command for Password {
type Request = PasswordRequest;
type Response = PasswordResponse;
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(())
}
}
+17 -7
View File
@@ -1,12 +1,22 @@
use crate::commands::{Command, empty_command_request, empty_command_response};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
pub struct Ping;
empty_command_request!(Ping, "ping");
empty_command_response!(Ping);
impl Command for Ping {
type Request = PingRequest;
type Response = PingResponse;
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(())
}
}
+15 -8
View File
@@ -1,15 +1,22 @@
use crate::{
commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
response_tokenizer::expect_property_type,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
Request,
};
pub struct Protocol;
empty_command_request!(Protocol, "protocol");
multi_item_command_response!(Protocol, "feature", String);
impl Command for Protocol {
type Request = ProtocolRequest;
type Response = ProtocolResponse;
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!()
}
}
@@ -1,12 +1,23 @@
use crate::commands::{Command, empty_command_request, empty_command_response};
use crate::{
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
Request,
};
pub struct ProtocolAll;
empty_command_request!(ProtocolAll, "protocol all");
empty_command_response!(ProtocolAll);
impl Command for ProtocolAll {
type Request = ProtocolAllRequest;
type Response = ProtocolAllResponse;
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(())
}
}
@@ -1,15 +1,22 @@
use crate::{
commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
response_tokenizer::expect_property_type,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
Request,
};
pub struct ProtocolAvailable;
empty_command_request!(ProtocolAvailable, "protocol available");
multi_item_command_response!(ProtocolAvailable, "feature", String);
impl Command for ProtocolAvailable {
type Request = ProtocolAvailableRequest;
type Response = ProtocolAvailableResponse;
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!()
}
}
@@ -1,12 +1,23 @@
use crate::commands::{Command, empty_command_request, empty_command_response};
use crate::{
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
Request,
};
pub struct ProtocolClear;
empty_command_request!(ProtocolClear, "protocol clear");
empty_command_response!(ProtocolClear);
impl Command for ProtocolClear {
type Request = ProtocolClearRequest;
type Response = ProtocolClearResponse;
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(())
}
}
@@ -1,59 +1,35 @@
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::Feature,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
Request,
};
pub struct ProtocolDisable;
pub struct ProtocolDisableRequest(Vec<Feature>);
impl ProtocolDisableRequest {
pub fn new(features: Vec<Feature>) -> Self {
ProtocolDisableRequest(features)
}
}
impl CommandRequest for ProtocolDisableRequest {
impl Command for ProtocolDisable {
type Response = ();
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> {
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let mut parts = parts.peekable();
if parts.peek().is_none() {
return Err(Self::missing_arguments_error(0));
return Err(RequestParserError::UnexpectedEOF);
}
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>>()?;
// TODO: verify that the features are split by whitespace
let mut features = Vec::new();
for feature in parts {
features.push(feature.to_string());
}
Ok(ProtocolDisableRequest(features))
Ok((Request::ProtocolDisable(features), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
}
empty_command_response!(ProtocolDisable);
impl Command for ProtocolDisable {
type Request = ProtocolDisableRequest;
type Response = ProtocolDisableResponse;
}
@@ -1,59 +1,35 @@
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::Feature,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
Request,
};
pub struct ProtocolEnable;
pub struct ProtocolEnableRequest(Vec<Feature>);
impl ProtocolEnableRequest {
pub fn new(features: Vec<Feature>) -> Self {
ProtocolEnableRequest(features)
}
}
impl CommandRequest for ProtocolEnableRequest {
impl Command for ProtocolEnable {
type Response = ();
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> {
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let mut parts = parts.peekable();
if parts.peek().is_none() {
return Err(Self::missing_arguments_error(0));
return Err(RequestParserError::UnexpectedEOF);
}
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>>()?;
// TODO: verify that the features are split by whitespace
let mut features = Vec::new();
for feature in parts {
features.push(feature.to_string());
}
Ok(ProtocolEnableRequest(features))
Ok((Request::ProtocolEnable(features), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
}
empty_command_response!(ProtocolEnable);
impl Command for ProtocolEnable {
type Request = ProtocolEnableRequest;
type Response = ProtocolEnableResponse;
}
+34 -7
View File
@@ -1,15 +1,42 @@
use crate::{
commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
response_tokenizer::expect_property_type,
use crate::commands::{
Command, GenericResponseValue, Request, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct TagTypes;
empty_command_request!(TagTypes, "tagtypes");
multi_item_command_response!(TagTypes, "tagtype", String);
pub type TagTypesResponse = Vec<String>;
impl Command for TagTypes {
type Request = TagTypesRequest;
type Response = TagTypesResponse;
const COMMAND: &'static str = "tagtypes";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::TagTypes, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: Vec<_> = parts.into();
let mut tagtypes = Vec::with_capacity(parts.len());
for (key, value) in parts.into_iter() {
debug_assert_eq!(key, "tagtype");
let tagtype = match value {
GenericResponseValue::Text(name) => name.to_string(),
GenericResponseValue::Binary(_) => {
return Err(ResponseParserError::UnexpectedPropertyType(
"tagtype", "Binary",
))
}
};
tagtypes.push(tagtype);
}
Ok(tagtypes)
}
}
@@ -1,12 +1,23 @@
use crate::commands::{Command, empty_command_request, empty_command_response};
use crate::{
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
Request,
};
pub struct TagTypesAll;
empty_command_request!(TagTypesAll, "tagtypes all");
empty_command_response!(TagTypesAll);
impl Command for TagTypesAll {
type Request = TagTypesAllRequest;
type Response = TagTypesAllResponse;
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(())
}
}
@@ -1,15 +1,22 @@
use crate::{
commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
response_tokenizer::expect_property_type,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
Request,
};
pub struct TagTypesAvailable;
empty_command_request!(TagTypesAvailable, "tagtypes available");
multi_item_command_response!(TagTypesAvailable, "tagtype", String);
impl Command for TagTypesAvailable {
type Request = TagTypesAvailableRequest;
type Response = TagTypesAvailableResponse;
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!()
}
}
@@ -1,12 +1,23 @@
use crate::commands::{Command, empty_command_request, empty_command_response};
use crate::{
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
Request,
};
pub struct TagTypesClear;
empty_command_request!(TagTypesClear, "tagtypes clear");
empty_command_response!(TagTypesClear);
impl Command for TagTypesClear {
type Request = TagTypesClearRequest;
type Response = TagTypesClearResponse;
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(())
}
}
@@ -1,61 +1,35 @@
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::TagName,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
Request,
};
pub struct TagTypesDisable;
pub struct TagTypesDisableRequest(Vec<TagName>);
impl TagTypesDisableRequest {
pub fn new(tag_types: Vec<TagName>) -> Self {
TagTypesDisableRequest(tag_types)
}
}
impl CommandRequest for TagTypesDisableRequest {
impl Command for TagTypesDisable {
type Response = ();
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> {
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let mut parts = parts.peekable();
if parts.peek().is_none() {
return Err(Self::missing_arguments_error(0));
return Err(RequestParserError::UnexpectedEOF);
}
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>>()?;
// 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(TagTypesDisableRequest(tag_types))
Ok((Request::TagTypesDisable(tag_types), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
}
empty_command_response!(TagTypesDisable);
impl Command for TagTypesDisable {
type Request = TagTypesDisableRequest;
type Response = TagTypesDisableResponse;
}
@@ -1,61 +1,35 @@
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::TagName,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
Request,
};
pub struct TagTypesEnable;
pub struct TagTypesEnableRequest(Vec<TagName>);
impl TagTypesEnableRequest {
pub fn new(tag_types: Vec<TagName>) -> Self {
TagTypesEnableRequest(tag_types)
}
}
impl CommandRequest for TagTypesEnableRequest {
impl Command for TagTypesEnable {
type Response = ();
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> {
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let mut parts = parts.peekable();
if parts.peek().is_none() {
return Err(Self::missing_arguments_error(0));
return Err(RequestParserError::UnexpectedEOF);
}
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>>()?;
// 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(TagTypesEnableRequest(tag_types))
Ok((Request::TagTypesEnable(tag_types), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
}
empty_command_response!(TagTypesEnable);
impl Command for TagTypesEnable {
type Request = TagTypesEnableRequest;
type Response = TagTypesEnableResponse;
}
@@ -1,61 +1,35 @@
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::TagName,
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
Request,
};
pub struct TagTypesReset;
pub struct TagTypesResetRequest(Vec<TagName>);
impl TagTypesResetRequest {
pub fn new(tag_types: Vec<TagName>) -> Self {
TagTypesResetRequest(tag_types)
}
}
impl CommandRequest for TagTypesResetRequest {
impl Command for TagTypesReset {
type Response = ();
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> {
fn parse_request(parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let mut parts = parts.peekable();
if parts.peek().is_none() {
return Err(Self::missing_arguments_error(0));
return Err(RequestParserError::UnexpectedEOF);
}
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>>()?;
// 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(TagTypesResetRequest(tag_types))
Ok((Request::TagTypesReset(tag_types), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
}
empty_command_response!(TagTypesReset);
impl Command for TagTypesReset {
type Request = TagTypesResetRequest;
type Response = TagTypesResetResponse;
}
+18 -18
View File
@@ -1,19 +1,19 @@
mod next;
mod pause;
mod play;
mod playid;
mod previous;
mod seek;
mod seekcur;
mod seekid;
mod stop;
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;
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::*;
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;
+17 -7
View File
@@ -1,12 +1,22 @@
use crate::commands::{Command, empty_command_request, empty_command_response};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
pub struct Next;
empty_command_request!(Next, "next");
empty_command_response!(Next);
impl Command for Next {
type Request = NextRequest;
type Response = NextResponse;
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(())
}
}
+19 -40
View File
@@ -1,52 +1,31 @@
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct Pause;
pub struct PauseRequest(Option<bool>);
impl PauseRequest {
pub fn new(state: Option<bool>) -> Self {
PauseRequest(state)
}
}
impl CommandRequest for PauseRequest {
impl Command for Pause {
type Response = ();
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> {
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
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),
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), "")),
};
Self::throw_if_too_many_arguments(parts)?;
debug_assert!(parts.next().is_none());
result.map(PauseRequest)
result
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
}
empty_command_response!(Pause);
impl Command for Pause {
type Request = PauseRequest;
type Response = PauseResponse;
}
+27 -8
View File
@@ -1,15 +1,34 @@
use crate::{
commands::{Command, empty_command_response, single_optional_item_command_request},
types::SongPosition,
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::SongPosition,
};
pub struct Play;
single_optional_item_command_request!(Play, "play", SongPosition);
empty_command_response!(Play);
impl Command for Play {
type Request = PlayRequest;
type Response = PlayResponse;
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(())
}
}
+27 -8
View File
@@ -1,15 +1,34 @@
use crate::{
commands::{Command, empty_command_response, single_optional_item_command_request},
types::SongId,
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::SongId,
};
pub struct PlayId;
single_optional_item_command_request!(PlayId, "playid", SongId);
empty_command_response!(PlayId);
impl Command for PlayId {
type Request = PlayIdRequest;
type Response = PlayIdResponse;
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(())
}
}
+17 -7
View File
@@ -1,12 +1,22 @@
use crate::commands::{Command, empty_command_request, empty_command_response};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
pub struct Previous;
empty_command_request!(Previous, "previous");
empty_command_response!(Previous);
impl Command for Previous {
type Request = PreviousRequest;
type Response = PreviousResponse;
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(())
}
}
+25 -52
View File
@@ -1,68 +1,41 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::{SongPosition, TimeWithFractions},
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::{SongPosition, TimeWithFractions},
};
pub struct Seek;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SeekRequest {
pub songpos: SongPosition,
pub time: TimeWithFractions,
}
impl SeekRequest {
pub fn new(songpos: SongPosition, time: TimeWithFractions) -> Self {
Self { songpos, time }
}
}
impl CommandRequest for SeekRequest {
impl Command for Seek {
type Response = ();
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> {
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let songpos = match parts.next() {
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)),
Some(s) => s
.parse::<SongPosition>()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
let time = match parts.next() {
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)),
Some(t) => t
.parse::<TimeWithFractions>()
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
Self::throw_if_too_many_arguments(parts)?;
debug_assert!(parts.next().is_none());
Ok(SeekRequest { songpos, time })
Ok((Request::Seek(songpos, time), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
}
empty_command_response!(Seek);
impl Command for Seek {
type Request = SeekRequest;
type Response = SeekResponse;
}
+28 -65
View File
@@ -1,91 +1,54 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::{SeekMode, TimeWithFractions},
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::TimeWithFractions,
request::SeekMode,
};
pub struct SeekCur;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SeekCurRequest {
pub mode: SeekMode,
pub time: TimeWithFractions,
}
impl SeekCurRequest {
pub fn new(mode: SeekMode, time: TimeWithFractions) -> Self {
Self { mode, time }
}
}
impl CommandRequest for SeekCurRequest {
impl Command for SeekCur {
type Response = ();
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> {
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let time_raw = match parts.next() {
Some(t) => t,
None => return Err(Self::missing_arguments_error(0)),
None => return Err(RequestParserError::UnexpectedEOF),
};
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::SubtypeParserError {
argument_index: 0,
expected_type: "TimeWithFractions",
raw_input: t[1..].to_owned(),
}
})?,
t[1..]
.parse::<TimeWithFractions>()
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?,
),
t if t.starts_with('-') => (
SeekMode::RelativeReverse,
t[1..].parse::<TimeWithFractions>().map_err(|_| {
RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "TimeWithFractions",
raw_input: t[1..].to_owned(),
}
})?,
SeekMode::Relative,
-t[1..]
.parse::<TimeWithFractions>()
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?,
),
t => (
SeekMode::Absolute,
t.parse::<TimeWithFractions>().map_err(|_| {
RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "TimeWithFractions",
raw_input: t.to_owned(),
}
})?,
t.parse::<TimeWithFractions>()
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?,
),
};
debug_assert!(time >= 0.0);
debug_assert!(parts.next().is_none());
Ok(SeekCurRequest { mode, time })
Ok((Request::SeekCur(mode, time), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
}
empty_command_response!(SeekCur);
impl Command for SeekCur {
type Request = SeekCurRequest;
type Response = SeekCurResponse;
}
+23 -48
View File
@@ -1,66 +1,41 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::{SongId, TimeWithFractions},
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::{SongId, TimeWithFractions},
};
pub struct SeekId;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SeekIdRequest {
pub songid: SongId,
pub time: TimeWithFractions,
}
impl SeekIdRequest {
pub fn new(songid: SongId, time: TimeWithFractions) -> Self {
Self { songid, time }
}
}
impl CommandRequest for SeekIdRequest {
impl Command for SeekId {
type Response = ();
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> {
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let songid = match parts.next() {
Some(s) => s
.parse::<SongId>()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "SongId",
raw_input: s.to_owned(),
})?,
None => return Err(Self::missing_arguments_error(0)),
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
let time = match parts.next() {
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)),
Some(t) => t
.parse::<TimeWithFractions>()
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
Self::throw_if_too_many_arguments(parts)?;
debug_assert!(parts.next().is_none());
Ok(SeekIdRequest { songid, time })
Ok((Request::SeekId(songid, time), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
}
empty_command_response!(SeekId);
impl Command for SeekId {
type Request = SeekIdRequest;
type Response = SeekIdResponse;
}
+17 -7
View File
@@ -1,12 +1,22 @@
use crate::commands::{Command, empty_command_request, empty_command_response};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
pub struct Stop;
empty_command_request!(Stop, "stop");
empty_command_response!(Stop);
impl Command for Stop {
type Request = StopRequest;
type Response = StopResponse;
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(())
}
}
+8 -8
View File
@@ -1,9 +1,9 @@
mod listmounts;
mod listneighbors;
mod mount;
mod unmount;
pub mod listmounts;
pub mod listneighbors;
pub mod mount;
pub mod unmount;
pub use listmounts::*;
pub use listneighbors::*;
pub use mount::*;
pub use unmount::*;
pub use listmounts::ListMounts;
pub use listneighbors::ListNeighbors;
pub use mount::Mount;
pub use unmount::Unmount;
+12 -29
View File
@@ -1,39 +1,22 @@
use crate::{
commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
response_tokenizer::expect_property_type,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
Request,
};
pub struct ListMounts;
empty_command_request!(ListMounts, "listmounts");
multi_item_command_response!(ListMounts, "mount", String);
impl Command for ListMounts {
type Request = ListMountsRequest;
type Response = ListMountsResponse;
}
type Response = Vec<(String, String)>;
const COMMAND: &'static str = "listmounts";
#[cfg(test)]
mod tests {
use indoc::indoc;
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ListMounts, ""))
}
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(),
]))
);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
}
@@ -1,50 +1,22 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandResponse, ResponseParserError, empty_command_request},
response_tokenizer::{ResponseAttributes, expect_property_type},
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
Request,
};
pub struct ListNeighbors;
empty_command_request!(ListNeighbors, "listneighbors");
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ListNeighborsResponse(HashMap<String, String>);
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;
type Response = Vec<(String, String)>;
const COMMAND: &'static str = "listneighbors";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ListNeighbors, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
}
+21 -49
View File
@@ -1,64 +1,36 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
types::{MountPath, Uri},
commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
Request,
};
pub struct Mount;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MountRequest {
pub path: MountPath,
pub uri: Uri,
}
impl MountRequest {
pub fn new(path: MountPath, uri: Uri) -> Self {
MountRequest { path, uri }
}
}
impl CommandRequest for MountRequest {
impl Command for Mount {
type Response = ();
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(),
})?;
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let path = parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
let uri = parts
.next()
.ok_or(Self::missing_arguments_error(1))?
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
Self::throw_if_too_many_arguments(parts)?;
debug_assert!(parts.next().is_none());
Ok(MountRequest { path, uri })
Ok((Request::Mount(path, uri), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
}
empty_command_response!(Mount);
impl Command for Mount {
type Request = MountRequest;
type Response = MountResponse;
}
+22 -45
View File
@@ -1,54 +1,31 @@
use crate::{
commands::{
Command, CommandRequest, RequestParserError, RequestTokenizer, empty_command_response,
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
types::MountPath,
Request,
};
pub struct Unmount;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct UnmountRequest(MountPath);
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;
type Response = ();
const COMMAND: &'static str = "unmount";
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(())
}
}
+36 -36
View File
@@ -1,37 +1,37 @@
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 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;
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::*;
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;
+30 -67
View File
@@ -1,78 +1,46 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_and_parse_property, get_property},
types::{Offset, Uri},
commands::{
get_and_parse_property, get_property, Command, Request, RequestParserError,
RequestParserResult, ResponseAttributes, ResponseParserError,
},
common::Offset,
};
pub struct AlbumArt;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AlbumArtRequest {
uri: Uri,
offset: Offset,
}
impl AlbumArtRequest {
pub fn new(uri: Uri, offset: Offset) -> Self {
AlbumArtRequest { uri, offset }
}
}
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(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_string(),
})?,
None => return Err(Self::missing_arguments_error(1)),
};
Self::throw_if_too_many_arguments(parts)?;
Ok(AlbumArtRequest {
uri: uri.to_string(),
offset,
})
}
}
#[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 Command for AlbumArt {
type Response = AlbumArtResponse;
const COMMAND: &'static str = "albumart";
impl CommandResponse for AlbumArtResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
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::AlbumArt(uri.to_string(), offset), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
let size = get_and_parse_property!(parts, "size", Text);
@@ -81,8 +49,3 @@ impl CommandResponse for AlbumArtResponse {
Ok(AlbumArtResponse { size, binary })
}
}
impl Command for AlbumArt {
type Request = AlbumArtRequest;
type Response = AlbumArtResponse;
}
+32 -82
View File
@@ -1,93 +1,48 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_and_parse_property},
types::GroupType,
commands::{
get_and_parse_property, Command, Request, RequestParserError, RequestParserResult,
ResponseAttributes, ResponseParserError,
},
filter::parse_filter,
};
pub struct Count;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CountRequest {
filter: Filter,
group: Option<GroupType>,
}
impl CountRequest {
pub fn new(filter: Filter, group: Option<GroupType>) -> Self {
CountRequest { filter, group }
}
}
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::MissingKeywordValue {
argument_index: 1,
keyword: "group",
})?;
Some(
group
.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 1,
expected_type: "GroupType",
raw_input: group.to_owned(),
})?,
)
} else {
None
};
Self::throw_if_too_many_arguments(parts)?;
Ok(CountRequest { filter, group })
}
}
#[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 Command for Count {
type Response = CountResponse;
const COMMAND: &'static str = "count";
impl CommandResponse for CountResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
let group = if let Some("group") = parts.next() {
Some(
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.parse()
.map_err(|_| RequestParserError::SyntaxError(1, "group".to_owned()))?,
)
} else {
None
};
debug_assert!(parts.next().is_none());
Ok((Request::Count(filter, group), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
let songs = get_and_parse_property!(parts, "songs", Text);
let playtime = get_and_parse_property!(parts, "playtime", Text);
@@ -95,8 +50,3 @@ impl CommandResponse for CountResponse {
Ok(CountResponse { songs, playtime })
}
}
impl Command for Count {
type Request = CountRequest;
type Response = CountResponse;
}
+24 -109
View File
@@ -1,136 +1,51 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
types::{DbSelectionPrintResponse, DbSongInfo, Sort, WindowRange},
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
};
pub struct Find;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FindRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
}
pub struct FindResponse {}
impl FindRequest {
pub fn new(filter: Filter, sort: Option<Sort>, window: Option<WindowRange>) -> Self {
Self {
filter,
sort,
window,
}
}
}
impl CommandRequest for FindRequest {
impl Command for Find {
type Response = FindResponse;
const COMMAND: &'static str = "find";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(3);
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_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
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(
s.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "Sort",
raw_input: s.to_string(),
})?,
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
);
sort_or_window = parts.next();
}
let mut window = None;
if let Some("window") = sort_or_window {
argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
window = Some(
w.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?,
);
}
Self::throw_if_too_many_arguments(parts)?;
debug_assert!(parts.next().is_none());
Ok(FindRequest {
filter,
sort,
window,
})
Ok((Request::Find(filter, sort, window), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
}
#[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;
}
+26 -106
View File
@@ -1,141 +1,61 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
filter::Filter,
request_tokenizer::RequestTokenizer,
types::{SongPosition, Sort, WindowRange},
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
};
pub struct FindAdd;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FindAddRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
}
pub struct FindAddResponse {}
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 {
impl Command for FindAdd {
type Response = FindAddResponse;
const COMMAND: &'static str = "findadd";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(4);
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_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
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(
s.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "Sort",
raw_input: s.to_string(),
})?,
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
);
sort_or_window_or_position = parts.next();
}
let mut window = None;
if let Some("window") = sort_or_window_or_position {
argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
window = Some(
w.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?,
);
sort_or_window_or_position = parts.next();
}
let mut position = None;
if let Some("position") = sort_or_window_or_position {
argument_index_counter += 1;
let p = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "position",
argument_index: argument_index_counter,
})?;
let p = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
position = Some(
p.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "SongPosition",
raw_input: p.to_string(),
})?,
.map_err(|_| RequestParserError::SyntaxError(0, p.to_string()))?,
);
}
Self::throw_if_too_many_arguments(parts)?;
debug_assert!(parts.next().is_none());
Ok(FindAddRequest {
filter,
sort,
window,
position,
})
Ok((Request::FindAdd(filter, sort, window, position), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
}
empty_command_response!(FindAdd);
impl Command for FindAdd {
type Request = FindAddRequest;
type Response = FindAddResponse;
}
+21 -22
View File
@@ -1,39 +1,38 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandResponse, ResponseParserError, single_item_command_request},
response_tokenizer::{ResponseAttributes, get_and_parse_property},
types::Uri,
use crate::commands::{
get_and_parse_property, Command, Request, RequestParserError, RequestParserResult,
ResponseAttributes, ResponseParserError,
};
pub struct GetFingerprint;
single_item_command_request!(GetFingerprint, "getfingerprint", Uri);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GetFingerprintResponse {
pub chromaprint: String,
}
impl GetFingerprintResponse {
pub fn new(chromaprint: String) -> Self {
Self { chromaprint }
}
}
impl Command for GetFingerprint {
type Response = GetFingerprintResponse;
const COMMAND: &'static str = "getfingerprint";
impl CommandResponse for GetFingerprintResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
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), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
let chromaprint = get_and_parse_property!(parts, "chromaprint", Text);
Ok(GetFingerprintResponse { chromaprint })
}
}
impl Command for GetFingerprint {
type Request = GetFingerprintRequest;
type Response = GetFingerprintResponse;
}
+54 -158
View File
@@ -1,167 +1,63 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, expect_property_type},
types::{GroupType, TagName, WindowRange},
commands::{
Command, GenericResponseValue, Request, RequestParserError, RequestParserResult,
ResponseAttributes, ResponseParserError,
},
filter::parse_filter,
};
pub struct List;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ListRequest {
tagname: TagName,
filter: Option<Filter>,
groups: Vec<GroupType>,
window: Option<WindowRange>,
}
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 serialize(&self) -> String {
let mut cmd = match &self.filter {
Some(f) => format!("{} {} {}", Self::COMMAND, self.tagname, f),
None => format!("{} {}", Self::COMMAND, self.tagname),
};
for group in &self.groups {
cmd.push_str(&format!(" group {}", group));
}
if let Some(window) = &self.window {
cmd.push_str(&format!(" window {}", window));
}
cmd.push('\n');
cmd
}
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_.first().map(|(k, _)| k);
parts_.iter().all(|(k, _)| k == key.unwrap())
});
let list = parts_
.into_iter()
.map(|(k, v)| Ok(expect_property_type!(Some(v), k, Text).to_string()))
.collect::<Result<Vec<_>, ResponseParserError>>()?;
Ok(ListResponse(list))
}
}
pub type ListResponse = Vec<String>;
impl Command for List {
type Request = ListRequest;
type Response = ListResponse;
const COMMAND: &'static str = "list";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let tagtype = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
let tagtype = tagtype
.parse()
.map_err(|_| RequestParserError::SyntaxError(1, "tagtype".to_owned()))?;
// 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
};
debug_assert!(parts.next().is_none());
Ok((Request::List(tagtype, filter, group), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!({
let key = parts.0.first().map(|(k, _)| k);
parts.0.iter().all(|(k, _)| k == key.unwrap())
});
let list = parts
.0
.iter()
.map(|(_, v)| match v {
GenericResponseValue::Text(value) => Ok(value.to_string()),
GenericResponseValue::Binary(_) => Err(
ResponseParserError::UnexpectedPropertyType("handler", "Binary"),
),
})
.collect::<Result<Vec<_>, ResponseParserError>>()?;
Ok(list)
}
}
+20 -147
View File
@@ -1,161 +1,34 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::ResponseAttributes,
types::{DbSelectionPrintResponse, Uri},
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
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
#[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))
}
}
pub type ListAllResponse = Vec<String>;
impl Command for ListAll {
type Request = ListAllRequest;
type Response = ListAllResponse;
}
const COMMAND: &'static str = "listall";
#[cfg(test)]
mod tests {
use std::path::PathBuf;
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = parts
.next()
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, "uri".to_owned()))
})
.transpose()?;
use crate::types::{DbDirectoryInfo, DbPlaylistInfo, DbSongInfo};
debug_assert!(parts.next().is_none());
use super::*;
Ok((Request::ListAll(uri), ""))
}
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());
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
})
]))
)
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
}
+21 -104
View File
@@ -1,118 +1,35 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::ResponseAttributes,
types::{DbSelectionPrintResponse, Uri},
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
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
#[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))
}
}
// in addition to the metadata of each entry
pub type ListAllInfoResponse = Vec<String>;
impl Command for ListAllInfo {
type Request = ListAllInfoRequest;
type Response = ListAllInfoResponse;
}
const COMMAND: &'static str = "listallinfo";
#[cfg(test)]
mod tests {
use std::path::PathBuf;
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = parts
.next()
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, "uri".to_owned()))
})
.transpose()?;
use crate::types::{DbDirectoryInfo, DbPlaylistInfo, DbSongInfo, Tag};
debug_assert!(parts.next().is_none());
use super::*;
Ok((Request::ListAllInfo(uri), ""))
}
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());
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())
})
])),
)
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
}
+26 -37
View File
@@ -1,45 +1,34 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::ResponseAttributes,
types::{DbDirectoryInfo, DbSelectionPrintResponse, Uri},
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct ListFiles;
single_optional_item_command_request!(ListFiles, "listfiles", Uri);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ListFilesResponse(Vec<DbDirectoryInfo>);
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)
}
}
// TODO: fix this type
pub type ListFilesResponse = Vec<String>;
impl Command for ListFiles {
type Request = ListFilesRequest;
type Response = ListFilesResponse;
const COMMAND: &'static str = "listfiles";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = parts
.next()
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, "uri".to_owned()))
})
.transpose()?;
debug_assert!(parts.next().is_none());
Ok((Request::ListFiles(uri), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
}
+21 -80
View File
@@ -1,93 +1,34 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::ResponseAttributes,
types::{DbSelectionPrintResponse, Uri},
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct LsInfo;
single_optional_item_command_request!(LsInfo, "lsinfo", Uri);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LsInfoResponse(Vec<DbSelectionPrintResponse>);
impl LsInfoResponse {
pub fn new(items: Vec<DbSelectionPrintResponse>) -> Self {
LsInfoResponse(items)
}
}
impl CommandResponse for LsInfoResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let result = DbSelectionPrintResponse::parse(parts)?;
Ok(LsInfoResponse(result))
}
}
// TODO: fix this type
pub type LsInfoResponse = Vec<String>;
impl Command for LsInfo {
type Request = LsInfoRequest;
type Response = LsInfoResponse;
}
const COMMAND: &'static str = "lsinfo";
#[cfg(test)]
mod tests {
use std::path::PathBuf;
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = parts
.next()
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, "uri".to_owned()))
})
.transpose()?;
use crate::types::{DbDirectoryInfo, DbPlaylistInfo};
debug_assert!(parts.next().is_none());
use super::*;
Ok((Request::LsInfo(uri), ""))
}
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())
})
])),
);
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
}
+22 -25
View File
@@ -1,45 +1,42 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandResponse, ResponseParserError, single_item_command_request},
response_tokenizer::{GenericResponseValue, ResponseAttributes},
types::Uri,
use crate::commands::{
Command, GenericResponseValue, Request, RequestParserError, RequestParserResult,
ResponseAttributes, ResponseParserError,
};
pub struct ReadComments;
single_item_command_request!(ReadComments, "readcomments", Uri);
pub type ReadCommentsResponse = HashMap<String, String>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReadCommentsResponse(HashMap<String, String>);
impl Command for ReadComments {
type Response = ReadCommentsResponse;
const COMMAND: &'static str = "readcomments";
impl ReadCommentsResponse {
pub fn new(comments: HashMap<String, String>) -> Self {
ReadCommentsResponse(comments)
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 CommandResponse for ReadCommentsResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
let comments = parts
.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()))
}
GenericResponseValue::Binary(_) => Err(ResponseParserError::SyntaxError(1, k)),
})
.collect::<Result<HashMap<_, _>, ResponseParserError>>()?;
Ok(ReadCommentsResponse(comments))
Ok(comments)
}
}
impl Command for ReadComments {
type Request = ReadCommentsRequest;
type Response = ReadCommentsResponse;
}
+33 -77
View File
@@ -1,90 +1,51 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
request_tokenizer::RequestTokenizer,
response_tokenizer::{
ResponseAttributes, get_and_parse_property, get_optional_property, get_property,
commands::{
get_and_parse_property, get_optional_property, get_property, Command, Request,
RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError,
},
types::{Offset, Uri},
common::Offset,
};
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 ReadPictureResponse {
pub fn new(size: usize, binary: Vec<u8>, mimetype: Option<String>) -> Self {
Self {
size,
binary,
mimetype,
}
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), ""))
}
}
impl CommandResponse for ReadPictureResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
// TODO: is empty response possible?
// if parts.is_empty() {
// return Err(ResponseParserError::UnexpectedEOF);
// }
if parts.is_empty() {
return Ok(None);
}
let size = get_and_parse_property!(parts, "size", Text);
@@ -92,15 +53,10 @@ impl CommandResponse for ReadPictureResponse {
let mimetype = get_optional_property!(parts, "mimetype", Text).map(|s| s.to_string());
Ok(ReadPictureResponse {
Ok(Some(ReadPictureResponse {
size,
binary,
mimetype,
})
}))
}
}
impl Command for ReadPicture {
type Request = ReadPictureRequest;
type Response = ReadPictureResponse;
}
+18 -24
View File
@@ -1,41 +1,35 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::{ResponseAttributes, get_and_parse_property},
types::Uri,
use crate::commands::{
get_and_parse_property, Command, Request, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
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 RescanResponse {
pub fn new(updating_db: usize) -> Self {
RescanResponse { updating_db }
}
}
impl Command for Rescan {
type Response = RescanResponse;
const COMMAND: &'static str = "rescan";
impl CommandResponse for RescanResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
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), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
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;
}
+24 -109
View File
@@ -1,136 +1,51 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::ResponseAttributes,
types::{DbSelectionPrintResponse, DbSongInfo, Sort, WindowRange},
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
};
pub struct Search;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
}
pub struct SearchResponse {}
impl SearchRequest {
pub fn new(filter: Filter, sort: Option<Sort>, window: Option<WindowRange>) -> Self {
Self {
filter,
sort,
window,
}
}
}
impl CommandRequest for SearchRequest {
impl Command for Search {
type Response = SearchResponse;
const COMMAND: &'static str = "search";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(3);
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_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
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(
s.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "Sort",
raw_input: s.to_string(),
})?,
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
);
sort_or_window = parts.next();
}
let mut window = None;
if let Some("window") = sort_or_window {
argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
window = Some(
w.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?,
);
}
Self::throw_if_too_many_arguments(parts)?;
debug_assert!(parts.next().is_none());
Ok(SearchRequest {
filter,
sort,
window,
})
Ok((Request::Search(filter, sort, window), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
}
#[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;
}
+26 -106
View File
@@ -1,141 +1,61 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
filter::Filter,
request_tokenizer::RequestTokenizer,
types::{SongPosition, Sort, WindowRange},
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
};
pub struct SearchAdd;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchAddRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
}
pub struct SearchAddResponse {}
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 {
impl Command for SearchAdd {
type Response = SearchAddResponse;
const COMMAND: &'static str = "searchadd";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(4);
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_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
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(
s.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "Sort",
raw_input: s.to_string(),
})?,
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
);
sort_or_window_or_position = parts.next();
}
let mut window = None;
if let Some("window") = sort_or_window_or_position {
argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
window = Some(
w.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?,
);
sort_or_window_or_position = parts.next();
}
let mut position = None;
if let Some("position") = sort_or_window_or_position {
argument_index_counter += 1;
let p = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "position",
argument_index: argument_index_counter,
})?;
let p = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
position = Some(
p.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "SongPosition",
raw_input: p.to_string(),
})?,
.map_err(|_| RequestParserError::SyntaxError(0, p.to_string()))?,
);
}
Self::throw_if_too_many_arguments(parts)?;
debug_assert!(parts.next().is_none());
Ok(SearchAddRequest {
filter,
sort,
window,
position,
})
Ok((Request::SearchAdd(filter, sort, window, position), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
}
empty_command_response!(SearchAdd);
impl Command for SearchAdd {
type Request = SearchAddRequest;
type Response = SearchAddResponse;
}
+30 -111
View File
@@ -1,150 +1,69 @@
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
filter::Filter,
request_tokenizer::RequestTokenizer,
types::{PlaylistName, SongPosition, Sort, WindowRange},
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
filter::parse_filter,
};
pub struct SearchAddPl;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchAddPlRequest {
playlist_name: PlaylistName,
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
}
pub struct SearchAddPlResponse {}
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 {
impl Command for SearchAddPl {
type Response = SearchAddPlResponse;
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> {
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let playlist_name = parts
.next()
.ok_or(Self::missing_arguments_error(0))?
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
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 filter = parse_filter(&mut parts)?;
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(
s.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "Sort",
raw_input: s.to_string(),
})?,
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string(),
);
sort_or_window_or_position = parts.next();
}
let mut window = None;
if let Some("window") = sort_or_window_or_position {
argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
window = Some(
w.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?,
);
sort_or_window_or_position = parts.next();
}
let mut position = None;
if let Some("position") = sort_or_window_or_position {
argument_index_counter += 1;
let p = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "position",
argument_index: argument_index_counter,
})?;
let p = parts.next().ok_or(RequestParserError::UnexpectedEOF)?;
position = Some(
p.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "SongPosition",
raw_input: p.to_string(),
})?,
.map_err(|_| RequestParserError::SyntaxError(0, p.to_string()))?,
);
}
Self::throw_if_too_many_arguments(parts)?;
debug_assert!(parts.next().is_none());
Ok(SearchAddPlRequest {
playlist_name,
filter,
sort,
window,
position,
})
Ok((
Request::SearchAddPl(playlist_name, filter, sort, window, position),
"",
))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
}
}
empty_command_response!(SearchAddPl);
impl Command for SearchAddPl {
type Request = SearchAddPlRequest;
type Response = SearchAddPlResponse;
}
+22 -71
View File
@@ -1,92 +1,48 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
filter::Filter,
request_tokenizer::RequestTokenizer,
response_tokenizer::{ResponseAttributes, get_and_parse_property},
types::{GroupType, Seconds},
commands::{
get_and_parse_property, Command, Request, RequestParserError, RequestParserResult,
ResponseAttributes, ResponseParserError,
},
filter::parse_filter,
};
pub struct SearchCount;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchCountRequest {
filter: Filter,
group: Option<GroupType>,
pub struct SearchCountResponse {
pub songs: usize,
pub playtime: u64,
}
impl SearchCountRequest {
pub fn new(filter: Filter, group: Option<GroupType>) -> Self {
Self { filter, group }
}
}
impl CommandRequest for SearchCountRequest {
impl Command for SearchCount {
type Response = SearchCountResponse;
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)),
};
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let filter = parse_filter(&mut parts)?;
let group = if let Some("group") = parts.next() {
let group = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "group",
argument_index: 1,
})?;
Some(
group
parts
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 1,
expected_type: "Group",
raw_input: group.to_string(),
})?,
.map_err(|_| RequestParserError::SyntaxError(1, "group".to_owned()))?,
)
} else {
None
};
Self::throw_if_too_many_arguments(parts)?;
debug_assert!(parts.next().is_none());
Ok(SearchCountRequest { filter, group })
Ok((Request::SearchCount(filter, group), ""))
}
}
#[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()?;
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
let songs = get_and_parse_property!(parts, "songs", Text);
let playtime = get_and_parse_property!(parts, "playtime", Text);
@@ -94,8 +50,3 @@ impl CommandResponse for SearchCountResponse {
Ok(SearchCountResponse { songs, playtime })
}
}
impl Command for SearchCount {
type Request = SearchCountRequest;
type Response = SearchCountResponse;
}
+18 -24
View File
@@ -1,41 +1,35 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::{ResponseAttributes, get_and_parse_property},
types::Uri,
use crate::commands::{
get_and_parse_property, Command, Request, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct Update;
single_optional_item_command_request!(Update, "update", Uri);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UpdateResponse {
updating_db: usize,
}
impl UpdateResponse {
pub fn new(updating_db: usize) -> Self {
UpdateResponse { updating_db }
}
}
impl Command for Update {
type Response = UpdateResponse;
const COMMAND: &'static str = "update";
impl CommandResponse for UpdateResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
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), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
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 @@
mod delpartition;
mod listpartitions;
mod moveoutput;
mod newpartition;
mod partition;
pub mod delpartition;
pub mod listpartitions;
pub mod moveoutput;
pub mod newpartition;
pub mod partition;
pub use delpartition::*;
pub use listpartitions::*;
pub use moveoutput::*;
pub use newpartition::*;
pub use partition::*;
pub use delpartition::DelPartition;
pub use listpartitions::ListPartitions;
pub use moveoutput::MoveOutput;
pub use newpartition::NewPartition;
pub use partition::Partition;
@@ -1,15 +1,29 @@
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::PartitionName,
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct DelPartition;
single_item_command_request!(DelPartition, "delpartition", PartitionName);
empty_command_response!(DelPartition);
impl Command for DelPartition {
type Request = DelPartitionRequest;
type Response = DelPartitionResponse;
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(())
}
}
@@ -1,16 +1,43 @@
use crate::{
commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
response_tokenizer::expect_property_type,
types::PartitionName,
use crate::commands::{
Command, GenericResponseValue, Request, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct ListPartitions;
empty_command_request!(ListPartitions, "listpartitions");
multi_item_command_response!(ListPartitions, "partition", PartitionName);
pub type ListPartitionsResponse = Vec<String>;
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 = match value {
GenericResponseValue::Text(name) => name.to_string(),
GenericResponseValue::Binary(_) => {
return Err(ResponseParserError::UnexpectedPropertyType(
"partition",
"Binary",
))
}
};
partitions.push(partition);
}
Ok(partitions)
}
}
+24 -7
View File
@@ -1,12 +1,29 @@
use crate::commands::{Command, empty_command_response, single_item_command_request};
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct MoveOutput;
single_item_command_request!(MoveOutput, "moveoutput", String);
empty_command_response!(MoveOutput);
impl Command for MoveOutput {
type Request = MoveOutputRequest;
type Response = MoveOutputResponse;
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(())
}
}
@@ -1,15 +1,29 @@
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::PartitionName,
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct NewPartition;
single_item_command_request!(NewPartition, "newpartition", PartitionName);
empty_command_response!(NewPartition);
impl Command for NewPartition {
type Request = NewPartitionRequest;
type Response = NewPartitionResponse;
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(())
}
}
+23 -9
View File
@@ -1,15 +1,29 @@
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::PartitionName,
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct Partition;
single_item_command_request!(Partition, "partition", PartitionName);
empty_command_response!(Partition);
impl Command for Partition {
type Request = PartitionRequest;
type Response = PartitionResponse;
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(())
}
}
+24 -24
View File
@@ -1,25 +1,25 @@
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 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;
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::*;
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;
+26 -9
View File
@@ -1,15 +1,32 @@
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::BoolOrOneshot,
use std::str::FromStr;
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct Consume;
single_item_command_request!(Consume, "consume", BoolOrOneshot);
empty_command_response!(Consume);
impl Command for Consume {
type Request = ConsumeRequest;
type Response = ConsumeResponse;
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(())
}
}
+27 -8
View File
@@ -1,15 +1,34 @@
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::Seconds,
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::Seconds,
};
pub struct Crossfade;
single_item_command_request!(Crossfade, "crossfade", Seconds);
empty_command_response!(Crossfade);
impl Command for Crossfade {
type Request = CrossfadeRequest;
type Response = CrossfadeResponse;
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(())
}
}
+22 -8
View File
@@ -1,15 +1,29 @@
use crate::{
commands::{Command, empty_command_request, single_item_command_response},
types::VolumeValue,
commands::{Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError},
request::VolumeValue,
};
pub struct GetVol;
empty_command_request!(GetVol, "getvol");
single_item_command_response!(GetVol, "volume", VolumeValue);
impl Command for GetVol {
type Request = GetVolRequest;
type Response = GetVolResponse;
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> {
unimplemented!()
// let volume = get_property!(parts, Volume, "volume");
// let volume = match parts.get("volume") {
// Some(GenericResponseValue::Volume(v)) => *v,
// _ => return Err(ResponseParserError::MissingField("volume")),
// };
// Ok(volume)
}
}
+26 -7
View File
@@ -1,12 +1,31 @@
use crate::commands::{Command, empty_command_response, single_item_command_request};
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct MixRampDb;
single_item_command_request!(MixRampDb, "mixrampdb", f32);
empty_command_response!(MixRampDb);
impl Command for MixRampDb {
type Request = MixRampDbRequest;
type Response = MixRampDbResponse;
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(())
}
}
+27 -8
View File
@@ -1,15 +1,34 @@
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::Seconds,
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
common::Seconds,
};
pub struct MixRampDelay;
single_item_command_request!(MixRampDelay, "mixrampdelay", Seconds);
empty_command_response!(MixRampDelay);
impl Command for MixRampDelay {
type Request = MixRampDelayRequest;
type Response = MixRampDelayResponse;
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(())
}
}
+17 -37
View File
@@ -1,51 +1,31 @@
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct Random;
pub struct RandomRequest(bool);
impl RandomRequest {
pub fn new(state: bool) -> Self {
Self(state)
}
}
impl CommandRequest for RandomRequest {
impl Command for Random {
type Response = ();
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> {
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let state = match parts.next() {
Some("0") => false,
Some("1") => true,
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)),
Some(s) => return Err(RequestParserError::SyntaxError(0, s.to_owned())),
None => return Err(RequestParserError::UnexpectedEOF),
};
Self::throw_if_too_many_arguments(parts)?;
debug_assert!(parts.next().is_none());
Ok(RandomRequest(state))
Ok((Request::Random(state), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
}
empty_command_response!(Random);
impl Command for Random {
type Request = RandomRequest;
type Response = RandomResponse;
}
+17 -37
View File
@@ -1,51 +1,31 @@
use crate::{
commands::{Command, CommandRequest, RequestParserError, empty_command_response},
request_tokenizer::RequestTokenizer,
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct Repeat;
pub struct RepeatRequest(bool);
impl RepeatRequest {
pub fn new(state: bool) -> Self {
RepeatRequest(state)
}
}
impl CommandRequest for RepeatRequest {
impl Command for Repeat {
type Response = ();
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> {
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let state = match parts.next() {
Some("0") => false,
Some("1") => true,
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)),
Some(s) => return Err(RequestParserError::SyntaxError(0, s.to_owned())),
None => return Err(RequestParserError::UnexpectedEOF),
};
Self::throw_if_too_many_arguments(parts)?;
debug_assert!(parts.next().is_none());
Ok(RepeatRequest(state))
Ok((Request::Repeat(state), ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
}
}
empty_command_response!(Repeat);
impl Command for Repeat {
type Request = RepeatRequest;
type Response = RepeatResponse;
}
@@ -1,15 +1,35 @@
use std::str::FromStr;
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::ReplayGainModeMode,
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
request::ReplayGainModeMode,
};
pub struct ReplayGainMode;
single_item_command_request!(ReplayGainMode, "replay_gain_mode", ReplayGainModeMode);
empty_command_response!(ReplayGainMode);
impl Command for ReplayGainMode {
type Request = ReplayGainModeRequest;
type Response = ReplayGainModeResponse;
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(())
}
}
@@ -3,43 +3,39 @@ use std::{collections::HashMap, str::FromStr};
use serde::{Deserialize, Serialize};
use crate::{
commands::{Command, CommandResponse, ResponseParserError, empty_command_request},
response_tokenizer::{ResponseAttributes, get_property},
types::ReplayGainModeMode,
commands::{
get_property, Command, Request, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
request::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 ReplayGainStatusResponse {
pub fn new(replay_gain_mode: ReplayGainModeMode) -> Self {
Self { replay_gain_mode }
}
}
impl Command for ReplayGainStatus {
type Response = ReplayGainStatusResponse;
const COMMAND: &'static str = "replay_gain_status";
impl CommandResponse for ReplayGainStatusResponse {
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
debug_assert!(parts.next().is_none());
Ok((Request::ReplayGainStatus, ""))
}
fn parse_response(
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
let parts: HashMap<_, _> = parts.into();
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".to_string(),
replay_gain_mode.to_string(),
)
ResponseParserError::InvalidProperty("replay_gain_mode", replay_gain_mode)
})?,
})
}
}
impl Command for ReplayGainStatus {
type Request = ReplayGainStatusRequest;
type Response = ReplayGainStatusResponse;
}
+28 -8
View File
@@ -1,15 +1,35 @@
use std::str::FromStr;
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::VolumeValue,
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
request::VolumeValue,
};
pub struct SetVol;
single_item_command_request!(SetVol, "setvol", VolumeValue);
empty_command_response!(SetVol);
impl Command for SetVol {
type Request = SetVolRequest;
type Response = SetVolResponse;
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(())
}
}
+26 -9
View File
@@ -1,15 +1,32 @@
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::BoolOrOneshot,
use std::str::FromStr;
use crate::commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct Single;
single_item_command_request!(Single, "single", BoolOrOneshot);
empty_command_response!(Single);
impl Command for Single {
type Request = SingleRequest;
type Response = SingleResponse;
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(())
}
}
+28 -8
View File
@@ -1,15 +1,35 @@
use std::str::FromStr;
use crate::{
commands::{Command, empty_command_response, single_item_command_request},
types::VolumeValue,
commands::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
},
request::VolumeValue,
};
pub struct Volume;
single_item_command_request!(Volume, "volume", VolumeValue);
empty_command_response!(Volume);
impl Command for Volume {
type Request = VolumeRequest;
type Response = VolumeResponse;
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(())
}
}
+10 -10
View File
@@ -1,11 +1,11 @@
mod clearerror;
mod currentsong;
mod idle;
mod stats;
mod status;
pub mod clearerror;
pub mod currentsong;
pub mod idle;
pub mod stats;
pub mod status;
pub use clearerror::*;
pub use currentsong::*;
pub use idle::*;
pub use stats::*;
pub use status::*;
pub use clearerror::ClearError;
pub use currentsong::CurrentSong;
pub use idle::Idle;
pub use stats::Stats;
pub use status::Status;
+18 -7
View File
@@ -1,13 +1,24 @@
use crate::commands::{Command, empty_command_request, empty_command_response};
use crate::commands::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
/// 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 Request = ClearErrorRequest;
type Response = ClearErrorResponse;
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(())
}
}

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