94 Commits

Author SHA1 Message Date
291aa6877c WIP 2025-12-08 18:43:44 +09:00
c48a8adcdc .gitea/workflows: run on debian-latest-slim
Some checks failed
Build and test / build (push) Failing after 19s
Build and test / check (push) Failing after 19s
Build and test / test (push) Failing after 23s
Build and test / docs (push) Failing after 15s
2025-12-08 18:43:29 +09:00
1f1e7fbb53 .gitea/workflows: update actions/checkout: v4 -> v6 2025-12-08 18:43:24 +09:00
7ec268094d commands: store SubtypeParserError expected type as &str
All checks were successful
Build and test / check (push) Successful in 58s
Build and test / build (push) Successful in 1m9s
Build and test / docs (push) Successful in 1m11s
Build and test / test (push) Successful in 2m26s
2025-12-08 17:48:44 +09:00
afa690366f commands: use new error variants for a few more commands
All checks were successful
Build and test / check (push) Successful in 1m2s
Build and test / build (push) Successful in 1m9s
Build and test / docs (push) Successful in 1m25s
Build and test / test (push) Successful in 1m38s
2025-12-08 17:40:07 +09:00
02ba7f2684 lib: fix doccomment url rendering
All checks were successful
Build and test / build (push) Successful in 1m4s
Build and test / check (push) Successful in 1m23s
Build and test / test (push) Successful in 1m52s
Build and test / docs (push) Successful in 1m16s
2025-12-08 16:27:55 +09:00
44903f2ce0 commands: add request parser error variant for keyword without value
Some checks failed
Build and test / check (push) Successful in 1m5s
Build and test / build (push) Successful in 1m6s
Build and test / docs (push) Has been cancelled
Build and test / test (push) Has been cancelled
2025-12-08 16:25:43 +09:00
9f50d61ad5 commands: clearly define how arguments are counted in request error 2025-12-08 16:25:40 +09:00
788a01ce83 Clean up misc. module doccomments
All checks were successful
Build and test / build (push) Successful in 1m10s
Build and test / check (push) Successful in 1m0s
Build and test / test (push) Successful in 2m15s
Build and test / docs (push) Successful in 1m19s
2025-12-08 16:16:34 +09:00
f5451b6c2f filter: document misc
All checks were successful
Build and test / check (push) Successful in 57s
Build and test / build (push) Successful in 1m10s
Build and test / docs (push) Successful in 1m13s
Build and test / test (push) Successful in 2m23s
2025-12-08 16:09:55 +09:00
2de18cdbdb .gitea/workflows: don't error on clippy warnings
All checks were successful
Build and test / check (push) Successful in 1m4s
Build and test / build (push) Successful in 1m29s
Build and test / test (push) Successful in 1m48s
Build and test / docs (push) Successful in 2m13s
2025-12-08 15:59:10 +09:00
23bfd0c4a7 .gitea/workflows: don't run with all features
Some checks failed
Build and test / build (push) Successful in 1m6s
Build and test / check (push) Failing after 1m23s
Build and test / test (push) Successful in 1m54s
Build and test / docs (push) Successful in 1m23s
2025-12-08 15:57:40 +09:00
3342293bf2 commands: use new error variants for various commands
Some checks failed
Build and test / build (push) Failing after 1m1s
Build and test / check (push) Failing after 1m25s
Build and test / test (push) Failing after 1m25s
Build and test / docs (push) Failing after 1m0s
2025-12-08 15:54:37 +09:00
2b3ab7389d Preallocate a few more response parsers
Some checks failed
Build and test / check (push) Failing after 58s
Build and test / build (push) Failing after 1m23s
Build and test / test (push) Failing after 1m28s
Build and test / docs (push) Failing after 1m1s
2025-12-08 14:27:42 +09:00
f80d36e962 commands: add and fix variants for RequestParserError
Some checks failed
Build and test / build (push) Failing after 1m0s
Build and test / check (push) Failing after 1m21s
Build and test / test (push) Failing after 1m18s
Build and test / docs (push) Failing after 1m48s
2025-12-08 14:02:39 +09:00
86edd4c5b3 filter: flatten module
Some checks failed
Build and test / check (push) Failing after 1m2s
Build and test / test (push) Failing after 1m17s
Build and test / build (push) Failing after 1m23s
Build and test / docs (push) Failing after 1m9s
2025-12-08 13:32:18 +09:00
a7a8ceedeb cargo fmt + clippy
Some checks failed
Build and test / build (push) Failing after 1m5s
Build and test / check (push) Failing after 1m17s
Build and test / test (push) Failing after 1m21s
Build and test / docs (push) Failing after 1m13s
2025-12-08 13:28:45 +09:00
7ce0d68021 commands: extend parser errors 2025-12-08 13:28:17 +09:00
b5cb4677ee Remove a few unused type aliases
Some checks failed
Build and test / check (push) Failing after 59s
Build and test / build (push) Failing after 1m1s
Build and test / test (push) Failing after 1m26s
Build and test / docs (push) Failing after 1m49s
2025-12-08 13:11:09 +09:00
38faf99de7 *_tokenizer: add module doccomment 2025-12-08 13:10:31 +09:00
69f252bad8 commands: force external users to interact with requests and responses through Command trait
Some checks failed
Build and test / check (push) Failing after 42s
Build and test / build (push) Failing after 1m0s
Build and test / docs (push) Failing after 1m7s
Build and test / test (push) Failing after 1m45s
2025-12-08 13:07:11 +09:00
c177c089a3 commands: add command executor directly on Command trait
Some checks failed
Build and test / check (push) Failing after 41s
Build and test / build (push) Failing after 59s
Build and test / test (push) Has been cancelled
Build and test / docs (push) Has been cancelled
2025-12-08 13:05:30 +09:00
d964e7857b examples/mpd-client: add some code
Some checks failed
Build and test / docs (push) Has been cancelled
Build and test / test (push) Has been cancelled
Build and test / build (push) Has been cancelled
Build and test / check (push) Has been cancelled
2025-12-08 13:00:46 +09:00
8430592bee commands: strip lifetimes 2025-12-08 12:30:19 +09:00
78cfc09d60 commands: add newline at end of all command serializers 2025-12-08 12:13:05 +09:00
a7b764ad0f client: init
Some checks failed
Build and test / check (push) Failing after 59s
Build and test / build (push) Failing after 1m7s
Build and test / test (push) Failing after 1m50s
Build and test / docs (push) Failing after 1m5s
2025-12-08 05:32:49 +09:00
f3e6fe13df MpdError: impl thiserror 2025-12-08 05:31:43 +09:00
4f8f5db620 make ResponseParserError self-contained, impl thiserror 2025-12-08 05:31:13 +09:00
1e39640508 common/types: flatten to types
Some checks failed
Build and test / check (push) Failing after 47s
Build and test / build (push) Failing after 1m3s
Build and test / docs (push) Failing after 1m1s
Build and test / test (push) Failing after 1m48s
2025-12-08 04:14:32 +09:00
b388bc727b commands: document module 2025-12-08 04:07:20 +09:00
9f859e1df1 response: add some notes about the errors
Some checks failed
Build and test / build (push) Successful in 1m7s
Build and test / check (push) Failing after 1m8s
Build and test / docs (push) Successful in 1m11s
Build and test / test (push) Successful in 2m20s
2025-12-08 01:46:14 +09:00
4bb5702eba cargo clippy
Some checks failed
Build and test / build (push) Successful in 1m3s
Build and test / check (push) Failing after 1m24s
Build and test / test (push) Successful in 1m50s
Build and test / docs (push) Successful in 1m14s
2025-12-08 01:08:58 +09:00
955fdbcff1 commands/stickernamestypes: add missing derive for response 2025-12-08 01:05:24 +09:00
322c8c8fc3 commands/listplaylist: add note about format
Some checks failed
Build and test / build (push) Successful in 1m10s
Build and test / check (push) Failing after 1m25s
Build and test / test (push) Successful in 1m36s
Build and test / docs (push) Successful in 1m17s
2025-12-08 01:03:23 +09:00
93afaf1bde commands/seekcur: fix parser 2025-12-08 01:03:11 +09:00
c915c67f08 commands: fix request enum conversion for unmount 2025-12-08 00:52:27 +09:00
813ffac614 flake.lock: bump inputs
Some checks failed
Build and test / check (push) Failing after 58s
Build and test / build (push) Successful in 1m29s
Build and test / test (push) Successful in 1m49s
Build and test / docs (push) Successful in 1m12s
2025-12-08 00:45:53 +09:00
b0bc2752cc commands: add response types for multiple commands
Some checks failed
Build and test / build (push) Successful in 1m3s
Build and test / test (push) Has been cancelled
Build and test / docs (push) Has been cancelled
Build and test / check (push) Has been cancelled
2025-12-08 00:44:37 +09:00
07e9161137 commands/currentsong: implement response parser
Some checks failed
Build and test / build (push) Successful in 1m6s
Build and test / check (push) Failing after 1m23s
Build and test / test (push) Successful in 1m38s
Build and test / docs (push) Successful in 2m4s
2025-12-08 00:29:00 +09:00
f2977f1ba9 commands/listplaylistinfo: fix response type 2025-12-08 00:28:38 +09:00
b0b4134829 commands: split Command trait into req + res parts
Some checks failed
Build and test / check (push) Failing after 1m2s
Build and test / build (push) Successful in 1m8s
Build and test / test (push) Successful in 2m23s
Build and test / docs (push) Successful in 1m24s
2025-12-08 00:07:37 +09:00
c1dbdd4644 cargo fmt
Some checks failed
Build and test / check (push) Failing after 58s
Build and test / build (push) Successful in 1m6s
Build and test / docs (push) Successful in 1m10s
Build and test / test (push) Successful in 2m19s
2025-12-07 21:40:50 +09:00
64c94d6e89 commands/listplaylistinfo: add response type
Some checks failed
Build and test / check (push) Failing after 44s
Build and test / build (push) Successful in 1m0s
Build and test / docs (push) Successful in 1m25s
Build and test / test (push) Successful in 3m0s
2025-12-07 21:33:01 +09:00
4d11df5ad1 commands: implement all database selection responses 2025-12-07 21:24:49 +09:00
0cacbe2229 common/types: add db selection print types 2025-12-07 21:22:35 +09:00
7bbe1c4ced types/tag: make orderable 2025-12-07 21:22:34 +09:00
83918fd432 commands/listplaylists: implement 2025-12-07 21:22:33 +09:00
07e1c76aa9 commands: parse to Self::Request
Some checks failed
Build and test / check (push) Failing after 43s
Build and test / build (push) Successful in 1m2s
Build and test / docs (push) Successful in 1m12s
Build and test / test (push) Successful in 1m44s
2025-12-05 22:00:11 +09:00
d84e653db2 filter: export all types from inner module 2025-12-05 21:00:01 +09:00
7834bbd956 common: don't expose types directly 2025-12-05 20:59:19 +09:00
249ffb2a36 commands: add docs for Command trait
Some checks failed
Build and test / check (push) Failing after 58s
Build and test / build (push) Successful in 1m10s
Build and test / docs (push) Successful in 1m10s
Build and test / test (push) Successful in 2m53s
2025-11-25 05:28:58 +09:00
fdd4880d05 common/types: add better alias for MountPath 2025-11-25 05:28:19 +09:00
9ca6544057 common/types: add better alias for AudioOutputId 2025-11-25 05:28:19 +09:00
e5f70ca87a common/types: add type for ChannelName 2025-11-25 05:28:19 +09:00
b03f60c985 common/types: add better alias for PlaylistVersion 2025-11-25 05:28:19 +09:00
b22016e970 common/types: add type for Sort 2025-11-25 03:56:01 +09:00
65c7798d01 response_tokenizer: rewrite
This commit contains a rewrite of the response tokenizer, which
introduces lazy parsing of the response, handling of binary data, some
tests, as well as just generally more robustness against errors.
2025-11-24 23:53:03 +09:00
5188809327 commands: fix request de/serialization for list 2025-11-24 22:00:42 +09:00
a4276a2caa commands: remove some fixed TODOs 2025-11-24 21:59:42 +09:00
7b27a650a1 commands: split response tokenizer into separate file 2025-11-24 19:28:28 +09:00
062dbcafb8 filter: implement basic parser 2025-11-24 19:25:01 +09:00
57a6b0a3ee common/types: case insensitive tags 2025-11-24 19:25:00 +09:00
b5fbaadca2 Implement a proper request tokenizer 2025-11-24 19:25:00 +09:00
3bd7aaaad2 WIP: serialize requests 2025-11-21 18:50:58 +09:00
8aba3d8859 filter: implement fmt::Display 2025-11-21 18:50:58 +09:00
e4ece7d6b2 common/types: implement serialization helpers for Tag 2025-11-21 18:50:58 +09:00
f07f90aee5 common/types: implement fmt::Display 2025-11-21 18:50:58 +09:00
6e2aa2ba65 Cargo.toml: bump deps 2025-11-21 16:15:16 +09:00
1ef8ef669a flake.lock: bump 2025-11-21 16:14:13 +09:00
c6a123a6e1 commands: implement response parser for lsinfo 2025-11-21 16:13:09 +09:00
06e24f0ce0 cargo fmt + clippy 2025-11-21 16:04:46 +09:00
da31ab75e2 filter: add unit tests 2025-11-21 15:49:27 +09:00
d09ca013d5 commands: implement response parser for tagtypes available 2025-11-21 15:16:30 +09:00
10913fd48c commands: implement response parser for protocol 2025-11-21 15:14:43 +09:00
73ddb6d498 commands: implement response parser for protocol available 2025-11-21 15:12:21 +09:00
ede28623ef commands: return runtime errors on invalid property names 2025-11-21 15:07:50 +09:00
130fe49597 commands: create result struct for readmessages 2025-11-21 14:55:57 +09:00
4a1df97ad6 commands: precalculate capacity and use iterators 2025-11-21 14:44:17 +09:00
7a966051d5 commands: make better use of expect_property_type! 2025-11-21 14:38:09 +09:00
7dc3d7f9cf commands: implement response parser for listmounts 2025-11-21 14:32:06 +09:00
2b1e99445a commands: implement common traits for responses 2025-11-21 14:18:44 +09:00
e932b62195 commands: implement response parser for decoders 2025-11-21 14:14:25 +09:00
49f440770e commands: implement response parser for listneighbors 2025-11-21 14:04:12 +09:00
153ae9520f commands: implement response parser for outputs 2025-11-21 13:55:10 +09:00
424c530d5d common/types: add debug assertions for range orders 2025-10-13 15:35:56 +09:00
a637f24e66 flake.lock: bump, Cargo.toml: update inputs 2025-10-12 23:01:17 +09:00
1d693b7b2a commands: fix clippy warnings about confusing elided lifetimes 2025-10-12 22:59:50 +09:00
484b1fb68d flake.lock: bump, Cargo.toml: update inputs 2025-09-20 18:05:21 +02:00
2fa58533ba .gitignore: ignore Cargo.lock
This is okay because this is a library
2025-09-20 18:03:35 +02:00
088665c9ff flake.lock: bump
Some checks failed
Build and test / build (push) Successful in 45s
Build and test / check (push) Failing after 46s
Build and test / test (push) Successful in 1m11s
Build and test / docs (push) Successful in 1m37s
2025-08-03 05:01:37 +02:00
3e2e3fdc68 .gitea/workflows: update gitea-web target host
Some checks failed
Build and test / build (push) Failing after 1m17s
Build and test / check (push) Failing after 1m18s
Build and test / docs (push) Failing after 1m26s
Build and test / test (push) Successful in 1m31s
2025-08-03 04:50:54 +02:00
59994ce740 Cargo.toml: update deps
Some checks failed
Build and test / build (push) Successful in 1m29s
Build and test / check (push) Failing after 1m37s
Build and test / docs (push) Failing after 2m1s
Build and test / test (push) Failing after 2m29s
2025-07-11 20:37:16 +02:00
b42cad0b52 flake.lock: bump 2025-07-11 20:36:58 +02:00
3c49ece1a9 flake.nix: add cargo-edit to devshell 2025-07-11 20:36:50 +02:00
186 changed files with 9971 additions and 4139 deletions

View File

@@ -7,20 +7,20 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: debian-latest-slim
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Install rust toolchain - name: Install rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Build - name: Build
run: cargo build --all-features --verbose --release run: cargo build --verbose --release
check: check:
runs-on: ubuntu-latest runs-on: debian-latest-slim
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Install rust toolchain - name: Install rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
@@ -31,12 +31,13 @@ jobs:
run: cargo fmt -- --check run: cargo fmt -- --check
- name: Check clippy - name: Check clippy
run: cargo clippy --all-features -- --deny warnings # run: cargo clippy -- --deny warnings
run: cargo clippy
test: test:
runs-on: ubuntu-latest runs-on: debian-latest-slim
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: cargo-bins/cargo-binstall@main - uses: cargo-bins/cargo-binstall@main
- name: Install rust toolchain - name: Install rust toolchain
@@ -49,7 +50,7 @@ jobs:
- name: Run tests - name: Run tests
run: | run: |
cargo nextest run --all-features --release --no-fail-fast cargo nextest run --release --no-fail-fast
env: env:
RUST_LOG: "trace" RUST_LOG: "trace"
RUSTFLAGS: "-Cinstrument-coverage" RUSTFLAGS: "-Cinstrument-coverage"
@@ -83,13 +84,13 @@ jobs:
target: ${{ gitea.ref_name }}/coverage/ target: ${{ gitea.ref_name }}/coverage/
username: gitea-web username: gitea-web
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }} ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }}
host: bekkalokk.pvv.ntnu.no host: pages.pvv.ntnu.no
known-hosts: "bekkalokk.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEI6VSaDrMG8+flg4/AeHlAFIen8RUzWh6URQKqFegSx" known-hosts: "pages.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH2QjfFB+city1SYqltkVqWACfo1j37k+oQQfj13mtgg"
docs: docs:
runs-on: ubuntu-latest runs-on: debian-latest-slim
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v6
- name: Install latest nightly toolchain - name: Install latest nightly toolchain
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@@ -98,7 +99,7 @@ jobs:
override: true override: true
- name: Build docs - name: Build docs
run: cargo doc --all-features --document-private-items --release run: cargo doc --document-private-items --release
- name: Transfer files - name: Transfer files
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v1 uses: https://git.pvv.ntnu.no/Projects/rsync-action@v1
@@ -107,6 +108,5 @@ jobs:
target: ${{ gitea.ref_name }}/docs/ target: ${{ gitea.ref_name }}/docs/
username: gitea-web username: gitea-web
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }} ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }}
host: bekkalokk.pvv.ntnu.no host: pages.pvv.ntnu.no
known-hosts: "bekkalokk.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEI6VSaDrMG8+flg4/AeHlAFIen8RUzWh6URQKqFegSx" known-hosts: "pages.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH2QjfFB+city1SYqltkVqWACfo1j37k+oQQfj13mtgg"

4
.gitignore vendored
View File

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

95
Cargo.lock generated
View File

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

View File

@@ -11,8 +11,24 @@ edition = "2024"
rust-version = "1.85.0" rust-version = "1.85.0"
[dependencies] [dependencies]
serde = { version = "1.0.210", features = ["derive"] } chrono = { version = "0.4.42", features = ["serde"] }
futures-util = { version = "0.3.31", 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.17"
tokio = { version = "1.48.0", optional = true, features = ["io-util"] }
[features]
default = ["tokio"]
futures = ["dep:futures-util"]
tokio = ["dep:tokio"]
[dev-dependencies] [dev-dependencies]
indoc = "2.0.5" anyhow = "1.0.100"
indoc = "2.0.7"
pretty_assertions = "1.4.1" pretty_assertions = "1.4.1"
tokio = { version = "1.48.0", features = ["macros", "net", "rt"] }
[build-dependencies]
lalrpop = "0.22.2"

8
build.rs Normal file
View File

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

View File

@@ -1,3 +1,14 @@
fn main() { use empidee::MpdClient;
todo!()
#[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);
println!("{}", client.read_initial_mpd_version().await?);
client.play(None).await?;
Ok(())
} }

12
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1746461020, "lastModified": 1764950072,
"narHash": "sha256-7+pG1I9jvxNlmln4YgnlW4o+w0TZX24k688mibiFDUE=", "narHash": "sha256-BmPWzogsG2GsXZtlT+MTcAWeDK5hkbGRZTeZNW42fwA=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3730d8a308f94996a9ba7c7138ede69c1b9ac4ae", "rev": "f61125a668a320878494449750330ca58b78c557",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -29,11 +29,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1746585402, "lastModified": 1765075567,
"narHash": "sha256-Pf+ufu6bYNA1+KQKHnGMNEfTwpD9ZIcAeLoE2yPWIP0=", "narHash": "sha256-KFDCdQcHJ0hE3Nt5Gm5enRIhmtEifAjpxgUQ3mzSJpA=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "72dd969389583664f87aa348b3458f2813693617", "rev": "769156779b41e8787a46ca3d7d76443aaf68be6f",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

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

105
src/client.rs Normal file
View File

@@ -0,0 +1,105 @@
//! 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::{Request, commands::*, types::SongPosition};
#[cfg(feature = "futures")]
use futures_util::{
AsyncBufReadExt,
io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader},
};
use thiserror::Error;
#[cfg(feature = "tokio")]
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
pub struct MpdClient<'a, T>
where
T: AsyncWrite + AsyncRead + Unpin,
{
connection: &'a mut T,
}
#[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 fn new(connection: &'a mut T) -> Self {
MpdClient { connection }
}
pub async fn read_initial_mpd_version(&mut self) -> Result<String, MpdClientError> {
let mut reader = BufReader::new(&mut self.connection);
let mut version_line = String::new();
reader
.read_line(&mut version_line)
.await
.map_err(MpdClientError::ConnectionError)?;
Ok(version_line.trim().to_string())
}
async fn read_response(&mut self) -> Result<Vec<u8>, MpdClientError> {
let mut response = Vec::new();
let mut reader = BufReader::new(&mut self.connection);
loop {
let mut line = Vec::new();
let bytes_read = reader
.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 play(
&mut self,
position: Option<SongPosition>,
) -> Result<PlayResponse, MpdClientError> {
let message = Request::Play(position);
let payload = message.serialize();
self.connection
.write_all(payload.as_bytes())
.await
.map_err(MpdClientError::ConnectionError)?;
self.connection
.flush()
.await
.map_err(MpdClientError::ConnectionError)?;
let response_bytes = self.read_response().await?;
let response = PlayResponse::parse_raw(&response_bytes)?;
Ok(response)
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,26 +1,15 @@
use crate::commands::{ use crate::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, commands::{Command, empty_command_response, single_item_command_request},
ResponseParserError, types::AudioOutputId,
}; };
pub struct DisableOutput; pub struct DisableOutput;
single_item_command_request!(DisableOutput, "disableoutput", AudioOutputId);
empty_command_response!(DisableOutput);
impl Command for DisableOutput { impl Command for DisableOutput {
type Response = (); type Request = DisableOutputRequest;
const COMMAND: &'static str = "disableoutput"; type Response = DisableOutputResponse;
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(())
}
} }

View File

@@ -1,26 +1,15 @@
use crate::commands::{ use crate::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, commands::{Command, empty_command_response, single_item_command_request},
ResponseParserError, types::AudioOutputId,
}; };
pub struct EnableOutput; pub struct EnableOutput;
single_item_command_request!(EnableOutput, "enableoutput", AudioOutputId);
empty_command_response!(EnableOutput);
impl Command for EnableOutput { impl Command for EnableOutput {
type Response = (); type Request = EnableOutputRequest;
const COMMAND: &'static str = "enableoutput"; type Response = EnableOutputResponse;
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(())
}
} }

View File

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

View File

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

View File

@@ -1,26 +1,15 @@
use crate::commands::{ use crate::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, commands::{Command, empty_command_response, single_item_command_request},
ResponseParserError, types::AudioOutputId,
}; };
pub struct ToggleOutput; pub struct ToggleOutput;
single_item_command_request!(ToggleOutput, "toggleoutput", AudioOutputId);
empty_command_response!(ToggleOutput);
impl Command for ToggleOutput { impl Command for ToggleOutput {
type Response = (); type Request = ToggleOutputRequest;
const COMMAND: &'static str = "toggleoutput"; type Response = ToggleOutputResponse;
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(())
}
} }

View File

@@ -1,11 +1,11 @@
pub mod channels; mod channels;
pub mod readmessages; mod readmessages;
pub mod sendmessage; mod sendmessage;
pub mod subscribe; mod subscribe;
pub mod unsubscribe; mod unsubscribe;
pub use channels::Channels; pub use channels::*;
pub use readmessages::ReadMessages; pub use readmessages::*;
pub use sendmessage::SendMessage; pub use sendmessage::*;
pub use subscribe::Subscribe; pub use subscribe::*;
pub use unsubscribe::Unsubscribe; pub use unsubscribe::*;

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,15 @@
use crate::commands::{ use crate::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, commands::{Command, empty_command_response, single_item_command_request},
ResponseParserError, types::ChannelName,
}; };
pub struct Subscribe; pub struct Subscribe;
single_item_command_request!(Subscribe, "subscribe", ChannelName);
empty_command_response!(Subscribe);
impl Command for Subscribe { impl Command for Subscribe {
type Response = (); type Request = SubscribeRequest;
const COMMAND: &'static str = "subscribe"; type Response = SubscribeResponse;
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(())
}
} }

View File

@@ -1,26 +1,15 @@
use crate::commands::{ use crate::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, commands::{Command, empty_command_response, single_item_command_request},
ResponseParserError, types::ChannelName,
}; };
pub struct Unsubscribe; pub struct Unsubscribe;
single_item_command_request!(Unsubscribe, "unsubscribe", ChannelName);
empty_command_response!(Unsubscribe);
impl Command for Unsubscribe { impl Command for Unsubscribe {
type Response = (); type Request = UnsubscribeRequest;
const COMMAND: &'static str = "unsubscribe"; type Response = UnsubscribeResponse;
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(())
}
} }

View File

@@ -1,37 +1,37 @@
pub mod binary_limit; mod binary_limit;
pub mod close; mod close;
pub mod kill; mod kill;
pub mod password; mod password;
pub mod ping; mod ping;
pub mod protocol; mod protocol;
pub mod protocol_all; mod protocol_all;
pub mod protocol_available; mod protocol_available;
pub mod protocol_clear; mod protocol_clear;
pub mod protocol_disable; mod protocol_disable;
pub mod protocol_enable; mod protocol_enable;
pub mod tag_types; mod tag_types;
pub mod tag_types_all; mod tag_types_all;
pub mod tag_types_available; mod tag_types_available;
pub mod tag_types_clear; mod tag_types_clear;
pub mod tag_types_disable; mod tag_types_disable;
pub mod tag_types_enable; mod tag_types_enable;
pub mod tag_types_reset; mod tag_types_reset;
pub use binary_limit::BinaryLimit; pub use binary_limit::*;
pub use close::Close; pub use close::*;
pub use kill::Kill; pub use kill::*;
pub use password::Password; pub use password::*;
pub use ping::Ping; pub use ping::*;
pub use protocol::Protocol; pub use protocol::*;
pub use protocol_all::ProtocolAll; pub use protocol_all::*;
pub use protocol_available::ProtocolAvailable; pub use protocol_available::*;
pub use protocol_clear::ProtocolClear; pub use protocol_clear::*;
pub use protocol_disable::ProtocolDisable; pub use protocol_disable::*;
pub use protocol_enable::ProtocolEnable; pub use protocol_enable::*;
pub use tag_types::TagTypes; pub use tag_types::*;
pub use tag_types_all::TagTypesAll; pub use tag_types_all::*;
pub use tag_types_available::TagTypesAvailable; pub use tag_types_available::*;
pub use tag_types_clear::TagTypesClear; pub use tag_types_clear::*;
pub use tag_types_disable::TagTypesDisable; pub use tag_types_disable::*;
pub use tag_types_enable::TagTypesEnable; pub use tag_types_enable::*;
pub use tag_types_reset::TagTypesReset; pub use tag_types_reset::*;

View File

@@ -1,27 +1,12 @@
use crate::commands::{ use crate::commands::{Command, empty_command_response, single_item_command_request};
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct BinaryLimit; pub struct BinaryLimit;
single_item_command_request!(BinaryLimit, "binarylimit", u64);
empty_command_response!(BinaryLimit);
impl Command for BinaryLimit { impl Command for BinaryLimit {
type Response = (); type Request = BinaryLimitRequest;
const COMMAND: &'static str = "binarylimit"; type Response = BinaryLimitResponse;
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(())
}
} }

View File

@@ -1,22 +1,12 @@
use crate::commands::{ use crate::commands::{Command, empty_command_request, empty_command_response};
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
pub struct Close; pub struct Close;
empty_command_request!(Close, "close");
empty_command_response!(Close);
impl Command for Close { impl Command for Close {
type Response = (); type Request = CloseRequest;
const COMMAND: &'static str = "close"; type Response = CloseResponse;
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(())
}
} }

View File

@@ -1,22 +1,12 @@
use crate::commands::{ use crate::commands::{Command, empty_command_request, empty_command_response};
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
pub struct Kill; pub struct Kill;
empty_command_request!(Kill, "kill");
empty_command_response!(Kill);
impl Command for Kill { impl Command for Kill {
type Response = (); type Request = KillRequest;
const COMMAND: &'static str = "kill"; type Response = KillResponse;
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(())
}
} }

View File

@@ -1,27 +1,12 @@
use crate::commands::{ use crate::commands::{Command, empty_command_response, single_item_command_request};
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct Password; pub struct Password;
single_item_command_request!(Password, "password", String);
empty_command_response!(Password);
impl Command for Password { impl Command for Password {
type Response = (); type Request = PasswordRequest;
const COMMAND: &'static str = "password"; type Response = PasswordResponse;
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(())
}
} }

View File

@@ -1,22 +1,12 @@
use crate::commands::{ use crate::commands::{Command, empty_command_request, empty_command_response};
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
pub struct Ping; pub struct Ping;
empty_command_request!(Ping, "ping");
empty_command_response!(Ping);
impl Command for Ping { impl Command for Ping {
type Response = (); type Request = PingRequest;
const COMMAND: &'static str = "ping"; type Response = PingResponse;
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(())
}
} }

View File

@@ -1,22 +1,15 @@
use crate::{ use crate::{
Request, commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError}, response_tokenizer::expect_property_type,
}; };
pub struct Protocol; pub struct Protocol;
empty_command_request!(Protocol, "protocol");
multi_item_command_response!(Protocol, "feature", String);
impl Command for Protocol { impl Command for Protocol {
type Response = (); type Request = ProtocolRequest;
const COMMAND: &'static str = "protocol"; type Response = ProtocolResponse;
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!()
}
} }

View File

@@ -1,23 +1,12 @@
use crate::{ use crate::commands::{Command, empty_command_request, empty_command_response};
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
};
pub struct ProtocolAll; pub struct ProtocolAll;
empty_command_request!(ProtocolAll, "protocol all");
empty_command_response!(ProtocolAll);
impl Command for ProtocolAll { impl Command for ProtocolAll {
type Response = (); type Request = ProtocolAllRequest;
const COMMAND: &'static str = "protocol all"; type Response = ProtocolAllResponse;
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(())
}
} }

View File

@@ -1,22 +1,15 @@
use crate::{ use crate::{
Request, commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError}, response_tokenizer::expect_property_type,
}; };
pub struct ProtocolAvailable; pub struct ProtocolAvailable;
empty_command_request!(ProtocolAvailable, "protocol available");
multi_item_command_response!(ProtocolAvailable, "feature", String);
impl Command for ProtocolAvailable { impl Command for ProtocolAvailable {
type Response = (); type Request = ProtocolAvailableRequest;
const COMMAND: &'static str = "protocol available"; type Response = ProtocolAvailableResponse;
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!()
}
} }

View File

@@ -1,23 +1,12 @@
use crate::{ use crate::commands::{Command, empty_command_request, empty_command_response};
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
};
pub struct ProtocolClear; pub struct ProtocolClear;
empty_command_request!(ProtocolClear, "protocol clear");
empty_command_response!(ProtocolClear);
impl Command for ProtocolClear { impl Command for ProtocolClear {
type Response = (); type Request = ProtocolClearRequest;
const COMMAND: &'static str = "protocol clear"; type Response = ProtocolClearResponse;
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(())
}
} }

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,12 @@
use crate::{ use crate::commands::{Command, empty_command_request, empty_command_response};
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
};
pub struct TagTypesAll; pub struct TagTypesAll;
empty_command_request!(TagTypesAll, "tagtypes all");
empty_command_response!(TagTypesAll);
impl Command for TagTypesAll { impl Command for TagTypesAll {
type Response = (); type Request = TagTypesAllRequest;
const COMMAND: &'static str = "tagtypes all"; type Response = TagTypesAllResponse;
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(())
}
} }

View File

@@ -1,22 +1,15 @@
use crate::{ use crate::{
Request, commands::{Command, ResponseParserError, empty_command_request, multi_item_command_response},
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError}, response_tokenizer::expect_property_type,
}; };
pub struct TagTypesAvailable; pub struct TagTypesAvailable;
empty_command_request!(TagTypesAvailable, "tagtypes available");
multi_item_command_response!(TagTypesAvailable, "tagtype", String);
impl Command for TagTypesAvailable { impl Command for TagTypesAvailable {
type Response = (); type Request = TagTypesAvailableRequest;
const COMMAND: &'static str = "tagtypes available"; type Response = TagTypesAvailableResponse;
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!()
}
} }

View File

@@ -1,23 +1,12 @@
use crate::{ use crate::commands::{Command, empty_command_request, empty_command_response};
Request,
commands::{Command, RequestParserResult, ResponseAttributes, ResponseParserError},
};
pub struct TagTypesClear; pub struct TagTypesClear;
empty_command_request!(TagTypesClear, "tagtypes clear");
empty_command_response!(TagTypesClear);
impl Command for TagTypesClear { impl Command for TagTypesClear {
type Response = (); type Request = TagTypesClearRequest;
const COMMAND: &'static str = "tagtypes clear"; type Response = TagTypesClearResponse;
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(())
}
} }

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,19 @@
pub mod next; mod next;
pub mod pause; mod pause;
pub mod play; mod play;
pub mod playid; mod playid;
pub mod previous; mod previous;
pub mod seek; mod seek;
pub mod seekcur; mod seekcur;
pub mod seekid; mod seekid;
pub mod stop; mod stop;
pub use next::Next; pub use next::*;
pub use pause::Pause; pub use pause::*;
pub use play::Play; pub use play::*;
pub use playid::PlayId; pub use playid::*;
pub use previous::Previous; pub use previous::*;
pub use seek::Seek; pub use seek::*;
pub use seekcur::SeekCur; pub use seekcur::*;
pub use seekid::SeekId; pub use seekid::*;
pub use stop::Stop; pub use stop::*;

View File

@@ -1,22 +1,12 @@
use crate::commands::{ use crate::commands::{Command, empty_command_request, empty_command_response};
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
pub struct Next; pub struct Next;
empty_command_request!(Next, "next");
empty_command_response!(Next);
impl Command for Next { impl Command for Next {
type Response = (); type Request = NextRequest;
const COMMAND: &'static str = "next"; type Response = NextResponse;
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(())
}
} }

View File

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

View File

@@ -1,34 +1,15 @@
use crate::{ use crate::{
commands::{ commands::{Command, empty_command_response, single_optional_item_command_request},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, types::SongPosition,
ResponseParserError,
},
common::SongPosition,
}; };
pub struct Play; pub struct Play;
single_optional_item_command_request!(Play, "play", SongPosition);
empty_command_response!(Play);
impl Command for Play { impl Command for Play {
type Response = (); type Request = PlayRequest;
const COMMAND: &'static str = "play"; type Response = PlayResponse;
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(())
}
} }

View File

@@ -1,34 +1,15 @@
use crate::{ use crate::{
commands::{ commands::{Command, empty_command_response, single_optional_item_command_request},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, types::SongId,
ResponseParserError,
},
common::SongId,
}; };
pub struct PlayId; pub struct PlayId;
single_optional_item_command_request!(PlayId, "playid", SongId);
empty_command_response!(PlayId);
impl Command for PlayId { impl Command for PlayId {
type Response = (); type Request = PlayIdRequest;
const COMMAND: &'static str = "playid"; type Response = PlayIdResponse;
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(())
}
} }

View File

@@ -1,22 +1,12 @@
use crate::commands::{ use crate::commands::{Command, empty_command_request, empty_command_response};
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
pub struct Previous; pub struct Previous;
empty_command_request!(Previous, "previous");
empty_command_response!(Previous);
impl Command for Previous { impl Command for Previous {
type Response = (); type Request = PreviousRequest;
const COMMAND: &'static str = "previous"; type Response = PreviousResponse;
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(())
}
} }

View File

@@ -1,41 +1,73 @@
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::{ commands::{Command, CommandRequest, RequestParserError, empty_command_response},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, request_tokenizer::RequestTokenizer,
ResponseParserError, types::{SongPosition, TimeWithFractions},
},
common::{SongPosition, TimeWithFractions},
}; };
pub struct Seek; pub struct Seek;
impl Command for Seek { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
type Response = (); pub struct SeekRequest {
const COMMAND: &'static str = "seek"; pub songpos: SongPosition,
pub time: TimeWithFractions,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { impl CommandRequest for SeekRequest {
const COMMAND: &'static str = "seek";
const MIN_ARGS: u32 = 2;
const MAX_ARGS: Option<u32> = Some(2);
fn into_request_enum(self) -> crate::Request {
crate::Request::Seek(self.songpos, self.time)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Seek(songpos, time) => Some(SeekRequest { songpos, time }),
_ => None,
}
}
fn serialize(&self) -> String {
format!("{} {} {}\n", Self::COMMAND, self.songpos, self.time)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let songpos = match parts.next() { let songpos = match parts.next() {
Some(s) => s Some(s) => {
.parse::<SongPosition>() s.parse::<SongPosition>()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?, .map_err(|_| RequestParserError::SubtypeParserError {
None => return Err(RequestParserError::UnexpectedEOF), argument_index: 0,
expected_type: "SongPosition",
raw_input: s.to_owned(),
})?
}
None => return Err(Self::missing_arguments_error(0)),
}; };
let time = match parts.next() { let time = match parts.next() {
Some(t) => t Some(t) => t.parse::<TimeWithFractions>().map_err(|_| {
.parse::<TimeWithFractions>() RequestParserError::SubtypeParserError {
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?, argument_index: 1,
None => return Err(RequestParserError::UnexpectedEOF), expected_type: "TimeWithFractions",
raw_input: t.to_owned(),
}
})?,
None => return Err(Self::missing_arguments_error(1)),
}; };
debug_assert!(parts.next().is_none()); Self::throw_if_too_many_arguments(parts)?;
Ok((Request::Seek(songpos, time), "")) Ok(SeekRequest { 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;
}

View File

@@ -1,53 +1,96 @@
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::{ commands::{Command, CommandRequest, RequestParserError, empty_command_response},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, request_tokenizer::RequestTokenizer,
ResponseParserError, types::{SeekMode, TimeWithFractions},
},
common::{SeekMode, TimeWithFractions},
}; };
pub struct SeekCur; pub struct SeekCur;
impl Command for SeekCur { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
type Response = (); pub struct SeekCurRequest {
const COMMAND: &'static str = "seekcur"; pub mode: SeekMode,
pub time: TimeWithFractions,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { impl CommandRequest for SeekCurRequest {
const COMMAND: &'static str = "seekcur";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(1);
fn into_request_enum(self) -> crate::Request {
crate::Request::SeekCur(self.mode, self.time)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::SeekCur(mode, time) => Some(SeekCurRequest { mode, time }),
_ => None,
}
}
fn serialize(&self) -> String {
let time_str = match self.mode {
SeekMode::Absolute => format!("{}", self.time),
SeekMode::Relative => format!("+{}", self.time),
SeekMode::RelativeReverse => format!("-{}", self.time),
};
format!("{} {}\n", Self::COMMAND, time_str)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let time_raw = match parts.next() { let time_raw = match parts.next() {
Some(t) => t, Some(t) => t,
None => return Err(RequestParserError::UnexpectedEOF), None => return Err(Self::missing_arguments_error(0)),
}; };
Self::throw_if_too_many_arguments(parts)?;
// TODO: DRY // TODO: DRY
let (mode, time) = match time_raw { let (mode, time) = match time_raw {
t if t.starts_with('+') => ( t if t.starts_with('+') => (
SeekMode::Relative, SeekMode::Relative,
t[1..] t[1..].parse::<TimeWithFractions>().map_err(|_| {
.parse::<TimeWithFractions>() RequestParserError::SubtypeParserError {
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?, argument_index: 0,
expected_type: "TimeWithFractions",
raw_input: t[1..].to_owned(),
}
})?,
), ),
t if t.starts_with('-') => ( t if t.starts_with('-') => (
SeekMode::Relative, SeekMode::RelativeReverse,
-t[1..] t[1..].parse::<TimeWithFractions>().map_err(|_| {
.parse::<TimeWithFractions>() RequestParserError::SubtypeParserError {
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?, argument_index: 0,
expected_type: "TimeWithFractions",
raw_input: t[1..].to_owned(),
}
})?,
), ),
t => ( t => (
SeekMode::Absolute, SeekMode::Absolute,
t.parse::<TimeWithFractions>() t.parse::<TimeWithFractions>().map_err(|_| {
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?, RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "TimeWithFractions",
raw_input: t.to_owned(),
}
})?,
), ),
}; };
debug_assert!(parts.next().is_none()); debug_assert!(time >= 0.0);
Ok((Request::SeekCur(mode, time), "")) Ok(SeekCurRequest { 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;
}

View File

@@ -1,41 +1,71 @@
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::{ commands::{Command, CommandRequest, RequestParserError, empty_command_response},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, request_tokenizer::RequestTokenizer,
ResponseParserError, types::{SongId, TimeWithFractions},
},
common::{SongId, TimeWithFractions},
}; };
pub struct SeekId; pub struct SeekId;
impl Command for SeekId { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
type Response = (); pub struct SeekIdRequest {
const COMMAND: &'static str = "seekid"; pub songid: SongId,
pub time: TimeWithFractions,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { impl CommandRequest for SeekIdRequest {
const COMMAND: &'static str = "seekid";
const MIN_ARGS: u32 = 2;
const MAX_ARGS: Option<u32> = Some(2);
fn into_request_enum(self) -> crate::Request {
crate::Request::SeekId(self.songid, self.time)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::SeekId(songid, time) => Some(SeekIdRequest { songid, time }),
_ => None,
}
}
fn serialize(&self) -> String {
format!("{} {} {}\n", Self::COMMAND, self.songid, self.time)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let songid = match parts.next() { let songid = match parts.next() {
Some(s) => s Some(s) => s
.parse::<SongId>() .parse::<SongId>()
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?, .map_err(|_| RequestParserError::SubtypeParserError {
None => return Err(RequestParserError::UnexpectedEOF), argument_index: 0,
expected_type: "SongId",
raw_input: s.to_owned(),
})?,
None => return Err(Self::missing_arguments_error(0)),
}; };
let time = match parts.next() { let time = match parts.next() {
Some(t) => t Some(t) => t.parse::<TimeWithFractions>().map_err(|_| {
.parse::<TimeWithFractions>() RequestParserError::SubtypeParserError {
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?, argument_index: 1,
None => return Err(RequestParserError::UnexpectedEOF), expected_type: "TimeWithFractions",
raw_input: t.to_owned(),
}
})?,
None => return Err(Self::missing_arguments_error(1)),
}; };
debug_assert!(parts.next().is_none()); Self::throw_if_too_many_arguments(parts)?;
Ok((Request::SeekId(songid, time), "")) Ok(SeekIdRequest { 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;
}

View File

@@ -1,22 +1,12 @@
use crate::commands::{ use crate::commands::{Command, empty_command_request, empty_command_response};
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
pub struct Stop; pub struct Stop;
empty_command_request!(Stop, "stop");
empty_command_response!(Stop);
impl Command for Stop { impl Command for Stop {
type Response = (); type Request = StopRequest;
const COMMAND: &'static str = "stop"; type Response = StopResponse;
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(())
}
} }

View File

@@ -1,9 +1,9 @@
pub mod listmounts; mod listmounts;
pub mod listneighbors; mod listneighbors;
pub mod mount; mod mount;
pub mod unmount; mod unmount;
pub use listmounts::ListMounts; pub use listmounts::*;
pub use listneighbors::ListNeighbors; pub use listneighbors::*;
pub use mount::Mount; pub use mount::*;
pub use unmount::Unmount; pub use unmount::*;

View File

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

View File

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

View File

@@ -1,36 +1,69 @@
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
Request, commands::{Command, CommandRequest, RequestParserError, empty_command_response},
commands::{ request_tokenizer::RequestTokenizer,
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError, types::{MountPath, Uri},
},
}; };
pub struct Mount; pub struct Mount;
impl Command for Mount { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
type Response = (); pub struct MountRequest {
const COMMAND: &'static str = "mount"; pub path: MountPath,
pub uri: Uri,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { impl CommandRequest for MountRequest {
let path = parts const COMMAND: &'static str = "mount";
.next() const MIN_ARGS: u32 = 2;
.ok_or(RequestParserError::UnexpectedEOF)? const MAX_ARGS: Option<u32> = Some(2);
.to_string();
fn into_request_enum(self) -> crate::Request {
crate::Request::Mount(self.path, self.uri)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Mount(path, uri) => Some(MountRequest { path, uri }),
_ => None,
}
}
fn serialize(&self) -> String {
debug_assert!(self.path.to_str().is_some());
format!(
"{} {} {}\n",
Self::COMMAND,
self.path.to_str().unwrap_or("<invalid path>"),
self.uri
)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let path = parts.next().ok_or(Self::missing_arguments_error(0))?;
let path = path
.parse()
.map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "MountPath",
raw_input: path.to_string(),
})?;
let uri = parts let uri = parts
.next() .next()
.ok_or(RequestParserError::UnexpectedEOF)? .ok_or(Self::missing_arguments_error(1))?
.to_string(); .to_string();
debug_assert!(parts.next().is_none()); Self::throw_if_too_many_arguments(parts)?;
Ok((Request::Mount(path, uri), "")) Ok(MountRequest { 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;
}

View File

@@ -1,31 +1,59 @@
use crate::{ use crate::{
Request,
commands::{ commands::{
Command, RequestParserError, RequestParserResult, ResponseAttributes, ResponseParserError, Command, CommandRequest, RequestParserError, RequestTokenizer, empty_command_response,
}, },
types::MountPath,
}; };
pub struct Unmount; pub struct Unmount;
impl Command for Unmount { #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
type Response = (); pub struct UnmountRequest(MountPath);
impl CommandRequest for UnmountRequest {
const COMMAND: &'static str = "unmount"; const COMMAND: &'static str = "unmount";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(1);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { fn into_request_enum(self) -> crate::Request {
let path = parts crate::Request::Unmount(self.0)
.next()
.ok_or(RequestParserError::UnexpectedEOF)?
.to_string();
debug_assert!(parts.next().is_none());
Ok((Request::Unmount(path), ""))
} }
fn parse_response( fn from_request_enum(request: crate::Request) -> Option<Self> {
parts: ResponseAttributes<'_>, match request {
) -> Result<Self::Response, ResponseParserError> { crate::Request::Unmount(item) => Some(UnmountRequest(item)),
debug_assert!(parts.is_empty()); _ => None,
Ok(()) }
}
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;
}

View File

@@ -1,37 +1,37 @@
pub mod albumart; mod albumart;
pub mod count; mod count;
pub mod find; mod find;
pub mod findadd; mod findadd;
pub mod getfingerprint; mod getfingerprint;
pub mod list; mod list;
pub mod listall; mod listall;
pub mod listallinfo; mod listallinfo;
pub mod listfiles; mod listfiles;
pub mod lsinfo; mod lsinfo;
pub mod readcomments; mod readcomments;
pub mod readpicture; mod readpicture;
pub mod rescan; mod rescan;
pub mod search; mod search;
pub mod searchadd; mod searchadd;
pub mod searchaddpl; mod searchaddpl;
pub mod searchcount; mod searchcount;
pub mod update; mod update;
pub use albumart::AlbumArt; pub use albumart::*;
pub use count::Count; pub use count::*;
pub use find::Find; pub use find::*;
pub use findadd::FindAdd; pub use findadd::*;
pub use getfingerprint::GetFingerprint; pub use getfingerprint::*;
pub use list::List; pub use list::*;
pub use listall::ListAll; pub use listall::*;
pub use listallinfo::ListAllInfo; pub use listallinfo::*;
pub use listfiles::ListFiles; pub use listfiles::*;
pub use lsinfo::LsInfo; pub use lsinfo::*;
pub use readcomments::ReadComments; pub use readcomments::*;
pub use readpicture::ReadPicture; pub use readpicture::*;
pub use rescan::Rescan; pub use rescan::*;
pub use search::Search; pub use search::*;
pub use searchadd::SearchAdd; pub use searchadd::*;
pub use searchaddpl::SearchAddPl; pub use searchaddpl::*;
pub use searchcount::SearchCount; pub use searchcount::*;
pub use update::Update; pub use update::*;

View File

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

View File

@@ -1,48 +1,100 @@
use std::collections::HashMap; use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::{ commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, filter::Filter,
ResponseParserError, get_and_parse_property, request_tokenizer::RequestTokenizer,
}, response_tokenizer::{ResponseAttributes, get_and_parse_property},
filter::parse_filter, types::GroupType,
}; };
pub struct Count; pub struct Count;
pub struct CountResponse { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub songs: usize, pub struct CountRequest {
pub playtime: u64, filter: Filter,
group: Option<GroupType>,
} }
impl Command for Count { impl CommandRequest for CountRequest {
type Response = CountResponse;
const COMMAND: &'static str = "count"; const COMMAND: &'static str = "count";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(2);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { fn into_request_enum(self) -> crate::Request {
let filter = parse_filter(&mut parts)?; crate::Request::Count(self.filter, self.group)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Count(filter, group) => Some(CountRequest { filter, group }),
_ => None,
}
}
fn serialize(&self) -> String {
let mut cmd = format!("{} {}", Self::COMMAND, self.filter);
if let Some(group) = self.group.as_ref() {
cmd.push_str(&format!(" group {}", group));
}
cmd.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 = if let Some("group") = parts.next() {
let group = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; let group = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
argument_index: 1,
keyword: "group",
})?;
Some( Some(
group group
.parse() .parse()
.map_err(|_| RequestParserError::SyntaxError(1, group.to_owned()))?, .map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 1,
expected_type: "GroupType",
raw_input: group.to_owned(),
})?,
) )
} else { } else {
None None
}; };
debug_assert!(parts.next().is_none()); Self::throw_if_too_many_arguments(parts)?;
Ok((Request::Count(filter, group), "")) Ok(CountRequest { filter, group })
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CountResponse {
pub songs: usize,
pub playtime: u64,
}
impl CommandResponse for CountResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
} }
fn parse_response( fn from_response_enum(response: crate::Response) -> Option<Self> {
parts: ResponseAttributes<'_>, todo!()
) -> Result<Self::Response, ResponseParserError> { }
let parts: HashMap<_, _> = parts.into();
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
let songs = get_and_parse_property!(parts, "songs", Text); let songs = get_and_parse_property!(parts, "songs", Text);
let playtime = get_and_parse_property!(parts, "playtime", Text); let playtime = get_and_parse_property!(parts, "playtime", Text);
@@ -50,3 +102,8 @@ impl Command for Count {
Ok(CountResponse { songs, playtime }) Ok(CountResponse { songs, playtime })
} }
} }
impl Command for Count {
type Request = CountRequest;
type Response = CountResponse;
}

View File

@@ -1,51 +1,143 @@
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::{ commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, filter::Filter,
ResponseParserError, request_tokenizer::RequestTokenizer,
}, response_tokenizer::ResponseAttributes,
filter::parse_filter, types::{DbSelectionPrintResponse, DbSongInfo, Sort, WindowRange},
}; };
pub struct Find; pub struct Find;
pub struct FindResponse {} #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FindRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
}
impl Command for Find { impl CommandRequest for FindRequest {
type Response = FindResponse;
const COMMAND: &'static str = "find"; const COMMAND: &'static str = "find";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(3);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { fn into_request_enum(self) -> crate::Request {
let filter = parse_filter(&mut parts)?; crate::Request::Find(self.filter, self.sort, self.window)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Find(filter, sort, window) => Some(FindRequest {
filter,
sort,
window,
}),
_ => None,
}
}
fn serialize(&self) -> String {
let mut cmd = format!("{} {}", Self::COMMAND, self.filter);
if let Some(sort) = &self.sort {
cmd.push_str(&format!(" sort {}", sort));
}
if let Some(window) = &self.window {
cmd.push_str(&format!(" window {}", window));
}
cmd.push('\n');
cmd
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let filter = match parts.next() {
Some(f) => {
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?
}
None => return Err(Self::missing_arguments_error(0)),
};
let mut argument_index_counter = 0;
let mut sort_or_window = parts.next(); let mut sort_or_window = parts.next();
let mut sort = None; let mut sort = None;
if let Some("sort") = sort_or_window { 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( sort = Some(
parts s.parse()
.next() .map_err(|_| RequestParserError::SubtypeParserError {
.ok_or(RequestParserError::UnexpectedEOF)? argument_index: argument_index_counter,
.to_string(), expected_type: "Sort",
raw_input: s.to_string(),
})?,
); );
sort_or_window = parts.next(); sort_or_window = parts.next();
} }
let mut window = None; let mut window = None;
if let Some("window") = sort_or_window { if let Some("window") = sort_or_window {
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
window = Some( window = Some(
w.parse() w.parse()
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?, .map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
); );
} }
debug_assert!(parts.next().is_none()); Self::throw_if_too_many_arguments(parts)?;
Ok((Request::Find(filter, sort, window), "")) Ok(FindRequest {
} filter,
sort,
fn parse_response( window,
parts: ResponseAttributes<'_>, })
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FindResponse(Vec<DbSongInfo>);
impl CommandResponse for FindResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
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;
}

View File

@@ -1,60 +1,141 @@
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::{ commands::{Command, CommandRequest, RequestParserError, empty_command_response},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, filter::Filter,
ResponseParserError, request_tokenizer::RequestTokenizer,
}, types::{SongPosition, Sort, WindowRange},
filter::parse_filter,
}; };
pub struct FindAdd; pub struct FindAdd;
impl Command for FindAdd { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
type Response = (); pub struct FindAddRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
}
impl CommandRequest for FindAddRequest {
const COMMAND: &'static str = "findadd"; const COMMAND: &'static str = "findadd";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(4);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { fn into_request_enum(self) -> crate::Request {
let filter = parse_filter(&mut parts)?; crate::Request::FindAdd(self.filter, self.sort, self.window, self.position)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::FindAdd(filter, sort, window, position) => Some(FindAddRequest {
filter,
sort,
window,
position,
}),
_ => None,
}
}
fn serialize(&self) -> String {
let mut cmd = format!("{} {}", Self::COMMAND, self.filter);
if let Some(sort) = &self.sort {
cmd.push_str(&format!(" sort {}", sort));
}
if let Some(window) = &self.window {
cmd.push_str(&format!(" window {}", window));
}
if let Some(position) = &self.position {
cmd.push_str(&format!(" position {}", position));
}
cmd.push('\n');
cmd
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let filter = match parts.next() {
Some(f) => {
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?
}
None => return Err(Self::missing_arguments_error(0)),
};
let mut argument_index_counter = 0;
let mut sort_or_window_or_position = parts.next(); let mut sort_or_window_or_position = parts.next();
let mut sort = None; let mut sort = None;
if let Some("sort") = sort_or_window_or_position { 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( sort = Some(
parts s.parse()
.next() .map_err(|_| RequestParserError::SubtypeParserError {
.ok_or(RequestParserError::UnexpectedEOF)? argument_index: argument_index_counter,
.to_string(), expected_type: "Sort",
raw_input: s.to_string(),
})?,
); );
sort_or_window_or_position = parts.next(); sort_or_window_or_position = parts.next();
} }
let mut window = None; let mut window = None;
if let Some("window") = sort_or_window_or_position { if let Some("window") = sort_or_window_or_position {
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
window = Some( window = Some(
w.parse() w.parse()
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?, .map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
); );
sort_or_window_or_position = parts.next(); sort_or_window_or_position = parts.next();
} }
let mut position = None; let mut position = None;
if let Some("position") = sort_or_window_or_position { if let Some("position") = sort_or_window_or_position {
let p = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; argument_index_counter += 1;
let p = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "position",
argument_index: argument_index_counter,
})?;
position = Some( position = Some(
p.parse() p.parse()
.map_err(|_| RequestParserError::SyntaxError(0, p.to_string()))?, .map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "SongPosition",
raw_input: p.to_string(),
})?,
); );
} }
debug_assert!(parts.next().is_none()); Self::throw_if_too_many_arguments(parts)?;
Ok((Request::FindAdd(filter, sort, window, position), "")) Ok(FindAddRequest {
} filter,
sort,
fn parse_response( window,
parts: ResponseAttributes<'_>, position,
) -> Result<Self::Response, ResponseParserError> { })
debug_assert!(parts.is_empty());
Ok(())
} }
} }
empty_command_response!(FindAdd);
impl Command for FindAdd {
type Request = FindAddRequest;
type Response = FindAddResponse;
}

View File

@@ -1,38 +1,41 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::commands::{ use serde::{Deserialize, Serialize};
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError, get_and_parse_property, use crate::{
commands::{Command, CommandResponse, ResponseParserError, single_item_command_request},
response_tokenizer::{ResponseAttributes, get_and_parse_property},
types::Uri,
}; };
pub struct GetFingerprint; pub struct GetFingerprint;
single_item_command_request!(GetFingerprint, "getfingerprint", Uri);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GetFingerprintResponse { pub struct GetFingerprintResponse {
pub chromaprint: String, pub chromaprint: String,
} }
impl Command for GetFingerprint { impl CommandResponse for GetFingerprintResponse {
type Response = GetFingerprintResponse; fn into_response_enum(self) -> crate::Response {
const COMMAND: &'static str = "getfingerprint"; todo!()
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( fn from_response_enum(response: crate::Response) -> Option<Self> {
parts: ResponseAttributes<'_>, todo!()
) -> Result<Self::Response, ResponseParserError> { }
let parts: HashMap<_, _> = parts.into();
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
let chromaprint = get_and_parse_property!(parts, "chromaprint", Text); let chromaprint = get_and_parse_property!(parts, "chromaprint", Text);
Ok(GetFingerprintResponse { chromaprint }) Ok(GetFingerprintResponse { chromaprint })
} }
} }
impl Command for GetFingerprint {
type Request = GetFingerprintRequest;
type Response = GetFingerprintResponse;
}

View File

@@ -1,58 +1,169 @@
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::{ commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, filter::Filter,
ResponseParserError, expect_property_type, request_tokenizer::RequestTokenizer,
}, response_tokenizer::{ResponseAttributes, expect_property_type},
filter::parse_filter, types::{GroupType, TagName, WindowRange},
}; };
pub struct List; pub struct List;
pub type ListResponse = Vec<String>; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ListRequest {
tagname: TagName,
filter: Option<Filter>,
groups: Vec<GroupType>,
window: Option<WindowRange>,
}
impl Command for List { impl CommandRequest for ListRequest {
type Response = ListResponse;
const COMMAND: &'static str = "list"; const COMMAND: &'static str = "list";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = None;
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { fn into_request_enum(self) -> crate::Request {
let tagtype = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; crate::Request::List(self.tagname, self.filter, self.groups, self.window)
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( fn from_request_enum(request: crate::Request) -> Option<Self> {
parts: ResponseAttributes<'_>, match request {
) -> Result<Self::Response, ResponseParserError> { crate::Request::List(tagname, filter, groups, window) => Some(ListRequest {
tagname,
filter,
groups,
window,
}),
_ => 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 CommandResponse for ListResponse {
fn into_response_enum(self) -> crate::Response {
todo!()
}
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts_: Vec<_> = parts.into_vec()?;
debug_assert!({ debug_assert!({
let key = parts.0.first().map(|(k, _)| k); let key = parts_.first().map(|(k, _)| k);
parts.0.iter().all(|(k, _)| k == key.unwrap()) parts_.iter().all(|(k, _)| k == key.unwrap())
}); });
let list = parts let list = parts_
.0
.into_iter() .into_iter()
.map(|(k, v)| Ok(expect_property_type!(Some(v), k, Text).to_string())) .map(|(k, v)| Ok(expect_property_type!(Some(v), k, Text).to_string()))
.collect::<Result<Vec<_>, ResponseParserError>>()?; .collect::<Result<Vec<_>, ResponseParserError>>()?;
Ok(list) Ok(ListResponse(list))
} }
} }
impl Command for List {
type Request = ListRequest;
type Response = ListResponse;
}

View File

@@ -1,34 +1,163 @@
use crate::commands::{ use serde::{Deserialize, Serialize};
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError, use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::ResponseAttributes,
types::{DbSelectionPrintResponse, Uri},
}; };
pub struct ListAll; 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 // TODO: This is supposed to be a tree-like structure, with directories containing files and playlists
pub type ListAllResponse = Vec<String>; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ListAllResponse(Vec<DbSelectionPrintResponse>);
impl Command for ListAll { impl CommandResponse for ListAllResponse {
type Response = ListAllResponse; fn into_response_enum(self) -> crate::Response {
const COMMAND: &'static str = "listall"; todo!()
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = parts
.next()
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))
})
.transpose()?;
debug_assert!(parts.next().is_none());
Ok((Request::ListAll(uri), ""))
} }
fn parse_response( fn from_response_enum(response: crate::Response) -> Option<Self> {
parts: ResponseAttributes<'_>, todo!()
) -> Result<Self::Response, ResponseParserError> { }
unimplemented!()
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let result = DbSelectionPrintResponse::parse(parts)?;
Ok(ListAllResponse(result))
}
}
impl Command for ListAll {
type Request = ListAllRequest;
type Response = ListAllResponse;
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::types::{DbDirectoryInfo, DbPlaylistInfo, DbSongInfo};
use super::*;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_parse_response() {
let response = indoc! {"
directory: albums
directory: albums/a
file: albums/a/song1.mp3
file: albums/a/song2.mp3
file: albums/a/song3.mp3
playlist: albums/a/album a.m3u8
directory: albums/b
file: albums/b/song1.mp3
file: albums/b/song2.mp3
file: albums/b/song3.mp3
playlist: albums/b/album b.m3u8
OK
"};
let result = ListAll::parse_raw_response(response.as_bytes());
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
})
]))
)
} }
} }

View File

@@ -1,35 +1,120 @@
use crate::commands::{ use serde::{Deserialize, Serialize};
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError, use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::ResponseAttributes,
types::{DbSelectionPrintResponse, Uri},
}; };
pub struct ListAllInfo; 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 // TODO: This is supposed to be a tree-like structure, with directories containing files and playlists
// in addition to the metadata of each entry #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub type ListAllInfoResponse = Vec<String>; pub struct ListAllInfoResponse(Vec<DbSelectionPrintResponse>);
impl Command for ListAllInfo { impl CommandResponse for ListAllInfoResponse {
type Response = ListAllInfoResponse; fn from_response_enum(response: crate::Response) -> Option<Self> {
const COMMAND: &'static str = "listallinfo"; todo!()
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = parts
.next()
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))
})
.transpose()?;
debug_assert!(parts.next().is_none());
Ok((Request::ListAllInfo(uri), ""))
} }
fn parse_response( fn into_response_enum(self) -> crate::Response {
parts: ResponseAttributes<'_>, todo!()
) -> Result<Self::Response, ResponseParserError> { }
unimplemented!()
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let result = DbSelectionPrintResponse::parse(parts)?;
Ok(ListAllInfoResponse(result))
}
}
impl Command for ListAllInfo {
type Request = ListAllInfoRequest;
type Response = ListAllInfoResponse;
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::types::{DbDirectoryInfo, DbPlaylistInfo, DbSongInfo, Tag};
use super::*;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_parse_response() {
let response = indoc! {"
directory: albums
Last-Modified: 2024-10-01T10:00:00Z
directory: albums/a
Last-Modified: 2024-10-01T10:00:00Z
file: albums/a/song1.mp3
Last-Modified: 2022-12-31T09:00:00Z
Added: 2021-12-31T09:00:00Z
Format: 44100:16:2
Title: Song A
Artist: Artist A
Album: Album A
AlbumArtist: Artist A
Composer: Artist A
Genre: Pop
Track: 1
Disc: 1
Date: 2020-01-01
Time: 360
duration: 360.123
playlist: albums/a/album a.m3u8
Last-Modified: 2022-12-31T09:00:00Z
OK
"};
let result = ListAllInfo::parse_raw_response(response.as_bytes());
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())
})
])),
)
} }
} }

View File

@@ -1,34 +1,47 @@
use crate::commands::{ use serde::{Deserialize, Serialize};
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError, use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::ResponseAttributes,
types::{DbDirectoryInfo, DbSelectionPrintResponse, Uri},
}; };
pub struct ListFiles; pub struct ListFiles;
// TODO: fix this type single_optional_item_command_request!(ListFiles, "listfiles", Uri);
pub type ListFilesResponse = Vec<String>;
impl Command for ListFiles { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
type Response = ListFilesResponse; pub struct ListFilesResponse(Vec<DbDirectoryInfo>);
const COMMAND: &'static str = "listfiles";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { impl CommandResponse for ListFilesResponse {
let uri = parts fn into_response_enum(self) -> crate::Response {
.next() todo!()
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))
})
.transpose()?;
debug_assert!(parts.next().is_none());
Ok((Request::ListFiles(uri), ""))
} }
fn parse_response( fn from_response_enum(response: crate::Response) -> Option<Self> {
parts: ResponseAttributes<'_>, todo!()
) -> Result<Self::Response, ResponseParserError> { }
unimplemented!()
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
DbSelectionPrintResponse::parse(parts)?
.into_iter()
.map(|i| match i {
DbSelectionPrintResponse::Directory(db_directory_info) => Ok(db_directory_info),
DbSelectionPrintResponse::Song(_db_song_info) => {
Err(ResponseParserError::UnexpectedProperty("file".to_string()))
}
DbSelectionPrintResponse::Playlist(_db_playlist_info) => Err(
ResponseParserError::UnexpectedProperty("playlist".to_string()),
),
})
.collect::<Result<Vec<_>, ResponseParserError>>()
.map(ListFilesResponse)
} }
} }
impl Command for ListFiles {
type Request = ListFilesRequest;
type Response = ListFilesResponse;
}

View File

@@ -1,34 +1,95 @@
use crate::commands::{ use serde::{Deserialize, Serialize};
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError, use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::ResponseAttributes,
types::{DbSelectionPrintResponse, Uri},
}; };
pub struct LsInfo; pub struct LsInfo;
// TODO: fix this type single_optional_item_command_request!(LsInfo, "lsinfo", Uri);
pub type LsInfoResponse = Vec<String>;
impl Command for LsInfo { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
type Response = LsInfoResponse; pub struct LsInfoResponse(Vec<DbSelectionPrintResponse>);
const COMMAND: &'static str = "lsinfo";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { impl CommandResponse for LsInfoResponse {
let uri = parts fn into_response_enum(self) -> crate::Response {
.next() todo!()
.map(|s| {
s.parse()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))
})
.transpose()?;
debug_assert!(parts.next().is_none());
Ok((Request::LsInfo(uri), ""))
} }
fn parse_response( fn from_response_enum(response: crate::Response) -> Option<Self> {
parts: ResponseAttributes<'_>, todo!()
) -> Result<Self::Response, ResponseParserError> { }
unimplemented!()
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let result = DbSelectionPrintResponse::parse(parts)?;
Ok(LsInfoResponse(result))
}
}
impl Command for LsInfo {
type Request = LsInfoRequest;
type Response = LsInfoResponse;
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::types::{DbDirectoryInfo, DbPlaylistInfo};
use super::*;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_parse_response() {
let response = indoc! {"
directory: albums
Last-Modified: 2024-10-01T10:00:00Z
directory: albums/a
Last-Modified: 2024-10-01T10:00:00Z
playlist: albums/a/album a.m3u8
Last-Modified: 2022-12-31T09:00:00Z
directory: albums/b
Last-Modified: 2023-10-01T10:00:00Z
playlist: albums/b/album b.m3u8
Last-Modified: 2021-12-31T09:00:00Z
OK
"};
let result = LsInfo::parse_raw_response(response.as_bytes());
assert_eq!(
result,
Ok(LsInfoResponse(vec![
DbSelectionPrintResponse::Directory(DbDirectoryInfo {
directory: PathBuf::from("albums"),
last_modified: Some("2024-10-01T10:00:00Z".to_string())
}),
DbSelectionPrintResponse::Directory(DbDirectoryInfo {
directory: PathBuf::from("albums/a"),
last_modified: Some("2024-10-01T10:00:00Z".to_string())
}),
DbSelectionPrintResponse::Playlist(DbPlaylistInfo {
playlist: PathBuf::from("albums/a/album a.m3u8"),
last_modified: Some("2022-12-31T09:00:00Z".to_string())
}),
DbSelectionPrintResponse::Directory(DbDirectoryInfo {
directory: PathBuf::from("albums/b"),
last_modified: Some("2023-10-01T10:00:00Z".to_string())
}),
DbSelectionPrintResponse::Playlist(DbPlaylistInfo {
playlist: PathBuf::from("albums/b/album b.m3u8"),
last_modified: Some("2021-12-31T09:00:00Z".to_string())
})
])),
);
} }
} }

View File

@@ -1,42 +1,47 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::commands::{ use serde::{Deserialize, Serialize};
Command, GenericResponseValue, Request, RequestParserError, RequestParserResult,
ResponseAttributes, ResponseParserError, use crate::{
commands::{Command, CommandResponse, ResponseParserError, single_item_command_request},
response_tokenizer::{GenericResponseValue, ResponseAttributes},
types::Uri,
}; };
pub struct ReadComments; pub struct ReadComments;
pub type ReadCommentsResponse = HashMap<String, String>; single_item_command_request!(ReadComments, "readcomments", Uri);
impl Command for ReadComments { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
type Response = ReadCommentsResponse; pub struct ReadCommentsResponse(HashMap<String, String>);
const COMMAND: &'static str = "readcomments";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { impl CommandResponse for ReadCommentsResponse {
let uri = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; fn into_response_enum(self) -> crate::Response {
let uri = uri todo!()
.parse()
.map_err(|_| RequestParserError::SyntaxError(1, uri.to_owned()))?;
debug_assert!(parts.next().is_none());
Ok((Request::ReadComments(uri), ""))
} }
fn parse_response( fn from_response_enum(response: crate::Response) -> Option<Self> {
parts: ResponseAttributes<'_>, todo!()
) -> Result<Self::Response, ResponseParserError> { }
let parts: HashMap<_, _> = parts.into();
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
let comments = parts let comments = parts
.iter() .iter()
.map(|(k, v)| match v { .map(|(k, v)| match v {
GenericResponseValue::Text(s) => Ok((k.to_string(), s.to_string())), GenericResponseValue::Text(s) => Ok((k.to_string(), s.to_string())),
GenericResponseValue::Binary(_) => Err(ResponseParserError::SyntaxError(1, k)), GenericResponseValue::Binary(_) => {
Err(ResponseParserError::SyntaxError(1, k.to_string()))
}
}) })
.collect::<Result<HashMap<_, _>, ResponseParserError>>()?; .collect::<Result<HashMap<_, _>, ResponseParserError>>()?;
Ok(comments) Ok(ReadCommentsResponse(comments))
} }
} }
impl Command for ReadComments {
type Request = ReadCommentsRequest;
type Response = ReadCommentsResponse;
}

View File

@@ -1,51 +1,93 @@
use std::collections::HashMap; use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::{ commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, request_tokenizer::RequestTokenizer,
ResponseParserError, get_and_parse_property, get_optional_property, get_property, response_tokenizer::{
ResponseAttributes, get_and_parse_property, get_optional_property, get_property,
}, },
common::Offset, types::{Offset, Uri},
}; };
pub struct ReadPicture; pub struct ReadPicture;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReadPictureRequest {
pub uri: Uri,
pub offset: Offset,
}
impl CommandRequest for ReadPictureRequest {
const COMMAND: &'static str = "readpicture";
const MIN_ARGS: u32 = 2;
const MAX_ARGS: Option<u32> = Some(2);
fn into_request_enum(self) -> crate::Request {
crate::Request::ReadPicture(self.uri, self.offset)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::ReadPicture(uri, offset) => Some(ReadPictureRequest { uri, offset }),
_ => None,
}
}
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 struct ReadPictureResponse {
pub size: usize, pub size: usize,
pub binary: Vec<u8>, pub binary: Vec<u8>,
pub mimetype: Option<String>, pub mimetype: Option<String>,
} }
impl Command for ReadPicture { impl CommandResponse for ReadPictureResponse {
type Response = Option<ReadPictureResponse>; fn into_response_enum(self) -> crate::Response {
const COMMAND: &'static str = "readpicture"; todo!()
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
let uri = match parts.next() {
Some(s) => s,
None => return Err(RequestParserError::UnexpectedEOF),
};
let offset = match parts.next() {
Some(s) => s
.parse::<Offset>()
.map_err(|_| RequestParserError::SyntaxError(1, s.to_owned()))?,
None => return Err(RequestParserError::UnexpectedEOF),
};
debug_assert!(parts.next().is_none());
Ok((Request::ReadPicture(uri.to_string(), offset), ""))
} }
fn parse_response( fn from_response_enum(response: crate::Response) -> Option<Self> {
parts: ResponseAttributes<'_>, todo!()
) -> Result<Self::Response, ResponseParserError> { }
let parts: HashMap<_, _> = parts.into();
if parts.is_empty() { fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
return Ok(None); let parts: HashMap<_, _> = parts.into_map()?;
}
// TODO: is empty response possible?
// if parts.is_empty() {
// return Err(ResponseParserError::UnexpectedEOF);
// }
let size = get_and_parse_property!(parts, "size", Text); let size = get_and_parse_property!(parts, "size", Text);
@@ -53,10 +95,15 @@ impl Command for ReadPicture {
let mimetype = get_optional_property!(parts, "mimetype", Text).map(|s| s.to_string()); let mimetype = get_optional_property!(parts, "mimetype", Text).map(|s| s.to_string());
Ok(Some(ReadPictureResponse { Ok(ReadPictureResponse {
size, size,
binary, binary,
mimetype, mimetype,
})) })
} }
} }
impl Command for ReadPicture {
type Request = ReadPictureRequest;
type Response = ReadPictureResponse;
}

View File

@@ -1,35 +1,43 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::commands::{ use serde::{Deserialize, Serialize};
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
get_and_parse_property, use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::{ResponseAttributes, get_and_parse_property},
types::Uri,
}; };
pub struct Rescan; pub struct Rescan;
single_optional_item_command_request!(Rescan, "rescan", Uri);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RescanResponse { pub struct RescanResponse {
pub updating_db: usize, pub updating_db: usize,
} }
impl Command for Rescan { impl CommandResponse for RescanResponse {
type Response = RescanResponse; fn into_response_enum(self) -> crate::Response {
const COMMAND: &'static str = "rescan"; todo!()
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( fn from_response_enum(response: crate::Response) -> Option<Self> {
parts: ResponseAttributes<'_>, todo!()
) -> Result<Self::Response, ResponseParserError> { }
let parts: HashMap<_, _> = parts.into();
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
let updating_db = get_and_parse_property!(parts, "updating_db", Text); let updating_db = get_and_parse_property!(parts, "updating_db", Text);
Ok(RescanResponse { updating_db }) Ok(RescanResponse { updating_db })
} }
} }
impl Command for Rescan {
type Request = RescanRequest;
type Response = RescanResponse;
}

View File

@@ -1,51 +1,143 @@
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::{ commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, filter::Filter,
ResponseParserError, request_tokenizer::RequestTokenizer,
}, response_tokenizer::ResponseAttributes,
filter::parse_filter, types::{DbSelectionPrintResponse, DbSongInfo, Sort, WindowRange},
}; };
pub struct Search; pub struct Search;
pub struct SearchResponse {} #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
}
impl Command for Search { impl CommandRequest for SearchRequest {
type Response = SearchResponse;
const COMMAND: &'static str = "search"; const COMMAND: &'static str = "search";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(3);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { fn into_request_enum(self) -> crate::Request {
let filter = parse_filter(&mut parts)?; crate::Request::Search(self.filter, self.sort, self.window)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Search(filter, sort, window) => Some(SearchRequest {
filter,
sort,
window,
}),
_ => None,
}
}
fn serialize(&self) -> String {
let mut cmd = format!("{} {}", Self::COMMAND, self.filter);
if let Some(sort) = &self.sort {
cmd.push_str(&format!(" sort {}", sort));
}
if let Some(window) = &self.window {
cmd.push_str(&format!(" window {}", window));
}
cmd.push('\n');
cmd
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let filter = match parts.next() {
Some(f) => {
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?
}
None => return Err(Self::missing_arguments_error(0)),
};
let mut argument_index_counter = 0;
let mut sort_or_window = parts.next(); let mut sort_or_window = parts.next();
let mut sort = None; let mut sort = None;
if let Some("sort") = sort_or_window { 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( sort = Some(
parts s.parse()
.next() .map_err(|_| RequestParserError::SubtypeParserError {
.ok_or(RequestParserError::UnexpectedEOF)? argument_index: argument_index_counter,
.to_string(), expected_type: "Sort",
raw_input: s.to_string(),
})?,
); );
sort_or_window = parts.next(); sort_or_window = parts.next();
} }
let mut window = None; let mut window = None;
if let Some("window") = sort_or_window { if let Some("window") = sort_or_window {
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
window = Some( window = Some(
w.parse() w.parse()
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?, .map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
); );
} }
debug_assert!(parts.next().is_none()); Self::throw_if_too_many_arguments(parts)?;
Ok((Request::Search(filter, sort, window), "")) Ok(SearchRequest {
} filter,
sort,
fn parse_response( window,
parts: ResponseAttributes<'_>, })
) -> Result<Self::Response, ResponseParserError> {
unimplemented!()
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchResponse(Vec<DbSongInfo>);
impl CommandResponse for SearchResponse {
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
}
fn into_response_enum(self) -> crate::Response {
todo!()
}
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;
}

View File

@@ -1,60 +1,141 @@
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::{ commands::{Command, CommandRequest, RequestParserError, empty_command_response},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, filter::Filter,
ResponseParserError, request_tokenizer::RequestTokenizer,
}, types::{SongPosition, Sort, WindowRange},
filter::parse_filter,
}; };
pub struct SearchAdd; pub struct SearchAdd;
impl Command for SearchAdd { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
type Response = (); pub struct SearchAddRequest {
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
}
impl CommandRequest for SearchAddRequest {
const COMMAND: &'static str = "searchadd"; const COMMAND: &'static str = "searchadd";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(4);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { fn into_request_enum(self) -> crate::Request {
let filter = parse_filter(&mut parts)?; crate::Request::SearchAdd(self.filter, self.sort, self.window, self.position)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::SearchAdd(filter, sort, window, position) => Some(SearchAddRequest {
filter,
sort,
window,
position,
}),
_ => None,
}
}
fn serialize(&self) -> String {
let mut cmd = format!("{} {}", Self::COMMAND, self.filter);
if let Some(sort) = &self.sort {
cmd.push_str(&format!(" sort {}", sort));
}
if let Some(window) = &self.window {
cmd.push_str(&format!(" window {}", window));
}
if let Some(position) = &self.position {
cmd.push_str(&format!(" position {}", position));
}
cmd.push('\n');
cmd
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let filter = match parts.next() {
Some(f) => {
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?
}
None => return Err(Self::missing_arguments_error(0)),
};
let mut argument_index_counter = 0;
let mut sort_or_window_or_position = parts.next(); let mut sort_or_window_or_position = parts.next();
let mut sort = None; let mut sort = None;
if let Some("sort") = sort_or_window_or_position { 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( sort = Some(
parts s.parse()
.next() .map_err(|_| RequestParserError::SubtypeParserError {
.ok_or(RequestParserError::UnexpectedEOF)? argument_index: argument_index_counter,
.to_string(), expected_type: "Sort",
raw_input: s.to_string(),
})?,
); );
sort_or_window_or_position = parts.next(); sort_or_window_or_position = parts.next();
} }
let mut window = None; let mut window = None;
if let Some("window") = sort_or_window_or_position { if let Some("window") = sort_or_window_or_position {
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
window = Some( window = Some(
w.parse() w.parse()
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?, .map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
); );
sort_or_window_or_position = parts.next(); sort_or_window_or_position = parts.next();
} }
let mut position = None; let mut position = None;
if let Some("position") = sort_or_window_or_position { if let Some("position") = sort_or_window_or_position {
let p = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; argument_index_counter += 1;
let p = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "position",
argument_index: argument_index_counter,
})?;
position = Some( position = Some(
p.parse() p.parse()
.map_err(|_| RequestParserError::SyntaxError(0, p.to_string()))?, .map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "SongPosition",
raw_input: p.to_string(),
})?,
); );
} }
debug_assert!(parts.next().is_none()); Self::throw_if_too_many_arguments(parts)?;
Ok((Request::SearchAdd(filter, sort, window, position), "")) Ok(SearchAddRequest {
} filter,
sort,
fn parse_response( window,
parts: ResponseAttributes<'_>, position,
) -> Result<Self::Response, ResponseParserError> { })
debug_assert!(parts.is_empty());
Ok(())
} }
} }
empty_command_response!(SearchAdd);
impl Command for SearchAdd {
type Request = SearchAddRequest;
type Response = SearchAddResponse;
}

View File

@@ -1,68 +1,157 @@
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::{ commands::{Command, CommandRequest, RequestParserError, empty_command_response},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, filter::Filter,
ResponseParserError, request_tokenizer::RequestTokenizer,
}, types::{PlaylistName, SongPosition, Sort, WindowRange},
filter::parse_filter,
}; };
pub struct SearchAddPl; pub struct SearchAddPl;
impl Command for SearchAddPl { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
type Response = (); pub struct SearchAddPlRequest {
const COMMAND: &'static str = "searchaddpl"; playlist_name: PlaylistName,
filter: Filter,
sort: Option<Sort>,
window: Option<WindowRange>,
position: Option<SongPosition>,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { impl CommandRequest for SearchAddPlRequest {
const COMMAND: &'static str = "searchaddpl";
const MIN_ARGS: u32 = 2;
const MAX_ARGS: Option<u32> = Some(5);
fn into_request_enum(self) -> crate::Request {
crate::Request::SearchAddPl(
self.playlist_name,
self.filter,
self.sort,
self.window,
self.position,
)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::SearchAddPl(playlist_name, filter, sort, window, position) => {
Some(SearchAddPlRequest {
playlist_name,
filter,
sort,
window,
position,
})
}
_ => None,
}
}
fn serialize(&self) -> String {
let mut cmd = format!("{} {} {}", Self::COMMAND, self.playlist_name, self.filter);
if let Some(sort) = &self.sort {
cmd.push_str(&format!(" sort {}", sort));
}
if let Some(window) = &self.window {
cmd.push_str(&format!(" window {}", window));
}
if let Some(position) = &self.position {
cmd.push_str(&format!(" position {}", position));
}
cmd.push('\n');
cmd
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let playlist_name = parts let playlist_name = parts
.next() .next()
.ok_or(RequestParserError::UnexpectedEOF)? .ok_or(Self::missing_arguments_error(0))?
.to_string(); .to_string();
let filter = parse_filter(&mut parts)?; let filter = match parts.next() {
Some(f) => {
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?
}
None => return Err(Self::missing_arguments_error(1)),
};
let mut argument_index_counter = 1;
let mut sort_or_window_or_position = parts.next(); let mut sort_or_window_or_position = parts.next();
let mut sort = None; let mut sort = None;
if let Some("sort") = sort_or_window_or_position { 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( sort = Some(
parts s.parse()
.next() .map_err(|_| RequestParserError::SubtypeParserError {
.ok_or(RequestParserError::UnexpectedEOF)? argument_index: argument_index_counter,
.to_string(), expected_type: "Sort",
raw_input: s.to_string(),
})?,
); );
sort_or_window_or_position = parts.next(); sort_or_window_or_position = parts.next();
} }
let mut window = None; let mut window = None;
if let Some("window") = sort_or_window_or_position { if let Some("window") = sort_or_window_or_position {
let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; argument_index_counter += 1;
let w = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "window",
argument_index: argument_index_counter,
})?;
window = Some( window = Some(
w.parse() w.parse()
.map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?, .map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "WindowRange",
raw_input: w.to_string(),
})?,
); );
sort_or_window_or_position = parts.next(); sort_or_window_or_position = parts.next();
} }
let mut position = None; let mut position = None;
if let Some("position") = sort_or_window_or_position { if let Some("position") = sort_or_window_or_position {
let p = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; argument_index_counter += 1;
let p = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "position",
argument_index: argument_index_counter,
})?;
position = Some( position = Some(
p.parse() p.parse()
.map_err(|_| RequestParserError::SyntaxError(0, p.to_string()))?, .map_err(|_| RequestParserError::SubtypeParserError {
argument_index: argument_index_counter,
expected_type: "SongPosition",
raw_input: p.to_string(),
})?,
); );
} }
debug_assert!(parts.next().is_none()); Self::throw_if_too_many_arguments(parts)?;
Ok(( Ok(SearchAddPlRequest {
Request::SearchAddPl(playlist_name, filter, sort, window, position), playlist_name,
"", filter,
)) sort,
} window,
position,
fn parse_response( })
parts: ResponseAttributes<'_>,
) -> Result<Self::Response, ResponseParserError> {
debug_assert!(parts.is_empty());
Ok(())
} }
} }
empty_command_response!(SearchAddPl);
impl Command for SearchAddPl {
type Request = SearchAddPlRequest;
type Response = SearchAddPlResponse;
}

View File

@@ -1,47 +1,101 @@
use std::collections::HashMap; use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::{ commands::{Command, CommandRequest, CommandResponse, RequestParserError, ResponseParserError},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, filter::Filter,
ResponseParserError, get_and_parse_property, request_tokenizer::RequestTokenizer,
}, response_tokenizer::{ResponseAttributes, get_and_parse_property},
filter::parse_filter, types::{GroupType, Seconds},
}; };
pub struct SearchCount; pub struct SearchCount;
pub struct SearchCountResponse { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub songs: usize, pub struct SearchCountRequest {
pub playtime: u64, filter: Filter,
group: Option<GroupType>,
} }
impl Command for SearchCount { impl CommandRequest for SearchCountRequest {
type Response = SearchCountResponse;
const COMMAND: &'static str = "searchcount"; const COMMAND: &'static str = "searchcount";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(2);
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { fn into_request_enum(self) -> crate::Request {
let filter = parse_filter(&mut parts)?; crate::Request::SearchCount(self.filter, self.group)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::SearchCount(filter, group) => {
Some(SearchCountRequest { filter, group })
}
_ => None,
}
}
fn serialize(&self) -> String {
let mut cmd = format!("{} {}", Self::COMMAND, self.filter);
if let Some(group) = &self.group {
cmd.push_str(&format!(" group {}", group));
}
cmd.push('\n');
cmd
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let filter = match parts.next() {
Some(f) => {
Filter::parse(f).map_err(|_| RequestParserError::SyntaxError(1, f.to_owned()))?
}
None => return Err(Self::missing_arguments_error(0)),
};
let group = if let Some("group") = parts.next() { let group = if let Some("group") = parts.next() {
let group = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; let group = parts
.next()
.ok_or(RequestParserError::MissingKeywordValue {
keyword: "group",
argument_index: 1,
})?;
Some( Some(
group group
.parse() .parse()
.map_err(|_| RequestParserError::SyntaxError(1, group.to_owned()))?, .map_err(|_| RequestParserError::SubtypeParserError {
argument_index: 1,
expected_type: "Group",
raw_input: group.to_string(),
})?,
) )
} else { } else {
None None
}; };
debug_assert!(parts.next().is_none()); Self::throw_if_too_many_arguments(parts)?;
Ok((Request::SearchCount(filter, group), "")) Ok(SearchCountRequest { filter, group })
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchCountResponse {
pub songs: usize,
pub playtime: Seconds,
}
impl CommandResponse for SearchCountResponse {
fn from_response_enum(response: crate::Response) -> Option<Self> {
todo!()
} }
fn parse_response( fn into_response_enum(self) -> crate::Response {
parts: ResponseAttributes<'_>, todo!()
) -> Result<Self::Response, ResponseParserError> { }
let parts: HashMap<_, _> = parts.into();
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
let songs = get_and_parse_property!(parts, "songs", Text); let songs = get_and_parse_property!(parts, "songs", Text);
let playtime = get_and_parse_property!(parts, "playtime", Text); let playtime = get_and_parse_property!(parts, "playtime", Text);
@@ -49,3 +103,8 @@ impl Command for SearchCount {
Ok(SearchCountResponse { songs, playtime }) Ok(SearchCountResponse { songs, playtime })
} }
} }
impl Command for SearchCount {
type Request = SearchCountRequest;
type Response = SearchCountResponse;
}

View File

@@ -1,35 +1,43 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::commands::{ use serde::{Deserialize, Serialize};
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
get_and_parse_property, use crate::{
commands::{
Command, CommandResponse, ResponseParserError, single_optional_item_command_request,
},
response_tokenizer::{ResponseAttributes, get_and_parse_property},
types::Uri,
}; };
pub struct Update; pub struct Update;
single_optional_item_command_request!(Update, "update", Uri);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UpdateResponse { pub struct UpdateResponse {
updating_db: usize, updating_db: usize,
} }
impl Command for Update { impl CommandResponse for UpdateResponse {
type Response = UpdateResponse; fn into_response_enum(self) -> crate::Response {
const COMMAND: &'static str = "update"; todo!()
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( fn from_response_enum(response: crate::Response) -> Option<Self> {
parts: ResponseAttributes<'_>, todo!()
) -> Result<Self::Response, ResponseParserError> { }
let parts: HashMap<_, _> = parts.into();
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let parts: HashMap<_, _> = parts.into_map()?;
let updating_db = get_and_parse_property!(parts, "updating_db", Text); let updating_db = get_and_parse_property!(parts, "updating_db", Text);
Ok(UpdateResponse { updating_db }) Ok(UpdateResponse { updating_db })
} }
} }
impl Command for Update {
type Request = UpdateRequest;
type Response = UpdateResponse;
}

View File

@@ -1,11 +1,11 @@
pub mod delpartition; mod delpartition;
pub mod listpartitions; mod listpartitions;
pub mod moveoutput; mod moveoutput;
pub mod newpartition; mod newpartition;
pub mod partition; mod partition;
pub use delpartition::DelPartition; pub use delpartition::*;
pub use listpartitions::ListPartitions; pub use listpartitions::*;
pub use moveoutput::MoveOutput; pub use moveoutput::*;
pub use newpartition::NewPartition; pub use newpartition::*;
pub use partition::Partition; pub use partition::*;

View File

@@ -1,29 +1,15 @@
use crate::commands::{ use crate::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, commands::{Command, empty_command_response, single_item_command_request},
ResponseParserError, types::PartitionName,
}; };
pub struct DelPartition; pub struct DelPartition;
single_item_command_request!(DelPartition, "delpartition", PartitionName);
empty_command_response!(DelPartition);
impl Command for DelPartition { impl Command for DelPartition {
type Response = (); type Request = DelPartitionRequest;
const COMMAND: &'static str = "delpartition"; type Response = DelPartitionResponse;
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(())
}
} }

View File

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

View File

@@ -1,29 +1,12 @@
use crate::commands::{ use crate::commands::{Command, empty_command_response, single_item_command_request};
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct MoveOutput; pub struct MoveOutput;
single_item_command_request!(MoveOutput, "moveoutput", String);
empty_command_response!(MoveOutput);
impl Command for MoveOutput { impl Command for MoveOutput {
type Response = (); type Request = MoveOutputRequest;
const COMMAND: &'static str = "moveoutput"; type Response = MoveOutputResponse;
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(())
}
} }

View File

@@ -1,29 +1,15 @@
use crate::commands::{ use crate::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, commands::{Command, empty_command_response, single_item_command_request},
ResponseParserError, types::PartitionName,
}; };
pub struct NewPartition; pub struct NewPartition;
single_item_command_request!(NewPartition, "newpartition", PartitionName);
empty_command_response!(NewPartition);
impl Command for NewPartition { impl Command for NewPartition {
type Response = (); type Request = NewPartitionRequest;
const COMMAND: &'static str = "newpartition"; type Response = NewPartitionResponse;
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(())
}
} }

View File

@@ -1,29 +1,15 @@
use crate::commands::{ use crate::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, commands::{Command, empty_command_response, single_item_command_request},
ResponseParserError, types::PartitionName,
}; };
pub struct Partition; pub struct Partition;
single_item_command_request!(Partition, "partition", PartitionName);
empty_command_response!(Partition);
impl Command for Partition { impl Command for Partition {
type Response = (); type Request = PartitionRequest;
const COMMAND: &'static str = "partition"; type Response = PartitionResponse;
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(())
}
} }

View File

@@ -1,25 +1,25 @@
pub mod consume; mod consume;
pub mod crossfade; mod crossfade;
pub mod getvol; mod getvol;
pub mod mixrampdb; mod mixrampdb;
pub mod mixrampdelay; mod mixrampdelay;
pub mod random; mod random;
pub mod repeat; mod repeat;
pub mod replay_gain_mode; mod replay_gain_mode;
pub mod replay_gain_status; mod replay_gain_status;
pub mod setvol; mod setvol;
pub mod single; mod single;
pub mod volume; mod volume;
pub use consume::Consume; pub use consume::*;
pub use crossfade::Crossfade; pub use crossfade::*;
pub use getvol::GetVol; pub use getvol::*;
pub use mixrampdb::MixRampDb; pub use mixrampdb::*;
pub use mixrampdelay::MixRampDelay; pub use mixrampdelay::*;
pub use random::Random; pub use random::*;
pub use repeat::Repeat; pub use repeat::*;
pub use replay_gain_mode::ReplayGainMode; pub use replay_gain_mode::*;
pub use replay_gain_status::ReplayGainStatus; pub use replay_gain_status::*;
pub use setvol::SetVol; pub use setvol::*;
pub use single::Single; pub use single::*;
pub use volume::Volume; pub use volume::*;

View File

@@ -1,32 +1,15 @@
use std::str::FromStr; use crate::{
commands::{Command, empty_command_response, single_item_command_request},
use crate::commands::{ types::BoolOrOneshot,
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
}; };
pub struct Consume; pub struct Consume;
single_item_command_request!(Consume, "consume", BoolOrOneshot);
empty_command_response!(Consume);
impl Command for Consume { impl Command for Consume {
type Response = (); type Request = ConsumeRequest;
const COMMAND: &'static str = "consume"; type Response = ConsumeResponse;
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(())
}
} }

View File

@@ -1,34 +1,15 @@
use crate::{ use crate::{
commands::{ commands::{Command, empty_command_response, single_item_command_request},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, types::Seconds,
ResponseParserError,
},
common::Seconds,
}; };
pub struct Crossfade; pub struct Crossfade;
single_item_command_request!(Crossfade, "crossfade", Seconds);
empty_command_response!(Crossfade);
impl Command for Crossfade { impl Command for Crossfade {
type Response = (); type Request = CrossfadeRequest;
const COMMAND: &'static str = "crossfade"; type Response = CrossfadeResponse;
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(())
}
} }

View File

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

View File

@@ -1,31 +1,12 @@
use crate::commands::{ use crate::commands::{Command, empty_command_response, single_item_command_request};
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
};
pub struct MixRampDb; pub struct MixRampDb;
single_item_command_request!(MixRampDb, "mixrampdb", f32);
empty_command_response!(MixRampDb);
impl Command for MixRampDb { impl Command for MixRampDb {
type Response = (); type Request = MixRampDbRequest;
const COMMAND: &'static str = "mixrampdb"; type Response = MixRampDbResponse;
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(())
}
} }

View File

@@ -1,34 +1,14 @@
use crate::{ use crate::{
commands::{ commands::{Command, empty_command_response, single_item_command_request},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, types::Seconds,
ResponseParserError,
},
common::Seconds,
}; };
pub struct MixRampDelay; pub struct MixRampDelay;
single_item_command_request!(MixRampDelay, "mixrampdelay", Seconds);
empty_command_response!(MixRampDelay);
impl Command for MixRampDelay { impl Command for MixRampDelay {
type Response = (); type Request = MixRampDelayRequest;
const COMMAND: &'static str = "mixrampdelay"; type Response = MixRampDelayResponse;
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(())
}
} }

View File

@@ -1,31 +1,56 @@
use crate::commands::{ use crate::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, commands::{Command, CommandRequest, RequestParserError, empty_command_response},
ResponseParserError, request_tokenizer::RequestTokenizer,
}; };
pub struct Random; pub struct Random;
impl Command for Random { pub struct RandomRequest(bool);
type Response = ();
const COMMAND: &'static str = "random";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { impl CommandRequest for RandomRequest {
const COMMAND: &'static str = "random";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(1);
fn into_request_enum(self) -> crate::Request {
crate::Request::Random(self.0)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Random(state) => Some(RandomRequest(state)),
_ => None,
}
}
fn serialize(&self) -> String {
let state = if self.0 { "1" } else { "0" };
format!("{} {}\n", Self::COMMAND, state)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let state = match parts.next() { let state = match parts.next() {
Some("0") => false, Some("0") => false,
Some("1") => true, Some("1") => true,
Some(s) => return Err(RequestParserError::SyntaxError(0, s.to_owned())), Some(s) => {
None => return Err(RequestParserError::UnexpectedEOF), return Err(RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "bool",
raw_input: s.to_owned(),
});
}
None => return Err(Self::missing_arguments_error(0)),
}; };
debug_assert!(parts.next().is_none()); Self::throw_if_too_many_arguments(parts)?;
Ok((Request::Random(state), "")) Ok(RandomRequest(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;
}

View File

@@ -1,31 +1,56 @@
use crate::commands::{ use crate::{
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, commands::{Command, CommandRequest, RequestParserError, empty_command_response},
ResponseParserError, request_tokenizer::RequestTokenizer,
}; };
pub struct Repeat; pub struct Repeat;
impl Command for Repeat { pub struct RepeatRequest(bool);
type Response = ();
const COMMAND: &'static str = "repeat";
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { impl CommandRequest for RepeatRequest {
const COMMAND: &'static str = "repeat";
const MIN_ARGS: u32 = 1;
const MAX_ARGS: Option<u32> = Some(1);
fn into_request_enum(self) -> crate::Request {
crate::Request::Repeat(self.0)
}
fn from_request_enum(request: crate::Request) -> Option<Self> {
match request {
crate::Request::Repeat(state) => Some(RepeatRequest(state)),
_ => None,
}
}
fn serialize(&self) -> String {
let state = if self.0 { "1" } else { "0" };
format!("{} {}\n", Self::COMMAND, state)
}
fn parse(mut parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError> {
let state = match parts.next() { let state = match parts.next() {
Some("0") => false, Some("0") => false,
Some("1") => true, Some("1") => true,
Some(s) => return Err(RequestParserError::SyntaxError(0, s.to_owned())), Some(s) => {
None => return Err(RequestParserError::UnexpectedEOF), return Err(RequestParserError::SubtypeParserError {
argument_index: 0,
expected_type: "bool",
raw_input: s.to_owned(),
});
}
None => return Err(Self::missing_arguments_error(0)),
}; };
debug_assert!(parts.next().is_none()); Self::throw_if_too_many_arguments(parts)?;
Ok((Request::Repeat(state), "")) Ok(RepeatRequest(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;
}

View File

@@ -1,35 +1,15 @@
use std::str::FromStr;
use crate::{ use crate::{
commands::{ commands::{Command, empty_command_response, single_item_command_request},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, types::ReplayGainModeMode,
ResponseParserError,
},
common::ReplayGainModeMode,
}; };
pub struct ReplayGainMode; pub struct ReplayGainMode;
single_item_command_request!(ReplayGainMode, "replay_gain_mode", ReplayGainModeMode);
empty_command_response!(ReplayGainMode);
impl Command for ReplayGainMode { impl Command for ReplayGainMode {
type Response = (); type Request = ReplayGainModeRequest;
const COMMAND: &'static str = "replay_gain_mode"; type Response = ReplayGainModeResponse;
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(())
}
} }

View File

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

View File

@@ -1,35 +1,15 @@
use std::str::FromStr;
use crate::{ use crate::{
commands::{ commands::{Command, empty_command_response, single_item_command_request},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, types::VolumeValue,
ResponseParserError,
},
common::VolumeValue,
}; };
pub struct SetVol; pub struct SetVol;
single_item_command_request!(SetVol, "setvol", VolumeValue);
empty_command_response!(SetVol);
impl Command for SetVol { impl Command for SetVol {
type Response = (); type Request = SetVolRequest;
const COMMAND: &'static str = "setvol"; type Response = SetVolResponse;
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(())
}
} }

View File

@@ -1,32 +1,15 @@
use std::str::FromStr; use crate::{
commands::{Command, empty_command_response, single_item_command_request},
use crate::commands::{ types::BoolOrOneshot,
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes,
ResponseParserError,
}; };
pub struct Single; pub struct Single;
single_item_command_request!(Single, "single", BoolOrOneshot);
empty_command_response!(Single);
impl Command for Single { impl Command for Single {
type Response = (); type Request = SingleRequest;
const COMMAND: &'static str = "single"; type Response = SingleResponse;
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(())
}
} }

View File

@@ -1,35 +1,15 @@
use std::str::FromStr;
use crate::{ use crate::{
commands::{ commands::{Command, empty_command_response, single_item_command_request},
Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, types::VolumeValue,
ResponseParserError,
},
common::VolumeValue,
}; };
pub struct Volume; pub struct Volume;
single_item_command_request!(Volume, "volume", VolumeValue);
empty_command_response!(Volume);
impl Command for Volume { impl Command for Volume {
type Response = (); type Request = VolumeRequest;
const COMMAND: &'static str = "volume"; type Response = VolumeResponse;
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(())
}
} }

View File

@@ -1,11 +1,11 @@
pub mod clearerror; mod clearerror;
pub mod currentsong; mod currentsong;
pub mod idle; mod idle;
pub mod stats; mod stats;
pub mod status; mod status;
pub use clearerror::ClearError; pub use clearerror::*;
pub use currentsong::CurrentSong; pub use currentsong::*;
pub use idle::Idle; pub use idle::*;
pub use stats::Stats; pub use stats::*;
pub use status::Status; pub use status::*;

View File

@@ -1,24 +1,13 @@
use crate::commands::{ use crate::commands::{Command, empty_command_request, empty_command_response};
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError,
};
/// Clears the current error message in status (this is also accomplished by any command that starts playback) /// Clears the current error message in status (this is also accomplished by any command that starts playback)
pub struct ClearError; pub struct ClearError;
empty_command_request!(ClearError, "clearerror");
empty_command_response!(ClearError);
impl Command for ClearError { impl Command for ClearError {
type Response = (); type Request = ClearErrorRequest;
const COMMAND: &'static str = "clearerror"; type Response = ClearErrorResponse;
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(())
}
} }

View File

@@ -1,28 +1,60 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::commands::{ use crate::{
Command, Request, RequestParserResult, ResponseAttributes, ResponseParserError, commands::{Command, CommandResponse, ResponseParserError, empty_command_request},
response_tokenizer::{
ResponseAttributes, get_and_parse_optional_property, get_and_parse_property,
},
types::{DbSongInfo, Priority, SongId, SongPosition},
}; };
/// Displays the song info of the current song (same song that is identified in status) /// Displays the song info of the current song (same song that is identified in status)
pub struct CurrentSong; pub struct CurrentSong;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] empty_command_request!(CurrentSong, "currentsong");
pub struct CurrentSongResponse {}
impl Command for CurrentSong { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
type Response = CurrentSongResponse; pub struct CurrentSongResponse {
const COMMAND: &'static str = "currentsong"; position: SongPosition,
id: SongId,
priority: Option<Priority>,
song_info: DbSongInfo,
}
fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { impl CommandResponse for CurrentSongResponse {
debug_assert!(parts.next().is_none()); fn into_response_enum(self) -> crate::Response {
todo!()
Ok((Request::CurrentSong, ""))
} }
fn parse_response( fn from_response_enum(response: crate::Response) -> Option<Self> {
_parts: ResponseAttributes<'_>, todo!()
) -> Result<Self::Response, ResponseParserError> { }
unimplemented!()
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError> {
let mut parts: HashMap<_, _> = parts.into_map()?;
let position: SongPosition = get_and_parse_property!(parts, "Pos", Text);
let id: SongId = get_and_parse_property!(parts, "Id", Text);
let priority: Option<Priority> = get_and_parse_optional_property!(parts, "Prio", Text);
parts.remove("Pos");
parts.remove("Id");
parts.remove("Prio");
let song_info = DbSongInfo::parse_map(parts)?;
Ok(CurrentSongResponse {
position,
id,
priority,
song_info,
})
} }
} }
impl Command for CurrentSong {
type Request = CurrentSongRequest;
type Response = CurrentSongResponse;
}

View File

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

View File

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

View File

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

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