diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c80aca6..339cadb 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -11,15 +11,24 @@ env: jobs: build: + strategy: + matrix: + rust-version: ["1.70", "stable"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install OpenSSL run: sudo apt-get install libssl-dev + - name: Install Rust + run: rustup default ${{ matrix.rust-version }} + - name: Add Target + run: rustup target add wasm32-unknown-unknown - name: Build run: cargo build --verbose - name: Run tests (All Features) run: cargo test --verbose --all-features - name: Run tests (No Default Features) run: cargo test --verbose --no-default-features + - name: Build (wasm32) + run: cargo build --verbose --target wasm32-unknown-unknown diff --git a/.gitignore b/.gitignore index 6936990..53eaa21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /target **/*.rs.bk -Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..db16ce1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1706 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cc" +version = "1.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.79", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.6.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", + "serde", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mockito" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d10030163d67f681db11810bc486df3149e6d91c8b4f3f96fa8b62b546c2cef8" +dependencies = [ + "assert-json-diff", + "colored", + "difference", + "httparse", + "lazy_static", + "log", + "rand", + "regex", + "serde_json", + "serde_urlencoded", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-socks", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rs621" +version = "0.7.0-alpha1" +dependencies = [ + "chrono", + "derivative", + "either", + "futures", + "gloo-timers", + "itertools", + "mockito", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serde_with", + "thiserror", + "tokio", + "url", + "urlencoding", + "web-time", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "serde_json" +version = "1.0.131" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67d42a0bd4ac281beff598909bb56a86acaf979b84483e1c79c10dcaf98f8cf3" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.6.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a1f0175e03a0973cf4afd476bef05c26e228520400eb1fd473ad417b1c00ffb" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.79", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] diff --git a/Cargo.toml b/Cargo.toml index 11571b7..00f3058 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.7.0-alpha1" authors = ["nasso "] edition = "2018" description = "Rust crate for the E621 API (a large online archive of furry art)." +rust-version = "1.70" repository = "https://github.com/nasso/rs621" readme = "README.md" keywords = ["e621", "e926", "furry", "api", "client"] @@ -33,6 +34,9 @@ derivative = "2" itertools = "0.10" futures = { version = "0.3", default-features = false } reqwest = { version = ">=0.11, <0.13", default-features = false, features = ["json"] } +serde_with = "3.11.0" +serde_repr = "0.1.19" +either = "1.13.0" [dev-dependencies] mockito = "0.30" diff --git a/examples/tags_search.rs b/examples/tags_search.rs new file mode 100644 index 0000000..d983e73 --- /dev/null +++ b/examples/tags_search.rs @@ -0,0 +1,27 @@ +use futures::prelude::*; +use rs621::{ + client::Client, + tag::{Order, Query}, +}; + +#[tokio::main] +async fn main() -> rs621::error::Result<()> { + let client = Client::new("https://e926.net", "MyProject/1.0 (by username on e621)")?; + + println!("Top ten tags by post count!"); + + let result_stream = client + .tag_search(Query::new().per_page(1).order(Order::Count)) + .take(10); + + futures::pin_mut!(result_stream); + + while let Some(tag) = result_stream.next().await { + match tag { + Ok(tag) => println!("- {} with a score of {}", tag.name, tag.post_count), + Err(e) => println!("- couldn't load tag: {}", e), + } + } + + Ok(()) +} diff --git a/src/client.rs b/src/client.rs index 9bbfd66..bd166be 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,21 +15,25 @@ mod rate_limit; #[cfg(feature = "rate-limit")] const REQ_COOLDOWN_DURATION: std::time::Duration = std::time::Duration::from_millis(600); +use crate::error::{Error, Result}; + use futures::Future; + +use reqwest::header::HeaderMap; use reqwest::{Response, Url}; + use serde::Serialize; -use { - super::error::{Error, Result}, - reqwest::header::HeaderMap, -}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; -#[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))] +use std::{fmt, num::ParseIntError, str::FromStr}; + +#[cfg(target_family = "wasm")] fn create_header_map>(_user_agent: T) -> Result { Ok(HeaderMap::new()) } -#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))] +#[cfg(not(target_family = "wasm"))] fn create_header_map>(user_agent: T) -> Result { if user_agent.as_ref() == b"" { Err(Error::CannotCreateClient(String::from( @@ -47,12 +51,12 @@ fn create_header_map>(user_agent: T) -> Result { } } -#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))] +#[cfg(not(target_family = "wasm"))] fn create_extra_query>(_user_agent: T) -> Result> { Ok(Default::default()) } -#[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))] +#[cfg(target_family = "wasm")] fn create_extra_query>(user_agent: T) -> Result> { let value = std::str::from_utf8(user_agent.as_ref()) .map_err(|e| Error::InvalidHeaderValue(format!("{}", e)))?; @@ -60,12 +64,49 @@ fn create_extra_query>(user_agent: T) -> Result> + Send>; -#[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))] +#[cfg(target_family = "wasm")] pub(crate) type QueryFuture = Box>>; +/// Where to begin returning results from in paginated requests. +#[derive(Debug, PartialEq, Eq, Clone, Copy, SerializeDisplay, DeserializeFromStr)] +pub enum Cursor { + /// Begin at the given page. Actual offset depends on page size. + Page(u64), + + /// Return page size items ordered before the given id. + Before(u64), + + /// Return page size items ordered after the given id. + After(u64), +} + +impl FromStr for Cursor { + type Err = ParseIntError; + + fn from_str(s: &str) -> std::result::Result { + let result = match s.chars().next() { + Some('a') => Self::After(s[1..].parse()?), + Some('b') => Self::Before(s[1..].parse()?), + None | Some(_) => Self::Page(s.parse()?), + }; + + Ok(result) + } +} + +impl fmt::Display for Cursor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Page(p) => write!(f, "{p}"), + Self::Before(p) => write!(f, "b{p}"), + Self::After(p) => write!(f, "a{p}"), + } + } +} + /// Client struct. #[derive(Debug)] pub struct Client { @@ -81,10 +122,10 @@ impl Client { fn create(url: &str, user_agent: impl AsRef<[u8]>, proxy: Option<&str>) -> Result { let client = reqwest::Client::builder(); let client = match proxy { - #[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))] + #[cfg(target_family = "wasm")] Some(_) => panic!("proxies are not supported in wasm"), - #[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))] + #[cfg(not(target_family = "wasm"))] Some(proxy) => { let proxy = reqwest::Proxy::https(proxy) .map_err(|e| Error::CannotCreateClient(format!("{}", e)))?; @@ -192,11 +233,7 @@ impl Client { where T: serde::Serialize, { - self.post_response(endpoint, body) - .await? - .json() - .await - .map_err(|e| Error::Serial(format!("{e}"))) + Ok(self.post_response(endpoint, body).await?.json().await?) } pub(crate) async fn delete(&self, endpoint: &str) -> Result<()> { @@ -212,6 +249,40 @@ impl Client { Ok(()) } + pub(crate) async fn get_json_endpoint_query(&self, endpoint: &str, query: &T) -> Result + where + T: serde::Serialize, + R: serde::de::DeserializeOwned, + { + let url = self.url(endpoint)?; + let future = self + .client + .get(url.clone()) + .query(query) + .headers(self.headers.clone()) + .send(); + + let res = self + .rate_limit + .clone() + .check(future) + .await + .map_err(|x| Error::CannotSendRequest(x.to_string()))?; + + if res.status().is_success() { + Ok(res.json().await?) + } else { + Err(Error::Http { + url: res.url().clone(), + code: res.status().as_u16(), + reason: match res.json::().await { + Ok(v) => v["reason"].as_str().map(ToString::to_string), + Err(_) => None, + }, + }) + } + } + pub fn get_json_endpoint( &self, endpoint: &str, @@ -227,9 +298,7 @@ impl Client { .map_err(|e| Error::CannotSendRequest(format!("{}", e)))?; if res.status().is_success() { - res.json() - .await - .map_err(|e| Error::Serial(format!("{}", e))) + Ok(res.json().await?) } else { Err(Error::Http { url: url?, @@ -260,15 +329,21 @@ mod tests { .create(); let server_url = Url::parse(&mockito::server_url()).unwrap(); - - assert_eq!( - client.get_json_endpoint("/post/show.json?id=8595").await, - Err(crate::error::Error::Http { - url: server_url.join("/post/show.json?id=8595").unwrap(), + let err = client + .get_json_endpoint("/post/show.json?id=8595") + .await + .unwrap_err(); + match err { + crate::error::Error::Http { + url, code: 500, - reason: Some(String::from("foo")) - }) - ); + reason, + } => { + assert_eq!(url, server_url.join("/post/show.json?id=8595").unwrap()); + assert_eq!(reason, Some(String::from("foo"))); + } + _ => panic!("expected Http error"), + } } #[tokio::test] @@ -280,15 +355,80 @@ mod tests { .create(); assert_eq!( - client.get_json_endpoint("/post/show.json?id=8595").await, - Ok({ + client + .get_json_endpoint("/post/show.json?id=8595") + .await + .unwrap() + .as_object() + .unwrap(), + &{ let mut m = serde_json::Map::new(); m.insert(String::from("dummy"), "json".into()); - m.into() - }) + m + } + ); + } + + #[tokio::test] + async fn get_json_endpoint_query_success() { + #[derive(Serialize)] + struct Query { + id: u64, + } + + let client = Client::new(&mockito::server_url(), b"rs621/unit_test").unwrap(); + + let _m = mock("GET", "/post/show.json?id=8595") + .with_body(r#"{"dummy":"json"}"#) + .create(); + + let query = Query { id: 8595 }; + assert_eq!( + client + .get_json_endpoint_query::<_, serde_json::Value>("/post/show.json", &query) + .await + .unwrap(), + serde_json::json!({ + "dummy": "json", + }), ); } + #[tokio::test] + async fn get_json_endpoint_query_http_error() { + #[derive(Serialize)] + struct Query { + id: u64, + } + + let client = Client::new(&mockito::server_url(), b"rs621/unit_test").unwrap(); + + // note: these are still using old endpoint but it doesn't matter here + let _m = mock("GET", "/post/show.json?id=8595") + .with_status(500) + .with_body(r#"{"success":false,"reason":"foo"}"#) + .create(); + + let server_url = Url::parse(&mockito::server_url()).unwrap(); + + let query = Query { id: 8595 }; + let err = client + .get_json_endpoint_query::<_, serde_json::Value>("/post/show.json", &query) + .await + .unwrap_err(); + match err { + crate::error::Error::Http { + url, + code: 500, + reason, + } => { + assert_eq!(url, server_url.join("/post/show.json?id=8595").unwrap()); + assert_eq!(reason, Some(String::from("foo"))); + } + _ => panic!("expected Http error"), + } + } + #[tokio::test] async fn create_client_with_proxy_works() { assert!(Client::with_proxy( diff --git a/src/client/tokio_rate_limit.rs b/src/client/tokio_rate_limit.rs index 1a63a70..05dd9e2 100644 --- a/src/client/tokio_rate_limit.rs +++ b/src/client/tokio_rate_limit.rs @@ -24,7 +24,7 @@ impl<'a> Drop for Guard<'a> { } impl RateLimit { - async fn lock(&self) -> Guard { + async fn lock(&self) -> Guard<'_> { loop { let now = Instant::now(); diff --git a/src/error.rs b/src/error.rs index ffc788d..7750987 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,7 @@ use url::Url; -#[derive(Debug, thiserror::Error, PartialEq)] +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] pub enum Error { #[error("Request to {url} returned HTTP code {code} (reason: {reason:?})")] Http { @@ -9,8 +10,11 @@ pub enum Error { reason: Option, }, - #[error("Serialization error: {0}")] - Serial(String), + #[error("reqwest error: {0}")] + Reqwest(#[from] reqwest::Error), + + #[error("json error: {0}")] + Json(#[from] serde_json::Error), #[error("Couldn't send request: {0}")] CannotSendRequest(String), diff --git a/src/lib.rs b/src/lib.rs index 290a883..b7ac81e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,3 +101,6 @@ pub mod post; /// Pool management. pub mod pool; + +/// Tag management. +pub mod tag; diff --git a/src/mocked/favorite.json b/src/mocked/favorite.json index 4299ae6..1238dad 100644 --- a/src/mocked/favorite.json +++ b/src/mocked/favorite.json @@ -1 +1 @@ -{"post":{"id":3758515,"created_at":"2022-12-18T20:00:05.288-05:00","updated_at":"2022-12-18T20:50:05.612-05:00","file":{"width":442,"height":441,"ext":"jpg","size":40532,"md5":"e7b5c53e621dcf21d364a00779db4fe8","url":"https://static1.e621.net/data/e7/b5/e7b5c53e621dcf21d364a00779db4fe8.jpg"},"preview":{"width":150,"height":149,"url":"https://static1.e621.net/data/preview/e7/b5/e7b5c53e621dcf21d364a00779db4fe8.jpg"},"sample":{"has":false,"height":441,"width":442,"url":"https://static1.e621.net/data/e7/b5/e7b5c53e621dcf21d364a00779db4fe8.jpg","alternates":{}},"score":{"up":1,"down":0,"total":1},"tags":{"general":["antennae_(anatomy)","anthro","black_sclera","clothing","fur","green_eyes","grey_clothing","hair","happy","looking_at_viewer","neck_tuft","one_eye_closed","open_mouth","smile","solo","tongue","tuft","white_body","white_clothing","white_fur","white_hair","wink"],"species":["alien","humanoid"],"character":["zyneru_(character)"],"copyright":[],"artist":["zyneru"],"invalid":[],"lore":[],"meta":["low_res"]},"locked_tags":[],"change_seq":44178850,"flags":{"pending":true,"flagged":false,"note_locked":false,"status_locked":false,"rating_locked":false,"comment_disabled":false,"deleted":false},"rating":"s","fav_count":5,"sources":["https://twitter.com/zyneru/status/1568452817010200576","https://pbs.twimg.com/media/FcRDhicaIAEgmsp?format=jpg\u0026name=orig"],"pools":[],"relationships":{"parent_id":null,"has_children":false,"has_active_children":false,"children":[]},"approver_id":null,"uploader_id":317610,"description":"h4. doing an ama for 20k special!\r\nIf you have questions on my ocs and lore clarification or other general questions this is your chance!\r\n\r\nalso THANK YOU AGAIN FOR 20K FOLLOWERS!!! Really appreciate all your support \u003c333","comment_count":0,"is_favorited":true,"has_notes":false,"duration":null}} \ No newline at end of file +{"favorite_count":158,"post_id":3758515} diff --git a/src/mocked/tags_empty.json b/src/mocked/tags_empty.json new file mode 100644 index 0000000..f4188d8 --- /dev/null +++ b/src/mocked/tags_empty.json @@ -0,0 +1 @@ +{"tags":[]} \ No newline at end of file diff --git a/src/mocked/tags_limit-2_page-a12047.json b/src/mocked/tags_limit-2_page-a12047.json new file mode 100644 index 0000000..61d29ff --- /dev/null +++ b/src/mocked/tags_limit-2_page-a12047.json @@ -0,0 +1 @@ +[{"id":12051,"name":"kash","post_count":5,"related_tags":"lagomorph 5 mammal 5 kash 5 male 5 leporid 4 rabbit 4 anthro 4 solo 4 balls 3 long_ears 3 dude 3 peanut 3 hair 3 purple_hair 2 affection 2 blue_countershading 2 gentle_taps 2 fur 2 mutt 2 biped 2 backpack 2 blue_markings 2 countershading 2 markings 2 challen 2","related_tags_updated_at":"2020-03-04T21:55:37.812-05:00","category":4,"is_locked":false,"created_at":"2020-03-05T05:49:37.994-05:00","updated_at":"2020-03-05T05:49:37.994-05:00"},{"id":12048,"name":"fish_bowl","post_count":147,"related_tags":"aquarium 108 fish_bowl 108 vivarium 108 mammal 76 marine 71 fish 69 hi_res 56 clothing 50 feral 49 male 49 female 48 water 48 fur 47 text 44 hair 42 ambiguous_gender 40 anthro 40 group 38 english_text 36 smile 36 simple_background 35 felid 34 open_mouth 34 solo 34 clothed 32","related_tags_updated_at":"2021-12-11T15:59:15.118-05:00","category":0,"is_locked":false,"created_at":"2020-03-05T05:49:37.994-05:00","updated_at":"2021-12-11T15:59:15.118-05:00"}] \ No newline at end of file diff --git a/src/mocked/tags_limit-2_page-a12051.json b/src/mocked/tags_limit-2_page-a12051.json new file mode 100644 index 0000000..4c45608 --- /dev/null +++ b/src/mocked/tags_limit-2_page-a12051.json @@ -0,0 +1 @@ +[{"id":12053,"name":"sefeiren","post_count":1581,"related_tags":"conditional_dnp 300 sefeiren 300 female 222 male 211 claws 176 anthro 173 gentle_taps 165 mammal 165 paws 161 hindpaw 151 hi_res 150 feral 149 fur 142 tongue 142 dude 131 duo 122 open_mouth 122 scalie 117 possibly_druids 116 frisky_ferals 113 horn 112 solo 112 peanut 108 text 104 blush 101","related_tags_updated_at":"2022-04-06T06:21:26.712-04:00","category":1,"is_locked":false,"created_at":"2020-03-05T05:49:37.994-05:00","updated_at":"2022-04-06T06:21:26.713-04:00"},{"id":12052,"name":"skee","post_count":2,"related_tags":"male 2 solo 2 skee 2 simple_background 2 shorts 1 bulge 1 clothing 1 genuine_outline 1 gentle_taps 1 hi_res 1 huge_balls 1 huge_peanut 1 hyper 1 hyper_balls 1 hyper_melancholia 1 hyper_peanut 1 limitedvision 1 lizard 1 anthro 1 multi_melancholia 1 multi_peanut 1 peanut 1 peanut_outline 1 reptile 1 scalie 1","related_tags_updated_at":"2020-02-24T21:51:39.390-05:00","category":1,"is_locked":false,"created_at":"2020-03-05T05:49:37.994-05:00","updated_at":"2020-03-05T05:49:37.994-05:00"}] \ No newline at end of file diff --git a/src/mocked/tags_limit-2_page-b12052.json b/src/mocked/tags_limit-2_page-b12052.json new file mode 100644 index 0000000..61d29ff --- /dev/null +++ b/src/mocked/tags_limit-2_page-b12052.json @@ -0,0 +1 @@ +[{"id":12051,"name":"kash","post_count":5,"related_tags":"lagomorph 5 mammal 5 kash 5 male 5 leporid 4 rabbit 4 anthro 4 solo 4 balls 3 long_ears 3 dude 3 peanut 3 hair 3 purple_hair 2 affection 2 blue_countershading 2 gentle_taps 2 fur 2 mutt 2 biped 2 backpack 2 blue_markings 2 countershading 2 markings 2 challen 2","related_tags_updated_at":"2020-03-04T21:55:37.812-05:00","category":4,"is_locked":false,"created_at":"2020-03-05T05:49:37.994-05:00","updated_at":"2020-03-05T05:49:37.994-05:00"},{"id":12048,"name":"fish_bowl","post_count":147,"related_tags":"aquarium 108 fish_bowl 108 vivarium 108 mammal 76 marine 71 fish 69 hi_res 56 clothing 50 feral 49 male 49 female 48 water 48 fur 47 text 44 hair 42 ambiguous_gender 40 anthro 40 group 38 english_text 36 smile 36 simple_background 35 felid 34 open_mouth 34 solo 34 clothed 32","related_tags_updated_at":"2021-12-11T15:59:15.118-05:00","category":0,"is_locked":false,"created_at":"2020-03-05T05:49:37.994-05:00","updated_at":"2021-12-11T15:59:15.118-05:00"}] \ No newline at end of file diff --git a/src/mocked/tags_limit-2_page-b12054.json b/src/mocked/tags_limit-2_page-b12054.json new file mode 100644 index 0000000..4c45608 --- /dev/null +++ b/src/mocked/tags_limit-2_page-b12054.json @@ -0,0 +1 @@ +[{"id":12053,"name":"sefeiren","post_count":1581,"related_tags":"conditional_dnp 300 sefeiren 300 female 222 male 211 claws 176 anthro 173 gentle_taps 165 mammal 165 paws 161 hindpaw 151 hi_res 150 feral 149 fur 142 tongue 142 dude 131 duo 122 open_mouth 122 scalie 117 possibly_druids 116 frisky_ferals 113 horn 112 solo 112 peanut 108 text 104 blush 101","related_tags_updated_at":"2022-04-06T06:21:26.712-04:00","category":1,"is_locked":false,"created_at":"2020-03-05T05:49:37.994-05:00","updated_at":"2022-04-06T06:21:26.713-04:00"},{"id":12052,"name":"skee","post_count":2,"related_tags":"male 2 solo 2 skee 2 simple_background 2 shorts 1 bulge 1 clothing 1 genuine_outline 1 gentle_taps 1 hi_res 1 huge_balls 1 huge_peanut 1 hyper 1 hyper_balls 1 hyper_melancholia 1 hyper_peanut 1 limitedvision 1 lizard 1 anthro 1 multi_melancholia 1 multi_peanut 1 peanut 1 peanut_outline 1 reptile 1 scalie 1","related_tags_updated_at":"2020-02-24T21:51:39.390-05:00","category":1,"is_locked":false,"created_at":"2020-03-05T05:49:37.994-05:00","updated_at":"2020-03-05T05:49:37.994-05:00"}] \ No newline at end of file diff --git a/src/mocked/tags_order-count_limit-1_page-1.json b/src/mocked/tags_order-count_limit-1_page-1.json new file mode 100644 index 0000000..0488e2f --- /dev/null +++ b/src/mocked/tags_order-count_limit-1_page-1.json @@ -0,0 +1 @@ +[{"id":12054,"name":"mammal","post_count":3350829,"related_tags":"mammal 300 anthro 224 hi_res 212 female 185 male 162 solo 147 gentle_taps 146 fur 137 hair 130 balloons 125 clothing 122 duo 111 canid 109 canine 107 peanut 101 skittles 99 possibly_druids 98 digital_media_(artwork) 89 simple_background 87 dude 84 tail 77 text 77 absurd_res 73 genuine_druids 72 blush 70","related_tags_updated_at":"2024-10-12T17:22:13.554-04:00","category":5,"is_locked":false,"created_at":"2020-03-05T05:49:37.994-05:00","updated_at":"2024-10-12T17:22:13.554-04:00"}] \ No newline at end of file diff --git a/src/mocked/tags_order-count_limit-1_page-2.json b/src/mocked/tags_order-count_limit-1_page-2.json new file mode 100644 index 0000000..7a96a76 --- /dev/null +++ b/src/mocked/tags_order-count_limit-1_page-2.json @@ -0,0 +1 @@ +[{"id":7115,"name":"anthro","post_count":3288365,"related_tags":"anthro 300 mammal 236 hi_res 211 male 189 solo 165 female 162 clothing 159 gentle_taps 152 balloons 130 fur 118 hair 114 peanut 112 duo 107 possibly_druids 105 skittles 104 tail 100 simple_background 98 clothed 91 dude 90 canid 89 canine 88 digital_media_(artwork) 87 genuine_druids 82 balls 79 mutt 77","related_tags_updated_at":"2024-10-11T10:06:56.303-04:00","category":0,"is_locked":false,"created_at":"2020-03-05T05:49:37.994-05:00","updated_at":"2024-10-11T10:06:56.303-04:00"}] \ No newline at end of file diff --git a/src/pool.rs b/src/pool.rs index 4d0eae5..1c0d09a 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -1,5 +1,3 @@ -use crate::error::Error; - use { super::{ client::{Client, QueryFuture}, @@ -251,7 +249,7 @@ impl<'a> Stream for PoolStream<'a> { Ok(res) => { res.into_iter().rev().map(|pool| Ok(pool)).collect() } - Err(e) => vec![Err(Error::Serial(format!("{}", e)))], + Err(e) => vec![Err(e.into())], }; // mark the stream as ended if there was no pools @@ -345,13 +343,10 @@ mod tests { async fn pool_search() { let client = Client::new(&mockito::server_url(), b"rs621/unit_test").unwrap(); - let expected: Vec> = serde_json::from_str::( - include_str!("mocked/pool_search-foo.json"), - ) - .unwrap() - .into_iter() - .map(|x| Ok(x)) - .collect(); + let expected: Vec = serde_json::from_str::(include_str!( + "mocked/pool_search-foo.json" + )) + .unwrap(); let _m = [ mock("GET", "/pools.json?page=1&search%5Bname_matches%5D=foo") @@ -364,10 +359,11 @@ mod tests { ]; // Should all contain foo in the name - let pools: Vec> = client + let pools: Vec = client .pool_search(PoolSearch::new().name_matches("foo")) - .collect() - .await; + .try_collect() + .await + .unwrap(); assert_eq!(pools, expected); } diff --git a/src/post.rs b/src/post.rs index c2464c5..714592c 100644 --- a/src/post.rs +++ b/src/post.rs @@ -2,7 +2,7 @@ use crate::error::Error; use { super::{ - client::{Client, QueryFuture}, + client::{Client, Cursor, QueryFuture}, error::Result as Rs621Result, }, chrono::{offset::Utc, DateTime}, @@ -22,18 +22,31 @@ use { /// Chunk size used for iterators performing requests const ITER_CHUNK_SIZE: u64 = 320; -#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +#[serde(from = "String")] +#[non_exhaustive] pub enum PostFileExtension { - #[serde(rename = "jpg")] Jpeg, - #[serde(rename = "png")] Png, - #[serde(rename = "gif")] Gif, - #[serde(rename = "swf")] Swf, - #[serde(rename = "webm")] WebM, + Mp4, + Other(String), +} + +impl From for PostFileExtension { + fn from(value: String) -> Self { + match value.as_str() { + "jpg" => Self::Jpeg, + "png" => Self::Png, + "gif" => Self::Gif, + "swf" => Self::Swf, + "webm" => Self::WebM, + "mp4" => Self::Mp4, + _ => Self::Other(value), + } + } } #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] @@ -191,14 +204,16 @@ pub struct Post { pub is_favorited: bool, } -#[derive(Debug, PartialEq, Eq, Deserialize)] -struct PostListApiResponse { - pub posts: Vec, +#[derive(Debug, PartialEq, Eq, Deserialize, Clone)] +#[non_exhaustive] +pub struct PostFavorite { + pub post_id: u64, + pub favorite_count: u64, } #[derive(Debug, PartialEq, Eq, Deserialize)] -struct PostShowApiResponse { - pub post: Post, +struct PostListApiResponse { + pub posts: Vec, } fn nullable_bool_from_json<'de, D>(de: D) -> Result @@ -250,13 +265,6 @@ where } } -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum SearchPage { - Page(u64), - BeforePost(u64), - AfterPost(u64), -} - /// Iterator returning posts from a search query. #[derive(Derivative)] #[derivative(Debug)] @@ -269,13 +277,13 @@ pub struct PostSearchStream<'a> { #[derivative(Debug = "ignore")] query_future: Option>, - next_page: SearchPage, + next_page: Cursor, chunk: Vec>, ended: bool, } impl<'a> PostSearchStream<'a> { - fn new>(client: &'a Client, query: T, page: SearchPage) -> Self { + fn new>(client: &'a Client, query: T, page: Cursor) -> Self { PostSearchStream { client: client, query: query.into(), @@ -321,7 +329,7 @@ impl<'a> Stream for PostSearchStream<'a> { .rev() .map(|post| Ok(post)) .collect(), - Err(e) => vec![Err(Error::Serial(format!("{}", e)))], + Err(e) => vec![Err(e.into())], }; let last_id = match this.chunk.first() { @@ -332,16 +340,14 @@ impl<'a> Stream for PostSearchStream<'a> { // we now know what will be the next page this.next_page = if this.query.ordered { match this.next_page { - SearchPage::Page(i) => SearchPage::Page(i + 1), - _ => SearchPage::Page(1), + Cursor::Page(i) => Cursor::Page(i + 1), + _ => Cursor::Page(1), } } else { match this.next_page { - SearchPage::Page(_) => SearchPage::BeforePost(last_id), - SearchPage::BeforePost(_) => { - SearchPage::BeforePost(last_id) - } - SearchPage::AfterPost(_) => SearchPage::AfterPost(last_id), + Cursor::Page(_) => Cursor::Before(last_id), + Cursor::Before(_) => Cursor::Before(last_id), + Cursor::After(_) => Cursor::After(last_id), } }; @@ -386,9 +392,9 @@ impl<'a> Stream for PostSearchStream<'a> { "/posts.json?limit={}&page={}&tags={}", ITER_CHUNK_SIZE, match this.next_page { - SearchPage::Page(i) => format!("{}", i), - SearchPage::BeforePost(i) => format!("b{}", i), - SearchPage::AfterPost(i) => format!("a{}", i), + Cursor::Page(i) => format!("{i}"), + Cursor::Before(i) => format!("b{i}"), + Cursor::After(i) => format!("a{i}"), }, this.query.url_encoded_tags ); @@ -475,7 +481,7 @@ where .rev() .map(|post| Ok(post)) .collect(), - Err(e) => vec![Err(Error::Serial(format!("{}", e)))], + Err(e) => vec![Err(e.into())], }; QueryPollRes::NotFetching @@ -569,7 +575,7 @@ impl Client { /// # Ok(()) } /// ``` pub fn post_search<'a, T: Into>(&'a self, tags: T) -> PostSearchStream<'a> { - self.post_search_from_page(tags, SearchPage::Page(1)) + self.post_search_from_page(tags, Cursor::Page(1)) } /// Returns a Stream over all the posts matching the search query, starting from the given page. @@ -579,13 +585,13 @@ impl Client { /// # rs621::{client::Client, post::PostRating}, /// # futures::prelude::*, /// # }; - /// use rs621::post::SearchPage; + /// use rs621::client::Cursor; /// # #[tokio::main] /// # async fn main() -> rs621::error::Result<()> { /// let client = Client::new("https://e926.net", "MyProject/1.0 (by username on e621)")?; /// /// let mut post_stream = client - /// .post_search_from_page(&["fluffy", "rating:s"][..], SearchPage::BeforePost(123456)) + /// .post_search_from_page(&["fluffy", "rating:s"][..], Cursor::Before(123456)) /// .take(3); /// /// while let Some(post) = post_stream.next().await { @@ -598,7 +604,7 @@ impl Client { pub fn post_search_from_page<'a, T: Into>( &'a self, tags: T, - page: SearchPage, + page: Cursor, ) -> PostSearchStream<'a> { PostSearchStream::new(self, tags, page) } @@ -614,11 +620,11 @@ impl Client { /// # async fn main() -> rs621::error::Result<()> { /// let client = Client::new("https://e926.net", "MyProject/1.0 (by username on e621)")?; /// - /// let post = client.post_favorite(1234).await?; - /// assert_eq!(post.id, 1234); + /// let response = client.post_favorite(1234).await?; + /// assert_eq!(response.post_id, 1234); /// # Ok(()) } /// ``` - pub async fn post_favorite(&self, id: u64) -> Result { + pub async fn post_favorite(&self, id: u64) -> Result { #[derive(Serialize)] struct Form { post_id: u64, @@ -628,13 +634,7 @@ impl Client { .post_form("/favorites.json", &Form { post_id: id }) .await?; - let value = response - .as_object() - .and_then(|o| o.get("post")) - .cloned() - .ok_or_else(|| Error::Serial("unexpected response".into()))?; - - serde_json::from_value(value).map_err(|e| Error::Serial(format!("{}", e))) + Ok(serde_json::from_value(response)?) } /// Mark a [`Post`] (identified by `id`) as no longer particularly liked. @@ -698,7 +698,7 @@ impl Client { ) .await?; - serde_json::from_value(response).map_err(|e| Error::Serial(format!("{}", e))) + Ok(serde_json::from_value(response)?) } } @@ -750,19 +750,10 @@ mod tests { .with_body(include_str!("mocked/favorite.json")) .create(); - let expected = - serde_json::from_str::(include_str!("mocked/favorite.json")) - .unwrap() - .as_object() - .unwrap() - .get("post") - .cloned() - .unwrap(); + let expected: PostFavorite = + serde_json::from_str(include_str!("mocked/favorite.json")).unwrap(); - assert_eq!( - client.post_favorite(3758515).await.unwrap(), - serde_json::from_value(expected).unwrap(), - ); + assert_eq!(client.post_favorite(3758515).await.unwrap(), expected,); } #[tokio::test] @@ -802,8 +793,9 @@ mod tests { client .post_search(query) .take(100) - .collect::>() - .await, + .try_collect::>() + .await + .unwrap(), serde_json::from_str::(include_str!( "mocked/320_page-1_fluffy_rating-s_order-score.json" )) @@ -811,7 +803,6 @@ mod tests { .posts .into_iter() .take(100) - .map(|x| Ok(x)) .collect::>() ); } @@ -851,8 +842,9 @@ mod tests { client .post_search(query) .take(400) - .collect::>() - .await, + .try_collect::>() + .await + .unwrap(), serde_json::from_str::(PAGES[0]) .unwrap() .posts @@ -864,7 +856,6 @@ mod tests { .into_iter() ) .take(400) - .map(|x| Ok(x)) .collect::>() ); } @@ -876,7 +867,7 @@ mod tests { let query = Query::from(&["fluffy", "rating:s"][..]); let response_json = include_str!("mocked/320_fluffy_rating-s_before-2269211.json"); let response: PostListApiResponse = serde_json::from_str(response_json).unwrap(); - let expected: Vec<_> = response.posts.into_iter().take(80).map(|x| Ok(x)).collect(); + let expected: Vec<_> = response.posts.into_iter().take(80).collect(); let _m = mock( "GET", @@ -890,10 +881,11 @@ mod tests { assert_eq!( client - .post_search_from_page(query, SearchPage::BeforePost(2269211)) + .post_search_from_page(query, Cursor::Before(2269211)) .take(80) - .collect::>() - .await, + .try_collect::>() + .await + .unwrap(), expected ); } @@ -918,7 +910,6 @@ mod tests { .into_iter() .chain(responses[1].take().unwrap().posts.into_iter()) .take(400) - .map(|x| Ok(x)) .collect(); let _m = [ @@ -946,8 +937,9 @@ mod tests { client .post_search(query) .take(400) - .collect::>() - .await, + .try_collect::>() + .await + .unwrap(), expected ); } @@ -970,7 +962,12 @@ mod tests { .create(); assert_eq!( - client.post_search(query).take(5).collect::>().await, + client + .post_search(query) + .take(5) + .try_collect::>() + .await + .unwrap(), vec![] ); } @@ -982,7 +979,7 @@ mod tests { let query = Query::from(&["fluffy", "rating:s"][..]); let response_json = include_str!("mocked/320_fluffy_rating-s.json"); let response: PostListApiResponse = serde_json::from_str(response_json).unwrap(); - let expected: Vec<_> = response.posts.into_iter().take(5).map(|x| Ok(x)).collect(); + let expected: Vec<_> = response.posts.into_iter().take(5).collect(); let _m = mock( "GET", @@ -995,7 +992,12 @@ mod tests { .create(); assert_eq!( - client.post_search(query).take(5).collect::>().await, + client + .post_search(query) + .take(5) + .try_collect::>() + .await + .unwrap(), expected ); } @@ -1015,9 +1017,10 @@ mod tests { assert_eq!( client .get_posts(&[8595, 535, 2105, 1470]) - .collect::>() - .await, - expected.into_iter().map(|p| Ok(p)).collect::>(), + .try_collect::>() + .await + .unwrap(), + expected, ); } } diff --git a/src/tag.rs b/src/tag.rs new file mode 100644 index 0000000..9f315fb --- /dev/null +++ b/src/tag.rs @@ -0,0 +1,724 @@ +use chrono::{DateTime, Utc}; + +use crate::client::{Client, Cursor}; +use crate::error::Result as Rs621Result; + +use either::Either; + +use futures::stream::unfold; +use futures::{Stream, StreamExt}; + +use itertools::Itertools; + +use serde::{Deserialize, Serialize}; + +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use serde_with::formats::CommaSeparator; +use serde_with::serde_as; + +use std::{fmt, num::ParseIntError, ops::Not, str::FromStr}; + +use thiserror::Error; + +/// Kind of property a [`Tag`] describes. +#[derive(Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr, Clone, Copy)] +#[repr(u8)] +pub enum Category { + General = 0, + Artist = 1, + Copyright = 3, + Character = 4, + Species = 5, + Invalid = 6, + Meta = 7, + Lore = 8, +} + +impl FromStr for Category { + type Err = ParseCategoryError; + + fn from_str(s: &str) -> Result { + let num: u8 = s.parse()?; + let result = match num { + 0 => Self::General, + 1 => Self::Artist, + 3 => Self::Copyright, + 4 => Self::Character, + 5 => Self::Species, + 6 => Self::Invalid, + 7 => Self::Meta, + 8 => Self::Lore, + _ => return Err(ParseCategoryError::Unknown(num)), + }; + + Ok(result) + } +} + +impl fmt::Display for Category { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", *self as u8) + } +} + +#[derive(Error, Debug)] +pub enum ParseCategoryError { + #[error("category string is not a u8")] + ParseInt(#[from] ParseIntError), + + #[error("unknown category {0}")] + Unknown(u8), +} + +/// How to sort results of a [`Query`]. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum Order { + /// By [`id`][Tag::id], smallest first. + IdAsc, + + /// By [`id`][Tag::id], largest first. + IdDsc, + + /// By [`name`][Tag::name], in alphabetical order. + Name, + + /// By [`id`][Tag::id], largest first. See [`app/models/tag.rb`][0]. + /// + /// [0]: https://github.com/e621ng/e621ng/blob/7866c700faa690194352433931b4ee1db063e632/app/models/tag.rb#L365-L366 + Date, + + /// By [`post_count`][Tag::post_count], largest first. + Count, + + /// By similarity to query, when doing a fuzzy search, with most similar first. + Similarity, +} + +/// Tags are keywords used to describe a [`Post`][crate::post::Post]. +#[derive(Debug, PartialEq, Eq, Deserialize, Clone)] +#[non_exhaustive] +pub struct Tag { + pub id: u64, + pub name: String, + pub post_count: u64, + pub related_tags: String, // Swagger docs say `array`? + #[serde(default)] + pub related_tags_updated_at: Option>, + pub category: Category, + pub is_locked: bool, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +type CommaSeparated = serde_with::StringWithSeparator; + +/// A search query returning [`Tag`] instances. +/// +/// ``` +/// # use rs621::tag::{Query, Order, Category}; +/// # use rs621::client::Cursor; +/// let query = Query::new() +/// .per_page(1) +/// .page(Cursor::Page(2)) +/// .id(3) +/// .order(Order::Similarity) +/// .fuzzy_name_matches("fuzzy") +/// .name_matches(String::from("name_matches")) +/// .name("name") +/// .names(["hello"]) +/// .category(Category::Species) +/// .categories([Category::Lore, Category::Meta]) +/// .hide_empty(true) +/// .has_wiki(true) +/// .has_artist(true); +/// ``` +#[serde_as] +#[derive(Default, Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] +pub struct Query { + // Renaming `limit` to `per_page` because `limit` implies a maximum on the total number of + // items returned, which isn't the case because of the auto-pagination code. + #[serde(default, rename = "limit", skip_serializing_if = "Option::is_none")] + per_page: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + page: Option, + + #[serde( + rename = "search[id]", + default, + skip_serializing_if = "Option::is_none" + )] + id: Option, + + #[serde( + rename = "search[order]", + default, + skip_serializing_if = "Option::is_none" + )] + order: Option, + + #[serde( + rename = "search[fuzzy_name_matches]", + default, + skip_serializing_if = "Option::is_none" + )] + fuzzy_name_matches: Option, + + #[serde( + rename = "search[name_matches]", + default, + skip_serializing_if = "Option::is_none" + )] + name_matches: Option, + + #[serde_as(as = "CommaSeparated")] + #[serde( + rename = "search[name]", + default, + skip_serializing_if = "Vec::is_empty" + )] + name: Vec, + + #[serde_as(as = "CommaSeparated")] + #[serde( + rename = "search[category]", + default, + skip_serializing_if = "Vec::is_empty" + )] + category: Vec, + + #[serde( + rename = "search[hide_empty]", + default, + skip_serializing_if = "<&bool>::not" + )] + hide_empty: bool, + + #[serde( + rename = "search[has_wiki]", + default, + skip_serializing_if = "<&bool>::not" + )] + has_wiki: bool, + + #[serde( + rename = "search[has_artist]", + default, + skip_serializing_if = "<&bool>::not" + )] + has_artist: bool, +} + +impl Query { + /// Create a new instance of `Query` with the default values. + pub fn new() -> Self { + Self::default() + } + + /// Set the number of items to retrieve at a time. Equivalent to the `limit` query parameter. + pub fn per_page>>(mut self, per_page: T) -> Self { + self.per_page = per_page.into(); + self + } + + /// Set the page/offset to start retrieving tags from. + /// + /// **Note:** Setting either [`Cursor::Before`] or [`Cursor::After`] will clear any previously + /// set ordering ([`Self::order`].) + pub fn page>>(mut self, page: T) -> Self { + self.page = page.into(); + if !matches!(self.page, None | Some(Cursor::Page(_))) { + self.order = None; + } + self + } + + /// Search by [`Tag::id`]. + pub fn id>>(mut self, id: T) -> Self { + self.id = id.into(); + self + } + + /// Set the sort order for returned tags. + /// + /// **Note:** Setting this to a non-`None` value will clear [`Self::page`], unless + /// [`Self::page`] is set to [`Cursor::Page`]. + pub fn order>>(mut self, order: T) -> Self { + self.order = order.into(); + if self.order.is_some() && !matches!(self.page, None | Some(Cursor::Page(_))) { + self.page = None; + } + self + } + + /// Search by [`Tag::name`], finding results that are similar but not necessarily an exact + /// match. Useful in combination with [`Order::Similarity`]. + pub fn fuzzy_name_matches>, S: Into>(mut self, name: T) -> Self { + self.fuzzy_name_matches = name.into().map(Into::into); + self + } + + /// Search by [`Tag::name`], exactly. Supports wildcards using `*`. + pub fn name_matches>, S: Into>(mut self, name: T) -> Self { + self.name_matches = name.into().map(Into::into); + self + } + + /// Search by [`Tag::name`], exactly. + /// + /// This is a convenience function for setting a single value. For multiple values, see + /// [`Self::names`]. + pub fn name>, S: Into>(mut self, name: T) -> Self { + self.name = name.into().into_iter().map(Into::into).collect(); + self + } + + /// Search by [`Tag::name`], exactly. + /// + /// For a convenience function to set a single value, see [`Self::name`]. + pub fn names, S: Into>(mut self, names: I) -> Self { + self.name = names.into_iter().map(Into::into).collect(); + self + } + + /// Search by [`Tag::category`]. + /// + /// This is a convenience function for setting a single value. For multiple values, see + /// [`Self::categories`]. + pub fn category>>(mut self, category: T) -> Self { + self.category = category.into().into_iter().collect(); + self + } + + /// Search by [`Tag::category`], exactly. + /// + /// For a convenience function to set a single value, see [`Self::category`]. + pub fn categories>(mut self, categories: I) -> Self { + self.category = categories.into_iter().collect(); + self + } + + /// Whether to hide results that have no posts. + pub fn hide_empty(mut self, hide_empty: bool) -> Self { + self.hide_empty = hide_empty; + self + } + + /// Whether to only show results that have an associated wiki page. + pub fn has_wiki(mut self, has_wiki: bool) -> Self { + self.has_wiki = has_wiki; + self + } + + /// Whether to only show results that either are artists or have a matching artist tag (eg. + /// `lips` and `lips_(artist)`). + pub fn has_artist(mut self, has_artist: bool) -> Self { + self.has_artist = has_artist; + self + } +} + +impl Client { + /// Returns a Stream over all the [`Tag`]s matching the search query. + /// + /// ```no_run + /// # use rs621::client::Client; + /// # use rs621::tag::Query; + /// use futures::prelude::*; + /// + /// # #[tokio::main] + /// # async fn main() -> rs621::error::Result<()> { + /// let client = Client::new("https://e926.net", "MyProject/1.0 (by username on e621)")?; + /// + /// let query = Query::new() + /// .name("fluffy") + /// .per_page(1); + /// + /// let tag_stream = client.tag_search(query); + /// futures::pin_mut!(tag_stream); + /// + /// while let Some(tag) = tag_stream.next().await { + /// assert_eq!(tag?.name, "fluffy"); + /// } + /// # Ok(()) } + /// ``` + #[cfg(not(target_family = "wasm"))] + pub fn tag_search(&self, query: Query) -> impl Stream> + '_ + Send { + // TODO: There should be a way to use `try_unfold` here instead. + unfold(Some(query), move |query| self.tag_search_page(query)) + .map(futures::stream::iter) + .flatten() + } + + /// Returns a Stream over all the [`Tag`]s matching the search query. + /// + /// ```no_run + /// # use rs621::client::Client; + /// # use rs621::tag::Query; + /// use futures::prelude::*; + /// + /// # #[tokio::main] + /// # async fn main() -> rs621::error::Result<()> { + /// let client = Client::new("https://e926.net", "MyProject/1.0 (by username on e621)")?; + /// + /// let query = Query::new() + /// .name("fluffy") + /// .per_page(1); + /// + /// let tag_stream = client.tag_search(query); + /// futures::pin_mut!(tag_stream); + /// + /// while let Some(tag) = tag_stream.next().await { + /// assert_eq!(tag?.name, "fluffy"); + /// } + /// # Ok(()) } + /// ``` + #[cfg(target_family = "wasm")] + pub fn tag_search(&self, query: Query) -> impl Stream> + '_ { + // TODO: There should be a way to use `try_unfold` here instead. + unfold(Some(query), move |query| self.tag_search_page(query)) + .map(futures::stream::iter) + .flatten() + } + + async fn tag_search_page( + &self, + query: Option, + ) -> Option<(impl Iterator>, Option)> { + // `/tags.json` is weird and returns `{ tags: [] }` instead of the empty array when there + // are no results. + #[derive(Deserialize)] + #[serde(untagged, deny_unknown_fields)] + enum MaybeTags { + #[allow(dead_code)] + Empty { + tags: [(); 0], + }, + Items(Vec), + } + + // `query` will be `None` if the previous `tag_search_page` errored. If that is the case, + // this run of `tag_search_page` will return `None` to end the stream. + let mut query = query?; + + let tags = match self.get_json_endpoint_query("/tags.json", &query).await { + Err(e) => return Some((Either::Left(std::iter::once(Err(e))), None)), + Ok(MaybeTags::Empty { .. }) => return None, + Ok(MaybeTags::Items(i)) if i.is_empty() => return None, + Ok(MaybeTags::Items(i)) => i, + }; + + let (min, max) = tags.iter().map(|x| x.id).minmax().into_option().unwrap(); + + let next_page = match query.page { + Some(Cursor::Before(_)) => Cursor::Before(min), + Some(Cursor::After(_)) => Cursor::After(max), + Some(Cursor::Page(p)) => Cursor::Page(p + 1), + None => match query.order { + None | Some(Order::IdDsc | Order::Date) => Cursor::Before(min), + Some(Order::IdAsc) => Cursor::After(max), + _ => Cursor::Page(2), + }, + }; + query.page = Some(next_page); + + let tag_results = tags.into_iter().map(Ok); + + Some((Either::Right(tag_results), Some(query))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::TryStreamExt; + use mockito::mock; + + #[test] + fn build_query() { + let query = Query::new() + .per_page(1) + .page(Cursor::Page(2)) + .id(3) + .order(Order::Similarity) + .fuzzy_name_matches("fuzzy") + .name_matches(String::from("name_matches")) + .name("name") + .names(["hello"]) + .category(Category::Species) + .categories([Category::Lore, Category::Meta]) + .hide_empty(true) + .has_wiki(true) + .has_artist(true); + + assert_eq!( + query, + Query { + per_page: Some(1), + page: Some(Cursor::Page(2)), + id: Some(3), + order: Some(Order::Similarity), + fuzzy_name_matches: Some("fuzzy".into()), + name_matches: Some("name_matches".into()), + name: vec!["hello".into()], + category: vec![Category::Lore, Category::Meta], + hide_empty: true, + has_wiki: true, + has_artist: true, + } + ); + } + + #[tokio::test] + async fn tags_paginated_ordered_by_count() { + let client = Client::new(&mockito::server_url(), b"rs621/unit_test").unwrap(); + + let query = Query::new().order(Order::Count).per_page(1); + + let _page1 = mock("GET", "/tags.json?limit=1&search%5Border%5D=count") + .with_body(include_str!("mocked/tags_order-count_limit-1_page-1.json")) + .create(); + let _page2 = mock("GET", "/tags.json?limit=1&page=2&search%5Border%5D=count") + .with_body(include_str!("mocked/tags_order-count_limit-1_page-2.json")) + .create(); + let _page3 = mock("GET", "/tags.json?limit=1&page=3&search%5Border%5D=count") + .with_body(include_str!("mocked/tags_empty.json")) + .create(); + + let expected = vec![ + Tag { + id: 12054, + name: "mammal".into(), + post_count: 3350829, + related_tags: concat!( + "mammal 300 anthro 224 hi_res 212 female 185 male 162 solo 147 ", + "gentle_taps 146 fur 137 hair 130 balloons 125 clothing 122 duo 111 ", + "canid 109 canine 107 peanut 101 skittles 99 possibly_druids 98 ", + "digital_media_(artwork) 89 simple_background 87 dude 84 tail 77 ", + "text 77 absurd_res 73 genuine_druids 72 blush 70" + ) + .into(), + related_tags_updated_at: "2024-10-12T17:22:13.554-04:00".parse().ok(), + category: Category::Species, + is_locked: false, + created_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + updated_at: "2024-10-12T17:22:13.554-04:00".parse().unwrap(), + }, + Tag { + id: 7115, + name: "anthro".into(), + post_count: 3288365, + related_tags: concat!( + "anthro 300 mammal 236 hi_res 211 male 189 solo 165 female 162 ", + "clothing 159 gentle_taps 152 balloons 130 fur 118 hair 114 peanut 112 ", + "duo 107 possibly_druids 105 skittles 104 tail 100 simple_background 98 ", + "clothed 91 dude 90 canid 89 canine 88 digital_media_(artwork) 87 ", + "genuine_druids 82 balls 79 mutt 77" + ) + .into(), + related_tags_updated_at: "2024-10-11T10:06:56.303-04:00".parse().ok(), + category: Category::General, + is_locked: false, + created_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + updated_at: "2024-10-11T10:06:56.303-04:00".parse().unwrap(), + }, + ]; + + let actual: Vec<_> = client.tag_search(query).try_collect().await.unwrap(); + + assert_eq!(actual, expected); + } + + #[tokio::test] + async fn tags_paginated_before() { + let client = Client::new(&mockito::server_url(), b"rs621/unit_test").unwrap(); + + let query = Query::new().per_page(2).page(Cursor::Before(12054)); + + let _page1 = mock("GET", "/tags.json?limit=2&page=b12054") + .with_body(include_str!("mocked/tags_limit-2_page-b12054.json")) + .create(); + let _page2 = mock("GET", "/tags.json?limit=2&page=b12052") + .with_body(include_str!("mocked/tags_limit-2_page-b12052.json")) + .create(); + let _page3 = mock("GET", "/tags.json?limit=2&page=b12048") + .with_body(include_str!("mocked/tags_empty.json")) + .create(); + + let expected = vec![ + Tag { + id: 12053, + name: "sefeiren".into(), + post_count: 1581, + related_tags: concat!( + "conditional_dnp 300 sefeiren 300 female 222 male 211 claws 176 ", + "anthro 173 gentle_taps 165 mammal 165 paws 161 hindpaw 151 hi_res 150 ", + "feral 149 fur 142 tongue 142 dude 131 duo 122 open_mouth 122 ", + "scalie 117 possibly_druids 116 frisky_ferals 113 horn 112 solo 112 ", + "peanut 108 text 104 blush 101", + ) + .into(), + related_tags_updated_at: "2022-04-06T06:21:26.712-04:00".parse().ok(), + category: Category::Artist, + is_locked: false, + created_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + updated_at: "2022-04-06T06:21:26.713-04:00".parse().unwrap(), + }, + Tag { + id: 12052, + name: "skee".into(), + post_count: 2, + related_tags: concat!( + "male 2 solo 2 skee 2 simple_background 2 shorts 1 bulge 1 ", + "clothing 1 genuine_outline 1 gentle_taps 1 hi_res 1 huge_balls 1 ", + "huge_peanut 1 hyper 1 hyper_balls 1 hyper_melancholia 1 ", + "hyper_peanut 1 limitedvision 1 lizard 1 anthro 1 multi_melancholia 1 ", + "multi_peanut 1 peanut 1 peanut_outline 1 reptile 1 scalie 1", + ) + .into(), + related_tags_updated_at: "2020-02-24T21:51:39.390-05:00".parse().ok(), + category: Category::Artist, + is_locked: false, + created_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + updated_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + }, + Tag { + id: 12051, + name: "kash".into(), + post_count: 5, + related_tags: concat!( + "lagomorph 5 mammal 5 kash 5 male 5 leporid 4 rabbit 4 anthro 4 ", + "solo 4 balls 3 long_ears 3 dude 3 peanut 3 hair 3 purple_hair 2 ", + "affection 2 blue_countershading 2 gentle_taps 2 fur 2 mutt 2 biped 2 ", + "backpack 2 blue_markings 2 countershading 2 markings 2 challen 2", + ) + .into(), + related_tags_updated_at: "2020-03-04T21:55:37.812-05:00".parse().ok(), + category: Category::Character, + is_locked: false, + created_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + updated_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + }, + Tag { + id: 12048, + name: "fish_bowl".into(), + post_count: 147, + related_tags: concat!( + "aquarium 108 fish_bowl 108 vivarium 108 mammal 76 marine 71 ", + "fish 69 hi_res 56 clothing 50 feral 49 male 49 female 48 ", + "water 48 fur 47 text 44 hair 42 ambiguous_gender 40 anthro 40 ", + "group 38 english_text 36 smile 36 simple_background 35 ", + "felid 34 open_mouth 34 solo 34 clothed 32", + ) + .into(), + related_tags_updated_at: "2021-12-11T15:59:15.118-05:00".parse().ok(), + category: Category::General, + is_locked: false, + created_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + updated_at: "2021-12-11T15:59:15.118-05:00".parse().unwrap(), + }, + ]; + + let actual: Vec<_> = client.tag_search(query).try_collect().await.unwrap(); + + assert_eq!(actual, expected); + } + + #[tokio::test] + async fn tags_paginated_after() { + let client = Client::new(&mockito::server_url(), b"rs621/unit_test").unwrap(); + + let query = Query::new().per_page(2).page(Cursor::After(12047)); + + let _page1 = mock("GET", "/tags.json?limit=2&page=a12047") + .with_body(include_str!("mocked/tags_limit-2_page-a12047.json")) + .create(); + let _page2 = mock("GET", "/tags.json?limit=2&page=a12051") + .with_body(include_str!("mocked/tags_limit-2_page-a12051.json")) + .create(); + let _page3 = mock("GET", "/tags.json?limit=2&page=a12053") + .with_body(include_str!("mocked/tags_empty.json")) + .create(); + + // Even with `page=a...`, each page is returned in descending order. + let expected = vec![ + Tag { + id: 12051, + name: "kash".into(), + post_count: 5, + related_tags: concat!( + "lagomorph 5 mammal 5 kash 5 male 5 leporid 4 rabbit 4 anthro 4 ", + "solo 4 balls 3 long_ears 3 dude 3 peanut 3 hair 3 purple_hair 2 ", + "affection 2 blue_countershading 2 gentle_taps 2 fur 2 mutt 2 biped 2 ", + "backpack 2 blue_markings 2 countershading 2 markings 2 challen 2", + ) + .into(), + related_tags_updated_at: "2020-03-04T21:55:37.812-05:00".parse().ok(), + category: Category::Character, + is_locked: false, + created_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + updated_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + }, + Tag { + id: 12048, + name: "fish_bowl".into(), + post_count: 147, + related_tags: concat!( + "aquarium 108 fish_bowl 108 vivarium 108 mammal 76 marine 71 ", + "fish 69 hi_res 56 clothing 50 feral 49 male 49 female 48 ", + "water 48 fur 47 text 44 hair 42 ambiguous_gender 40 anthro 40 ", + "group 38 english_text 36 smile 36 simple_background 35 ", + "felid 34 open_mouth 34 solo 34 clothed 32", + ) + .into(), + related_tags_updated_at: "2021-12-11T15:59:15.118-05:00".parse().ok(), + category: Category::General, + is_locked: false, + created_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + updated_at: "2021-12-11T15:59:15.118-05:00".parse().unwrap(), + }, + Tag { + id: 12053, + name: "sefeiren".into(), + post_count: 1581, + related_tags: concat!( + "conditional_dnp 300 sefeiren 300 female 222 male 211 claws 176 ", + "anthro 173 gentle_taps 165 mammal 165 paws 161 hindpaw 151 hi_res 150 ", + "feral 149 fur 142 tongue 142 dude 131 duo 122 open_mouth 122 ", + "scalie 117 possibly_druids 116 frisky_ferals 113 horn 112 solo 112 ", + "peanut 108 text 104 blush 101", + ) + .into(), + related_tags_updated_at: "2022-04-06T06:21:26.712-04:00".parse().ok(), + category: Category::Artist, + is_locked: false, + created_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + updated_at: "2022-04-06T06:21:26.713-04:00".parse().unwrap(), + }, + Tag { + id: 12052, + name: "skee".into(), + post_count: 2, + related_tags: concat!( + "male 2 solo 2 skee 2 simple_background 2 shorts 1 bulge 1 ", + "clothing 1 genuine_outline 1 gentle_taps 1 hi_res 1 huge_balls 1 ", + "huge_peanut 1 hyper 1 hyper_balls 1 hyper_melancholia 1 ", + "hyper_peanut 1 limitedvision 1 lizard 1 anthro 1 multi_melancholia 1 ", + "multi_peanut 1 peanut 1 peanut_outline 1 reptile 1 scalie 1", + ) + .into(), + related_tags_updated_at: "2020-02-24T21:51:39.390-05:00".parse().ok(), + category: Category::Artist, + is_locked: false, + created_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + updated_at: "2020-03-05T05:49:37.994-05:00".parse().unwrap(), + }, + ]; + + let actual: Vec<_> = client.tag_search(query).try_collect().await.unwrap(); + + assert_eq!(actual, expected); + } +}