diff --git a/Cargo.lock b/Cargo.lock index 270a618..e751a4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,100 +1,122 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 4 + [[package]] -name = "aho-corasick" -version = "0.7.13" +name = "addr2line" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ - "memchr", + "gimli", ] [[package]] -name = "arc-swap" -version = "0.4.7" +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] [[package]] name = "assert-json-diff" -version = "1.1.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4259cbe96513d2f1073027a259fc2ca917feb3026a5a8d984e3628e490255cc0" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" dependencies = [ - "extend", "serde", "serde_json", ] [[package]] -name = "atty" -version = "0.2.14" +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ - "hermit-abi", + "addr2line", + "cfg-if", "libc", - "winapi 0.3.9", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] -name = "autocfg" -version = "1.0.1" +name = "base64" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] -name = "base64" -version = "0.12.3" +name = "bitflags" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "1.2.1" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "bumpalo" -version = "3.4.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" -version = "0.5.6" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.0.59" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66120af515773fb005778dc07c261bd201ec8ce50bd6e7144c927753fe013381" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" -version = "0.1.10" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "colored" -version = "1.9.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ - "atty", "lazy_static", - "winapi 0.3.9", + "windows-sys 0.59.0", ] [[package]] name = "core-foundation" -version = "0.7.0" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -102,15 +124,20 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.7.0" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "difference" -version = "2.0.0" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "dotenv" @@ -118,33 +145,37 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" -[[package]] -name = "dtoa" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" - [[package]] name = "encoding_rs" -version = "0.8.24" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a51b8cf747471cb9499b6d59e59b0444f4c90eba8968c4e44874e92b5b64ace2" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] -name = "extend" -version = "0.1.2" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47da3a72ec598d9c8937a7ebca8962a5c7a1f28444e38c2b33c771ba3f55f05" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "libc", + "windows-sys 0.60.2", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fnv" version = "1.0.7" @@ -168,85 +199,86 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ - "matches", "percent-encoding", ] -[[package]] -name = "fuchsia-zircon" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" -dependencies = [ - "bitflags", - "fuchsia-zircon-sys", -] - -[[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" - [[package]] name = "futures-channel" -version = "0.3.5" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.5" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-sink" -version = "0.3.5" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.5" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.5" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-task", - "pin-project", + "pin-project-lite", "pin-utils", ] [[package]] name = "getrandom" -version = "0.1.14" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "r-efi", + "wasi 0.14.3+wasi-0.2.4", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "h2" -version = "0.2.6" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", @@ -263,24 +295,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00d63df3d41950fb462ed38308eea019113ad1508da725bbedcd0fa5a85ef5f7" - -[[package]] -name = "hermit-abi" -version = "0.1.15" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" -dependencies = [ - "libc", -] +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" [[package]] name = "http" -version = "0.2.1" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -289,25 +312,32 @@ dependencies = [ [[package]] name = "http-body" -version = "0.3.1" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", + "pin-project-lite", ] [[package]] name = "httparse" -version = "1.3.4" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.13.7" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e68a8dd9716185d9e64ea473ea6ef63529252e3e27623295a0378a19665d5eb" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -317,10 +347,10 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", - "pin-project", - "socket2", - "time", + "pin-project-lite", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -329,216 +359,263 @@ dependencies = [ [[package]] name = "hyper-tls" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", "hyper", "native-tls", "tokio", - "tokio-tls", + "tokio-native-tls", ] [[package]] -name = "idna" -version = "0.2.0" +name = "icu_collections" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", ] [[package]] -name = "indexmap" -version = "1.6.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ - "autocfg", - "hashbrown", + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", ] [[package]] -name = "iovec" -version = "0.1.4" +name = "icu_normalizer" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ - "libc", + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", ] [[package]] -name = "ipnet" -version = "2.3.0" +name = "icu_normalizer_data" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] -name = "itoa" -version = "0.4.6" +name = "icu_properties" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] [[package]] -name = "js-sys" -version = "0.3.45" +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ - "wasm-bindgen", + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", ] [[package]] -name = "kernel32-sys" -version = "0.2.2" +name = "idna" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "idna_adapter" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] [[package]] -name = "libc" -version = "0.2.76" +name = "indexmap" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown", +] [[package]] -name = "log" -version = "0.4.11" +name = "io-uring" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ + "bitflags 2.9.3", "cfg-if", + "libc", ] [[package]] -name = "matches" -version = "0.1.8" +name = "ipnet" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] -name = "memchr" -version = "2.3.3" +name = "itoa" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] -name = "mime" -version = "0.3.16" +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "mime_guess" -version = "2.0.3" +name = "libc" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" -dependencies = [ - "mime", - "unicase", -] +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] -name = "mio" -version = "0.6.22" +name = "linux-raw-sys" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" -dependencies = [ - "cfg-if", - "fuchsia-zircon", - "fuchsia-zircon-sys", - "iovec", - "kernel32-sys", - "libc", - "log", - "miow 0.2.1", - "net2", - "slab", - "winapi 0.2.8", -] +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] -name = "mio-named-pipes" -version = "0.1.7" +name = "litemap" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" -dependencies = [ - "log", - "mio", - "miow 0.3.5", - "winapi 0.3.9", -] +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] -name = "mio-uds" -version = "0.6.8" +name = "lock_api" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ - "iovec", - "libc", - "mio", + "autocfg", + "scopeguard", ] [[package]] -name = "miow" -version = "0.2.1" +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[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.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "kernel32-sys", - "net2", - "winapi 0.2.8", - "ws2_32-sys", + "adler2", ] [[package]] -name = "miow" -version = "0.3.5" +name = "mio" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b88fb9795d4d36d62a012dfbf49a8f5cf12751f36d31a9dbe66d528e58979e" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ - "socket2", - "winapi 0.3.9", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] name = "mockito" -version = "0.27.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a634720d366bcbce30fb05871a35da229cef101ad0b2ea4e46cf5abf031a273" +checksum = "80f9fece9bd97ab74339fe19f4bcaf52b76dcc18e5364c7977c1838f76b38de9" dependencies = [ "assert-json-diff", "colored", - "difference", "httparse", "lazy_static", "log", "rand", "regex", "serde_json", - "serde_urlencoded 0.6.1", + "serde_urlencoded", + "similar", ] [[package]] name = "native-tls" -version = "0.2.4" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -551,53 +628,58 @@ dependencies = [ ] [[package]] -name = "net2" -version = "0.2.35" +name = "object" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ebc3ec692ed7c9a255596c67808dee269f64655d8baf7b4f0638e51ba1d6853" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ - "cfg-if", - "libc", - "winapi 0.3.9", + "memchr", ] [[package]] -name = "num_cpus" -version = "1.13.0" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" -dependencies = [ - "hermit-abi", - "libc", -] +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl" -version = "0.10.30" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags", + "bitflags 2.9.3", "cfg-if", "foreign-types", - "lazy_static", "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", +] + [[package]] name = "openssl-probe" -version = "0.1.2" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.58" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ - "autocfg", "cc", "libc", "pkg-config", @@ -605,36 +687,39 @@ dependencies = [ ] [[package]] -name = "percent-encoding" -version = "2.1.0" +name = "parking_lot" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] [[package]] -name = "pin-project" -version = "0.4.23" +name = "parking_lot_core" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ - "pin-project-internal", + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", ] [[package]] -name = "pin-project-internal" -version = "0.4.23" +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.1.7" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -644,76 +729,68 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" - -[[package]] -name = "ppv-lite86" -version = "0.2.9" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "potential_utf" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", + "zerovec", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "proc-macro2", - "quote", - "version_check", + "zerocopy", ] [[package]] name = "proc-macro2" -version = "1.0.20" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175c513d55719db99da20232b06cda8bab6b83ec2d04e3283edf0213c37c1a29" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.7" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" -version = "0.7.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "getrandom", "libc", "rand_chacha", "rand_core", - "rand_hc", ] [[package]] name = "rand_chacha" -version = "0.2.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", @@ -721,84 +798,84 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.5.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", ] [[package]] -name = "rand_hc" -version = "0.2.0" +name = "redox_syscall" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "rand_core", + "bitflags 2.9.3", ] -[[package]] -name = "redox_syscall" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - [[package]] name = "regex" -version = "1.3.9" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", + "regex-automata", "regex-syntax", - "thread_local", ] [[package]] -name = "regex-syntax" -version = "0.6.18" +name = "regex-automata" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] [[package]] -name = "remove_dir_all" -version = "0.5.3" +name = "regex-syntax" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi 0.3.9", -] +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "reqwest" -version = "0.10.8" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9eaa17ac5d7b838b7503d118fa16ad88f440498bf9ffe5424e621f93190d61e" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "hyper", "hyper-tls", "ipnet", "js-sys", - "lazy_static", "log", "mime", - "mime_guess", "native-tls", + "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile", "serde", "serde_json", - "serde_urlencoded 0.6.1", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", "tokio", - "tokio-tls", + "tokio-native-tls", + "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -816,64 +893,103 @@ dependencies = [ "reqwest", "serde", "serde_json", - "serde_urlencoded 0.7.0", + "serde_urlencoded", "thiserror", "tokio", ] [[package]] -name = "ryu" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" - -[[package]] -name = "schannel" -version = "0.1.19" +name = "rustc-demangle" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" -dependencies = [ - "lazy_static", - "winapi 0.3.9", -] +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] -name = "security-framework" -version = "0.4.4" +name = "rustix" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", + "bitflags 2.9.3", + "errno", "libc", - "security-framework-sys", + "linux-raw-sys", + "windows-sys 0.60.2", ] [[package]] -name = "security-framework-sys" -version = "0.4.3" +name = "rustls-pemfile" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "core-foundation-sys", - "libc", + "base64", ] [[package]] -name = "serde" -version = "1.0.115" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" -dependencies = [ - "serde_derive", -] +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.3", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" -version = "1.0.115" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -882,167 +998,204 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.57" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "serde_urlencoded" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ - "dtoa", + "form_urlencoded", "itoa", + "ryu", "serde", - "url", ] [[package]] -name = "serde_urlencoded" -version = "0.7.0" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.2.1" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e12110bc539e657a646068aaf5eb5b63af9d0c1f7b29c97113fad80e15f035" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ - "arc-swap", "libc", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "slab" -version = "0.4.2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.3.12" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ - "cfg-if", "libc", - "redox_syscall", - "winapi 0.3.9", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "syn" -version = "1.0.40" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963f7d3cc59b59b9325165add223142bbf1df27655d07789f109896d353d8350" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] -name = "tempfile" -version = "3.1.0" +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "cfg-if", - "libc", - "rand", - "redox_syscall", - "remove_dir_all", - "winapi 0.3.9", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "thiserror" -version = "1.0.20" +name = "system-configuration" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ - "thiserror-impl", + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", ] [[package]] -name = "thiserror-impl" -version = "1.0.20" +name = "system-configuration-sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ - "proc-macro2", - "quote", - "syn", + "core-foundation-sys", + "libc", ] [[package]] -name = "thread_local" -version = "1.0.1" +name = "tempfile" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ - "lazy_static", + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.60.2", ] [[package]] -name = "time" -version = "0.1.44" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi 0.3.9", + "thiserror-impl", ] [[package]] -name = "tinyvec" -version = "0.3.4" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] [[package]] name = "tokio" -version = "0.2.22" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d34ca54d84bf2b5b4d7d31e901a8464f7b60ac145a284fba25ceb801f2ddccd" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ + "backtrace", "bytes", - "fnv", - "futures-core", - "iovec", - "lazy_static", + "io-uring", "libc", - "memchr", "mio", - "mio-named-pipes", - "mio-uds", - "num_cpus", + "parking_lot", "pin-project-lite", "signal-hook-registry", "slab", + "socket2 0.6.0", "tokio-macros", - "winapi 0.3.9", + "windows-sys 0.59.0", ] [[package]] name = "tokio-macros" -version = "0.2.5" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -1050,10 +1203,10 @@ dependencies = [ ] [[package]] -name = "tokio-tls" +name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", @@ -1061,148 +1214,121 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.3.1" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", "futures-sink", - "log", "pin-project-lite", "tokio", ] [[package]] name = "tower-service" -version = "0.3.0" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.19" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ - "cfg-if", - "log", + "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.16" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcf46c1f1f06aeea2d6b81f3c863d0930a596c86ad1920d4e5bad6dd1d7119a" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] name = "try-lock" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" - -[[package]] -name = "unicase" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" -dependencies = [ - "matches", -] - -[[package]] -name = "unicode-normalization" -version = "0.1.13" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" -dependencies = [ - "tinyvec", -] +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "unicode-xid" -version = "0.2.1" +name = "unicode-ident" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "url" -version = "2.1.1" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ + "form_urlencoded", "idna", - "matches", "percent-encoding", + "serde", ] [[package]] -name = "vcpkg" -version = "0.2.10" +name = "utf8_iter" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "version_check" -version = "0.9.2" +name = "vcpkg" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] [[package]] name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.14.3+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +dependencies = [ + "wit-bindgen", +] [[package]] name = "wasm-bindgen" -version = "0.2.68" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", - "serde", - "serde_json", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.68" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", - "lazy_static", "log", "proc-macro2", "quote", @@ -1212,21 +1338,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.18" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7866cab0aa01de1edf8b5d7936938a7e397ee50ce24119aef3e1eaa3b6171da" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.68" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1234,9 +1361,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.68" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -1247,69 +1374,367 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.68" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.45" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "winapi" -version = "0.2.8" +name = "windows-link" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] -name = "winapi" -version = "0.3.9" +name = "windows-sys" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "windows-targets 0.48.5", ] [[package]] -name = "winapi-build" -version = "0.1.1" +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-sys" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows-targets" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +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 = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "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-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[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_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[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_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[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_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[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_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[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_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[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_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[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_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[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 = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winreg" -version = "0.7.0" +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 = "wit-bindgen" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ - "winapi 0.3.9", + "proc-macro2", + "quote", + "syn", + "synstructure", ] [[package]] -name = "ws2_32-sys" -version = "0.2.1" +name = "zerotrie" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 3b16e56..0d190e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ description = "Asynchronous client for the Tidal music service API" version = "0.1.2" authors = ["4xposed "] keywords = ["Tidal", "API", "Asynchronous"] -edition = "2018" +edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/4xposed/rstidal" readme = "README.md" @@ -12,14 +12,14 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -log = "0.4.11" -reqwest = { version = "0.10", features = ["json"] } -serde = { version = "1.0.115", features = ["derive"] } -serde_json = "1.0.57" -serde_urlencoded = "0.7.0" -thiserror = "1.0" +log = "0.4.17" +reqwest = { version = "0.11.18", features = ["json"] } +serde = { version = "1.0.163", features = ["derive"] } +serde_json = "1.0.96" +serde_urlencoded = "0.7.1" +thiserror = "1.0.40" [dev-dependencies] -mockito = "0.27.0" -tokio = { version = "0.2", features = ["full"] } -dotenv = { version = "0.15.0" } +mockito = "0.31" +tokio = { version = "1.28", features = ["full"] } +dotenv = "0.15.0" diff --git a/src/auth.rs b/src/auth.rs index 574ea00..0509792 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -9,6 +9,10 @@ use mockito; // Use built-in library use std::collections::HashMap; +// Use internal modules +use crate::config::params; +use crate::error::{AuthError, TidalError, TidalResult}; + #[derive(Clone, Debug)] pub struct TidalCredentials { pub token: String, @@ -29,33 +33,25 @@ impl TidalCredentials { self } - #[must_use] - pub async fn create_session(self, username: &str, password: &str) -> Self { + /// Create a session with the provided credentials + /// Returns an error if the token is empty or session creation fails + pub async fn create_session(self, username: &str, password: &str) -> TidalResult { if self.token.is_empty() { - // A token needs to be set before this function can be called - panic!("Application Token needs to be set") + return Err(TidalError::Authentication(AuthError::MissingToken)); } - let token = self.token.to_owned(); - let session = Session::get_session(&token, username, password).await.ok(); - self.session(session) + + let session = Session::get_session(&self.token, username, password).await?; + Ok(self.session(Some(session))) } } //Tidal session example: //{ - //"userId": 173393989, - //"sessionId": "84df94d0-9t0b-537a-a485-4404e45581ft", - //"countryCode": "DE" +//"userId": 173393989, +//"sessionId": "84df94d0-9t0b-537a-a485-4404e45581ft", +//"countryCode": "DE" //} -#[derive(thiserror::Error,Debug)] -pub enum AuthError { - #[error("The Authe request Failed")] - AuthRequestFailed { #[from] source: reqwest::Error }, - #[error("Fetch session failed")] - CreateSessionFailed -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Session { @@ -65,17 +61,16 @@ pub struct Session { } impl Session { - pub async fn get_session(token: &str, username: &str, password: &str) -> Result { - let mut payload: HashMap<&str, &str> = HashMap::new(); + pub async fn get_session(token: &str, username: &str, password: &str) -> TidalResult { + let mut payload: HashMap<&str, &str> = HashMap::with_capacity(2); payload.insert("username", username); payload.insert("password", password); Self::fetch_session_data(token, &payload).await } - async fn fetch_session_data(token: &str, payload: &HashMap<&str, &str>) -> Result { + async fn fetch_session_data(token: &str, payload: &HashMap<&str, &str>) -> TidalResult { let client = Client::new(); - let token = token.to_owned(); - let query = [("token", &token)]; + let query = [(params::TOKEN, token)]; #[cfg(not(test))] let url = "https://api.tidalhifi.com/v1/login/username"; @@ -86,21 +81,32 @@ impl Session { let response = client .post(url) .query(&query) - .form(&payload) + .form(payload) .send() - .await?; + .await + .map_err(AuthError::RequestFailed)?; if response.status().is_success() { - debug!("response content: {:?}", response); - let session: Session = response.json().await?; + debug!("Session creation successful"); + let session: Session = response + .json() + .await + .map_err(|e| TidalError::Authentication(AuthError::RequestFailed(e)))?; Ok(session) } else { error!( - "Creating session failed. token: {:?}, form: {:?}", - &token, &payload + "Session creation failed. Status: {}, Token length: {}, Payload: {:?}", + response.status(), + token.len(), + payload.keys().collect::>() ); - error!("{:?}", response); - Err(AuthError::CreateSessionFailed) + + match response.status() { + reqwest::StatusCode::UNAUTHORIZED => { + Err(TidalError::Authentication(AuthError::InvalidCredentials)) + } + _ => Err(TidalError::Authentication(AuthError::SessionCreationFailed)), + } } } } @@ -131,13 +137,17 @@ mod tests { async fn test_credential_create_session_w_token() { let token = "some_token"; let username = "myuser@example.com"; - let password = "somepawssowrd"; + let password = "somepassword"; let credentials = TidalCredentials::new(token); - // Test scucessful login + // Test successful login { let _mock = mock_successful_login(); - let credential_w_session = credentials.clone().create_session(username, password).await; + let credential_w_session = credentials + .clone() + .create_session(username, password) + .await + .unwrap(); assert_eq!( credential_w_session.session.unwrap().session_id, "session-id-123" @@ -146,12 +156,21 @@ mod tests { // Test failed login { let _mock = mock_failed_login(); - let credential_wo_session = - credentials.clone().create_session(username, password).await; - assert_eq!(credential_wo_session.session.is_none(), true); + let credential_result = credentials.clone().create_session(username, password).await; + assert!(credential_result.is_err()); } } + #[tokio::test] + async fn test_create_session_empty_token() { + let credentials = TidalCredentials::new(""); + let result = credentials.create_session("user", "pass").await; + assert!(matches!( + result, + Err(TidalError::Authentication(AuthError::MissingToken)) + )); + } + fn mock_successful_login() -> mockito::Mock { mock("POST", "/?token=some_token") .with_status(200) diff --git a/src/client.rs b/src/client.rs index 231a032..982d216 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,9 +1,8 @@ // Use 3rd party -use log::{debug, warn}; +use log::debug; use reqwest::header::HeaderMap; -use reqwest::{Client, Method, Response, StatusCode}; +use reqwest::{Client, Method, Response}; use serde::Deserialize; -use thiserror::Error; #[cfg(test)] use mockito; @@ -14,58 +13,13 @@ use std::collections::HashMap; // Use internal modules use crate::auth::{Session, TidalCredentials}; +use crate::config::{headers, params, Config}; +use crate::error::{ClientError, SessionError, TidalError, TidalResult}; use crate::model::album::Album; use crate::model::artist::Artist; use crate::model::playlist::Playlist; use crate::model::track::Track; -// Possible errors returned from `rstidal` client. -#[derive(Debug, Error)] -pub enum ClientError { - #[error("request unauthorized")] - Unauthorized, - #[error("tidal error: {0}")] - Api(#[from] ApiError), - #[error("etag heeader parse error")] - ParseEtag, - #[error("json parse error: {0}")] - ParseJSON(#[from] serde_json::Error), - #[error("request error: {0}")] - Request(#[from] reqwest::Error), - #[error("status code: {0}")] - StatusCode(StatusCode), -} - -impl ClientError { - async fn from_response(response: Response) -> Self { - match response.status() { - StatusCode::UNAUTHORIZED => Self::Unauthorized, - status @ StatusCode::FORBIDDEN | status @ StatusCode::NOT_FOUND => response - .json::() - .await - .map_or_else(|_| status.into(), Into::into), - status => status.into(), - } - } -} - -impl From for ClientError { - fn from(code: StatusCode) -> Self { - Self::StatusCode(code) - } -} -#[derive(Debug, Error, Deserialize)] -pub enum ApiError { - #[error("{status}: {message}")] - Regular { - status: u16, - #[serde(alias = "userMessage")] - message: String, - }, -} - -pub type ClientResult = Result; - #[derive(Default, Debug, Deserialize)] pub struct TidalItems { pub items: Vec, @@ -79,30 +33,47 @@ pub struct TidalSearch { pub tracks: TidalItems, } -// Tidal API - +/// Main Tidal API client pub struct Tidal { client: Client, pub(crate) credentials: TidalCredentials, + config: Config, } impl Tidal { - #[must_use] - pub fn new(credentials: TidalCredentials) -> Self { + /// Create a new Tidal client with credentials and default configuration + pub fn new(credentials: TidalCredentials) -> TidalResult { + Self::with_config(credentials, Config::default()) + } + + /// Create a new Tidal client with custom configuration + pub fn with_config(credentials: TidalCredentials, config: Config) -> TidalResult { if credentials.session.is_none() { - panic!("A session needs to be obtatined before using Tidal"); + return Err(TidalError::Session(SessionError::NoSession)); } - Self { + Ok(Self { client: Client::new(), credentials, - } + config, + }) } - pub fn user_id(&self) -> u32 { - // Here it's safe to use unwrap because in ::new() we already checked that there's a valid - // session - self.credentials.session.as_ref().unwrap().user_id + /// Get the user ID from the current session + pub fn user_id(&self) -> TidalResult { + self.credentials + .session + .as_ref() + .map(|session| session.user_id) + .ok_or_else(|| TidalError::Session(SessionError::NoSession)) + } + + /// Get the session information + pub fn session(&self) -> TidalResult<&Session> { + self.credentials + .session + .as_ref() + .ok_or_else(|| TidalError::Session(SessionError::NoSession)) } async fn api_call( @@ -112,55 +83,78 @@ impl Tidal { query: Option<&HashMap>, payload: Option<&HashMap<&str, &str>>, etag: Option, - ) -> ClientResult { + ) -> TidalResult { #[cfg(not(test))] - let base_url: &str = "https://api.tidalhifi.com/v1"; + let base_url = self.config.base_url; #[cfg(test)] - let base_url: &str = &mockito::server_url(); + let base_url = &mockito::server_url(); let mut url: Cow = url.into(); if !url.starts_with("http") { - url = [base_url, &url].concat().into(); + url = format!("{}{}", base_url, url).into(); } - let Session { session_id, country_code, .. } = self.credentials.session.as_ref().unwrap(); + let session = self.session()?; let mut headers = HeaderMap::new(); - headers.insert("X-Tidal-SessionId", session_id.parse().unwrap()); - headers.insert("Origin", "http://listen.tidal.com".parse().unwrap()); + + // Use safe header parsing with proper error handling + headers.insert( + headers::X_TIDAL_SESSION_ID, + session + .session_id + .parse() + .map_err(|_| TidalError::Client(ClientError::InvalidResponse))?, + ); + headers.insert( + headers::ORIGIN, + self.config + .origin_header + .parse() + .map_err(|_| TidalError::Client(ClientError::InvalidResponse))?, + ); + if let Some(etag) = etag { - headers.insert("If-None-Match", etag.parse().unwrap()); + headers.insert( + headers::IF_NONE_MATCH, + etag.parse() + .map_err(|_| TidalError::Client(ClientError::EtagParse))?, + ); } // Tidal's API requires countryCode to always be passed - let mut query_params: HashMap = HashMap::new(); - query_params.insert("countryCode".to_owned(), country_code.to_owned()); + let query_capacity = 1 + query.as_ref().map_or(0, |q| q.len()); + let mut query_params: HashMap = HashMap::with_capacity(query_capacity); + query_params.insert( + params::COUNTRY_CODE.to_owned(), + session.country_code.clone(), + ); if let Some(query) = query { - for (key, value) in query.iter() { - query_params.insert(key.clone(), value.clone()); - } + query_params.extend(query.iter().map(|(k, v)| (k.clone(), v.clone()))); } - let response = { - let builder = self - .client - .request(method, &url.into_owned()) - .headers(headers) - .query(&query_params); - - // Only add payload when sent - let builder = if let Some(form) = payload { - builder.form(form) - } else { - builder - }; - - debug!("request builder: {:?}", builder); - builder.send().await.map_err(ClientError::from)? + let builder = self + .client + .request(method, url.as_ref()) + .headers(headers) + .query(&query_params); + + // Only add payload when provided + let builder = if let Some(form) = payload { + builder.form(form) + } else { + builder }; - debug!("response content: {:?}", response); + debug!("Making API request to: {}", url); + let response = builder + .send() + .await + .map_err(|e| TidalError::Client(ClientError::Request(e)))?; + + debug!("API response status: {}", response.status()); + if response.status().is_success() { Ok(response) } else { @@ -168,458 +162,101 @@ impl Tidal { } } - pub async fn etag(&self, url: &str) -> ClientResult { - // Tidal's API requires countryCode to always be passed - let headers = self - .api_call(Method::GET, &url, None, None, None) - .await? - .headers() - .clone(); + /// Get the ETag header for a given URL + pub async fn etag(&self, url: &str) -> TidalResult { + let response = self.api_call(Method::GET, url, None, None, None).await?; + let headers = response.headers(); - if let Ok(etag) = headers - .get("etag") - .expect("etag header to be present") + headers + .get(headers::ETAG) + .ok_or_else(|| TidalError::Client(ClientError::EtagParse))? .to_str() - { - Ok(etag.to_owned()) - } else { - Err(ClientError::ParseEtag) - } + .map(|s| s.to_owned()) + .map_err(|_| TidalError::Client(ClientError::EtagParse)) } - pub async fn get( - &self, - url: &str, - params: &mut HashMap, - ) -> ClientResult { - self.api_call(Method::GET, &url, Some(params), None, None) + /// Make a GET request and return the response body as text + pub async fn get(&self, url: &str, params: &HashMap) -> TidalResult { + self.api_call(Method::GET, url, Some(params), None, None) .await? .text() .await - .map_err(Into::into) + .map_err(|e| TidalError::Client(ClientError::Request(e))) } + /// Make a POST request with form data and return the response body as text pub async fn post( &self, url: &str, payload: &HashMap<&str, &str>, etag: Option, - ) -> ClientResult { - self.api_call(Method::POST, &url, None, Some(payload), etag) + ) -> TidalResult { + self.api_call(Method::POST, url, None, Some(payload), etag) .await? .text() .await - .map_err(Into::into) + .map_err(|e| TidalError::Client(ClientError::Request(e))) } + /// Make a PUT request with form data and return the response body as text pub async fn put( &self, url: &str, payload: &HashMap<&str, &str>, etag: String, - ) -> ClientResult { + ) -> TidalResult { self.api_call(Method::PUT, url, None, Some(payload), Some(etag)) .await? .text() .await - .map_err(Into::into) - } - - // The following functions are for backward compatibility only - // - pub async fn search(&self, term: &str, limit: Option) -> ClientResult { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .searches().find()"); - self.searches().find(term, limit).await - } - - pub async fn artist(&self, id: &str) -> ClientResult { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .artists().get()"); - self.artists().get(id).await + .map_err(|e| TidalError::Client(ClientError::Request(e))) } - pub async fn search_artist(&self, term: &str, limit: Option) -> ClientResult> { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .artists().search()"); - self.artists().search(term, limit).await - } - - pub async fn album(&self, id: &str) -> ClientResult { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .albums().get()"); - self.albums().get(id).await - } - - pub async fn artist_albums(&self, id: &str) -> ClientResult> { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .artists().albums()"); - self.artists().albums(id).await - } - - pub async fn search_album(&self, term: &str, limit: Option) -> ClientResult> { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .albums().search()"); - self.albums().search(term, limit).await - } - - pub async fn album_tracks(&self, id: &str) -> ClientResult> { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .albums().tracks()"); - self.albums().tracks(id).await - } - - pub async fn search_track(&self, term: &str, limit: Option) -> ClientResult> { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .tracks().search()"); - self.tracks().search(term, limit).await - } - - pub async fn playlist(&self, id: &str) -> ClientResult { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .playlists().get()"); - self.playlists().get(id).await - } - - pub async fn search_playlist(&self, term: &str, limit: Option) -> ClientResult> { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .playlists().search()"); - self.playlists().search(term, limit).await - } - - pub async fn user_playlists(&self) -> ClientResult> { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .playlists().user_playlists()"); - self.playlists().user_playlists().await - } - - pub async fn playlist_tracks(&self, id: &str) -> ClientResult> { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .playlists().tracks()"); - self.playlists().tracks(id).await - } - - pub async fn playlist_add_tracks(&self, id: &str, tracks: Vec, add_dupes: bool) -> ClientResult { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .playlists().add_tracks()"); - self.playlists().add_tracks(id, tracks, add_dupes).await - } - - pub async fn create_playlist(&self, title: &str, description: &str) -> ClientResult { - warn!("DEPRECATION WARNING!: This method will be deprecated in the next version. Please favor using .playlists().create()"); - self.playlists().create(title, description).await - } - - pub fn convert_result<'a, T: Deserialize<'a>>(input: &'a str) -> ClientResult { - serde_json::from_str::(input).map_err(Into::into) + /// Convert a JSON string to the desired type + pub fn convert_result<'a, T: Deserialize<'a>>(input: &'a str) -> TidalResult { + serde_json::from_str::(input).map_err(|e| TidalError::Client(ClientError::JsonParse(e))) } } -#[cfg(test)] -pub mod tests { - use super::*; - use crate::auth::Session; - use mockito::{mock, Matcher}; - - #[tokio::test] - async fn client_get() { - let mut params: HashMap = HashMap::new(); - - // All requesets going to Tidal ned to append ?countryCode=$USER_REGION - let _mock = mock_request_success( - "GET", - "/", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - r#"{"result": "ok"}"#, - ); - - let client = client(); - let response = client.get("/", &mut params).await.unwrap(); - assert_eq!(response, r#"{"result": "ok"}"#) - } - - #[tokio::test] - async fn client_search() { - let _mock = mock_request_success_from_file( - "GET", - "/search", - vec![ - Matcher::UrlEncoded("countryCode".into(), "US".into()), - Matcher::UrlEncoded("query".into(), "trivium".into()), - Matcher::UrlEncoded("limit".into(), "10".into()), - ], - "tests/files/search.json", - ) - .create(); - - let result: TidalSearch = client().search("trivium", None).await.unwrap(); - - assert_eq!(result.artists.items.len(), 10); - assert_eq!(result.albums.items.len(), 10); - assert_eq!(result.tracks.items.len(), 10); - assert_eq!(result.playlists.items.len(), 10); - } - - #[tokio::test] - async fn client_artist() { - let _mock = mock_request_success_from_file( - "GET", - "/artists/37312", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/artist.json", - ) - .create(); - - let result: Artist = client().artist("37312").await.unwrap(); - let expected_result = Artist { - id: Some(37312), - name: Some("myband".to_owned()), - ..Default::default() - }; - assert_eq!(result.id, expected_result.id); - assert_eq!(result.name, expected_result.name); - } - - #[tokio::test] - async fn client_search_artist() { - let _mock = mock_request_success_from_file( - "GET", - "/search", - vec![ - Matcher::UrlEncoded("countryCode".into(), "US".into()), - Matcher::UrlEncoded("query".into(), "trivium".into()), - ], - "tests/files/search.json", - ) - .create(); - - let result: Vec = client().search_artist("trivium", None).await.unwrap(); - - assert_eq!(result.len(), 10); - } - - #[tokio::test] - async fn client_artist_albums() { - let _mock = mock_request_success_from_file( - "GET", - "/artists/37312/albums", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/artist_albums.json", - ); - - let result: Vec = client().artist_albums("37312").await.unwrap(); - let expected_first_result = Album { - id: Some(138458220), - title: Some("What The Dead Men Say".to_owned()), - ..Default::default() - }; - assert_eq!(result[0].id, expected_first_result.id); - assert_eq!(result[0].title, expected_first_result.title); - } - - #[tokio::test] - async fn client_album() { - let _mock = mock_request_success_from_file( - "GET", - "/albums/79914998", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/album.json", - ); - - let result: Album = client().album("79914998").await.unwrap(); - let expected_result = Album { - id: Some(79914998), - title: Some("My Album".to_owned()), - ..Default::default() - }; - assert_eq!(result.id, expected_result.id); - assert_eq!(result.title, expected_result.title); - } - - #[tokio::test] - async fn client_search_album() { - let _mock = mock_request_success_from_file( - "GET", - "/search", - vec![ - Matcher::UrlEncoded("countryCode".into(), "US".into()), - Matcher::UrlEncoded("query".into(), "trivium".into()), - ], - "tests/files/search.json", - ) - .create(); - - let result: Vec = client().search_album("trivium", None).await.unwrap(); - - assert_eq!(result.len(), 10); - } - - #[tokio::test] - async fn client_album_tracks() { - let _mock = mock_request_success_from_file( - "GET", - "/albums/79914998/tracks", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/album_tracks.json", - ); - - let result: Vec = client().album_tracks("79914998").await.unwrap(); - let expected_first_result = Track { - title: Some("The Sin and the Sentence".to_owned()), - ..Default::default() - }; - assert_eq!(result[0].title, expected_first_result.title); - } - - #[tokio::test] - async fn client_search_tracks() { - let _mock = mock_request_success_from_file( - "GET", - "/search", - vec![ - Matcher::UrlEncoded("countryCode".into(), "US".into()), - Matcher::UrlEncoded("query".into(), "trivium".into()), - ], - "tests/files/search.json", - ) - .create(); - - let result: Vec = client().search_track("trivium", None).await.unwrap(); - - assert_eq!(result.len(), 10); - } - - #[tokio::test] - async fn client_playlist() { - let _mock = mock_request_success_from_file( - "GET", - "/playlists/7ce7df87-6d37-4465-80db-84535a4e44a4", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/playlist.json", - ); - - let result: Playlist = client() - .playlist("7ce7df87-6d37-4465-80db-84535a4e44a4") - .await - .unwrap(); - let expected_result = Playlist { - uuid: Some("7ce7df87-6d37-4465-80db-84535a4e44a4".to_owned()), - title: Some("Metal - TIDAL Masters".to_owned()), - ..Default::default() - }; - assert_eq!(result.uuid, expected_result.uuid); - assert_eq!(result.title, expected_result.title); - } - - #[tokio::test] - async fn client_user_playlists() { - let _mock = mock_request_success_from_file( - "GET", - "/users/1234/playlists", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/user_playlists.json", - ); - - let result: Vec = client().user_playlists().await.unwrap(); - let expected_result = Playlist { - uuid: Some("8edf5a89-fec4-4aa3-80ab-9e00a83633a2".to_owned()), - title: Some("roadtrip".to_owned()), - ..Default::default() - }; - assert_eq!(result[0].uuid, expected_result.uuid); - assert_eq!(result[0].title, expected_result.title); - } - - #[tokio::test] - async fn client_playlist_tracks() { - let _mock = mock_request_success_from_file( - "GET", - "/playlists/7ce7df87-6d37-4465-80db-84535a4e44a4/tracks", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/playlist_tracks.json", - ); - - let result: Vec = client() - .playlist_tracks("7ce7df87-6d37-4465-80db-84535a4e44a4") - .await - .unwrap(); - let expected_first_result = Track { - title: Some("FULL OF HEALTH".to_owned()), - ..Default::default() - }; - assert_eq!(result[0].title, expected_first_result.title); +impl std::fmt::Debug for Tidal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Tidal") + .field("credentials", &self.credentials) + .field("config", &self.config) + .finish() } +} - #[tokio::test] - async fn client_playlist_add_tracks() { - let _mock_reload_playlist = mock_request_success_from_file( - "GET", - "/playlists/7ce7df87-6d37-4465-80db-84535a4e44a4", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/playlist.json", - ); +// Endpoint implementations +use crate::endpoints::albums::Albums; +use crate::endpoints::artists::Artists; +use crate::endpoints::playlists::Playlists; +use crate::endpoints::search::Search; +use crate::endpoints::tracks::Tracks; - let track_1 = Track { - id: Some(79914998), - ..Default::default() - }; - let track_2 = Track { - id: Some(7915000), - ..Default::default() - }; - let tracks = vec![track_1, track_2]; - - let _mock_etag_req = mock( - "GET", - "/playlists/7ce7df87-6d37-4465-80db-84535a4e44a4/items", - ) - .match_query(Matcher::UrlEncoded("countryCode".into(), "US".into())) - .with_body("") - .with_header("etag", "123457689") - .create(); - - let mock_update_playlist = mock( - "POST", - "/playlists/7ce7df87-6d37-4465-80db-84535a4e44a4/items", - ) - .match_query(Matcher::UrlEncoded("countryCode".into(), "US".into())) - .match_header("if-none-match", "123457689") - .with_body(r#"{ "lastUpdated": 1600273268158, "addedItemIds": [ 79914999, 79915000 ] }"#) - .create(); - - let _result: Playlist = client() - .playlist_add_tracks("7ce7df87-6d37-4465-80db-84535a4e44a4", tracks, false) - .await - .unwrap(); - mock_update_playlist.assert(); +impl Tidal { + /// Access album-related endpoints + pub const fn albums(&self) -> Albums<'_> { + Albums(self) } - fn mock_request_success( - method: &str, - path: &str, - query: Vec, - body: &str, - ) -> mockito::Mock { - mock(method, path) - .match_query(Matcher::AllOf(query)) - .with_status(200) - .with_body(body) - .create() + /// Access artist-related endpoints + pub const fn artists(&self) -> Artists<'_> { + Artists(self) } - pub fn mock_request_success_from_file( - method: &str, - path: &str, - query: Vec, - file_path: &str, - ) -> mockito::Mock { - mock(method, path) - .match_query(Matcher::AllOf(query)) - .with_status(200) - .with_body_from_file(file_path) - .create() + /// Access playlist-related endpoints + pub const fn playlists(&self) -> Playlists<'_> { + Playlists(self) } - pub fn client() -> Tidal { - Tidal::new(credential()) + /// Access search endpoints + pub const fn searches(&self) -> Search<'_> { + Search(self) } - fn credential() -> TidalCredentials { - let session: Session = Session { - user_id: 1234, - session_id: "session-id-1".to_owned(), - country_code: "US".to_owned(), - }; - TidalCredentials { - token: "some_token".to_owned(), - session: Some(session), - } + /// Access track-related endpoints + pub const fn tracks(&self) -> Tracks<'_> { + Tracks(self) } } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..4df4917 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,64 @@ +//! Configuration module for centralized settings and constants + +/// API configuration constants +#[derive(Debug)] +pub struct Config { + pub base_url: &'static str, + pub login_url: &'static str, + pub origin_header: &'static str, + pub default_country_code: &'static str, +} + +impl Default for Config { + fn default() -> Self { + Self { + base_url: "https://api.tidalhifi.com/v1", + login_url: "https://api.tidalhifi.com/v1/login/username", + origin_header: "http://listen.tidal.com", + default_country_code: "US", + } + } +} + +impl Config { + /// Create a new configuration with custom settings + pub fn new() -> Self { + Self::default() + } + + /// Create a configuration for testing + pub fn test(base_url: &str) -> Self { + Self { + base_url: Box::leak(base_url.to_string().into_boxed_str()), + login_url: Box::leak(base_url.to_string().into_boxed_str()), + origin_header: "http://listen.tidal.com", + default_country_code: "US", + } + } +} + +/// HTTP header constants +pub mod headers { + pub const X_TIDAL_SESSION_ID: &str = "X-Tidal-SessionId"; + pub const ORIGIN: &str = "Origin"; + pub const IF_NONE_MATCH: &str = "If-None-Match"; + pub const ETAG: &str = "etag"; +} + +/// API endpoint constants +pub mod endpoints { + pub const ARTISTS: &str = "/artists"; + pub const ALBUMS: &str = "/albums"; + pub const TRACKS: &str = "/tracks"; + pub const PLAYLISTS: &str = "/playlists"; + pub const SEARCH: &str = "/search"; + pub const USER_PLAYLISTS: &str = "/users/{user_id}/playlists"; +} + +/// Query parameter constants +pub mod params { + pub const COUNTRY_CODE: &str = "countryCode"; + pub const QUERY: &str = "query"; + pub const LIMIT: &str = "limit"; + pub const TOKEN: &str = "token"; +} diff --git a/src/endpoints/albums.rs b/src/endpoints/albums.rs index 8dcc582..7a4f680 100644 --- a/src/endpoints/albums.rs +++ b/src/endpoints/albums.rs @@ -2,27 +2,30 @@ use std::collections::HashMap; -use crate::client::{ClientResult, Tidal, TidalItems}; +use crate::client::{Tidal, TidalItems}; +use crate::error::TidalResult; use crate::model::album::Album; use crate::model::track::Track; pub struct Albums<'a>(pub &'a Tidal); impl Albums<'_> { - pub async fn get(self, id: &str) -> ClientResult { + pub async fn get(&self, id: &str) -> TidalResult { let url = format!("/albums/{}", id); - let result = self.0.get(&url, &mut HashMap::new()).await?; + let params = HashMap::new(); + let result = self.0.get(&url, ¶ms).await?; Tidal::convert_result::(&result) } - pub async fn search(&self, term: &str, limit: Option) -> ClientResult> { - let albums = self.0.search(term, limit).await?.albums.items; + pub async fn search(&self, term: &str, limit: Option) -> TidalResult> { + let albums = self.0.searches().find(term, limit).await?.albums.items; Ok(albums) } - pub async fn tracks(&self, id: &str) -> ClientResult> { + pub async fn tracks(&self, id: &str) -> TidalResult> { let url = format!("/albums/{}/tracks", id); - let result = self.0.get(&url, &mut HashMap::new()).await?; + let params = HashMap::new(); + let result = self.0.get(&url, ¶ms).await?; let tracks = Tidal::convert_result::>(&result)?.items; Ok(tracks) } @@ -30,61 +33,5 @@ impl Albums<'_> { #[cfg(test)] mod tests { - use super::*; - use crate::client::tests::{client, mock_request_success_from_file}; - use mockito::Matcher; - - #[tokio::test] - async fn get() { - let _mock = mock_request_success_from_file( - "GET", - "/albums/79914998", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/album.json", - ); - - let result: Album = client().album("79914998").await.unwrap(); - let expected_result = Album { - id: Some(79914998), - title: Some("My Album".to_owned()), - ..Default::default() - }; - assert_eq!(result.id, expected_result.id); - assert_eq!(result.title, expected_result.title); - } - - #[tokio::test] - async fn search() { - let _mock = mock_request_success_from_file( - "GET", - "/search", - vec![ - Matcher::UrlEncoded("countryCode".into(), "US".into()), - Matcher::UrlEncoded("query".into(), "trivium".into()), - ], - "tests/files/search.json", - ) - .create(); - - let result: Vec = client().search_album("trivium", None).await.unwrap(); - - assert_eq!(result.len(), 10); - } - - #[tokio::test] - async fn tracks() { - let _mock = mock_request_success_from_file( - "GET", - "/albums/79914998/tracks", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/album_tracks.json", - ); - - let result: Vec = client().album_tracks("79914998").await.unwrap(); - let expected_first_result = Track { - title: Some("The Sin and the Sentence".to_owned()), - ..Default::default() - }; - assert_eq!(result[0].title, expected_first_result.title); - } + // Tests have been moved to separate test files for better organization } diff --git a/src/endpoints/artists.rs b/src/endpoints/artists.rs index ed4a8ed..8dde2be 100644 --- a/src/endpoints/artists.rs +++ b/src/endpoints/artists.rs @@ -2,27 +2,30 @@ use std::collections::HashMap; -use crate::client::{ClientResult, Tidal, TidalItems}; +use crate::client::{Tidal, TidalItems}; +use crate::error::TidalResult; use crate::model::album::Album; use crate::model::artist::Artist; pub struct Artists<'a>(pub &'a Tidal); impl Artists<'_> { - pub async fn get(&self, id: &str) -> ClientResult { + pub async fn get(&self, id: &str) -> TidalResult { let url = format!("/artists/{}", id); - let result = self.0.get(&url, &mut HashMap::new()).await?; + let params = HashMap::new(); + let result = self.0.get(&url, ¶ms).await?; Tidal::convert_result::(&result) } - pub async fn search(&self, term: &str, limit: Option) -> ClientResult> { - let artists = self.0.search(term, limit).await?.artists.items; + pub async fn search(&self, term: &str, limit: Option) -> TidalResult> { + let artists = self.0.searches().find(term, limit).await?.artists.items; Ok(artists) } - pub async fn albums(&self, id: &str) -> ClientResult> { + pub async fn albums(&self, id: &str) -> TidalResult> { let url = format!("/artists/{}/albums", id); - let result = self.0.get(&url, &mut HashMap::new()).await?; + let params = HashMap::new(); + let result = self.0.get(&url, ¶ms).await?; let albums = Tidal::convert_result::>(&result)?.items; Ok(albums) } @@ -30,63 +33,5 @@ impl Artists<'_> { #[cfg(test)] mod tests { - use super::*; - use crate::client::tests::{client, mock_request_success_from_file}; - use mockito::Matcher; - - #[tokio::test] - async fn get() { - let _mock = mock_request_success_from_file( - "GET", - "/artists/37312", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/artist.json", - ); - - let result: Artist = client().artists().get("37312").await.unwrap(); - let expected_result = Artist { - id: Some(37312), - name: Some("myband".to_owned()), - ..Default::default() - }; - assert_eq!(result.id, expected_result.id); - assert_eq!(result.name, expected_result.name); - } - - #[tokio::test] - async fn search() { - let _mock = mock_request_success_from_file( - "GET", - "/search", - vec![ - Matcher::UrlEncoded("countryCode".into(), "US".into()), - Matcher::UrlEncoded("query".into(), "trivium".into()), - ], - "tests/files/search.json", - ) - .create(); - - let result: Vec = client().artists().search("trivium", None).await.unwrap(); - - assert_eq!(result.len(), 10); - } - - #[tokio::test] - async fn albums() { - let _mock = mock_request_success_from_file( - "GET", - "/artists/37312/albums", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/artist_albums.json", - ); - - let result: Vec = client().artists().albums("37312").await.unwrap(); - let expected_first_result = Album { - id: Some(138458220), - title: Some("What The Dead Men Say".to_owned()), - ..Default::default() - }; - assert_eq!(result[0].id, expected_first_result.id); - assert_eq!(result[0].title, expected_first_result.title); - } + // Tests have been moved to separate test files for better organization } diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index c8bd3d9..28b0961 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -3,34 +3,3 @@ pub mod artists; pub mod playlists; pub mod search; pub mod tracks; - -use crate::client::Tidal; -use crate::endpoints::albums::*; -use crate::endpoints::artists::*; -use crate::endpoints::playlists::*; -use crate::endpoints::search::*; -use crate::endpoints::tracks::*; - -// Endpoint function namespaces - -impl Tidal { - pub const fn albums(&self) -> Albums { - Albums(&self) - } - - pub const fn artists(&self) -> Artists { - Artists(&self) - } - - pub const fn playlists(&self) -> Playlists { - Playlists(&self) - } - - pub const fn searches(&self) -> Search { - Search(&self) - } - - pub const fn tracks(&self) -> Tracks { - Tracks(&self) - } -} diff --git a/src/endpoints/playlists.rs b/src/endpoints/playlists.rs index a3b1eb5..d9124a8 100644 --- a/src/endpoints/playlists.rs +++ b/src/endpoints/playlists.rs @@ -2,42 +2,38 @@ use std::collections::HashMap; -use crate::client::{ClientResult, Tidal, TidalItems}; +use crate::client::{Tidal, TidalItems}; +use crate::error::TidalResult; use crate::model::playlist::Playlist; use crate::model::track::Track; pub struct Playlists<'a>(pub &'a Tidal); impl Playlists<'_> { - pub async fn get(&self, id: &str) -> ClientResult { + pub async fn get(&self, id: &str) -> TidalResult { let url = format!("/playlists/{}", id); - let result = self.0.get(&url, &mut HashMap::new()).await?; + let params = HashMap::new(); + let result = self.0.get(&url, ¶ms).await?; Tidal::convert_result::(&result) } - pub async fn search(&self, term: &str, limit: Option) -> ClientResult> { - let playlists = self.0.search(term, limit).await?.playlists.items; + pub async fn search(&self, term: &str, limit: Option) -> TidalResult> { + let playlists = self.0.searches().find(term, limit).await?.playlists.items; Ok(playlists) } - pub async fn tracks(&self, id: &str) -> ClientResult> { + pub async fn tracks(&self, id: &str) -> TidalResult> { let url = format!("/playlists/{}/tracks", id); - let result = self.0.get(&url, &mut HashMap::new()).await?; + let params = HashMap::new(); + let result = self.0.get(&url, ¶ms).await?; let tracks = Tidal::convert_result::>(&result)?.items; Ok(tracks) } - pub async fn create(&self, title: &str, description: &str) -> ClientResult { - let user_id = self - .0 - .credentials - .session - .as_ref() - .expect("A valid session must be initialized") - .user_id; + pub async fn create(&self, title: &str, description: &str) -> TidalResult { + let user_id = self.0.user_id()?; let url = format!("/users/{}/playlists", user_id); - println!("URL: {:?}", url); - let mut form: HashMap<&str, &str> = HashMap::new(); + let mut form: HashMap<&str, &str> = HashMap::with_capacity(2); form.insert("title", title); form.insert("description", description); let result = self.0.post(&url, &form, None).await?; @@ -49,18 +45,25 @@ impl Playlists<'_> { id: &str, tracks: Vec, add_dupes: bool, - ) -> ClientResult { + ) -> TidalResult { let url = format!("/playlists/{}/items", id); // Get etag for the Playlist to be allowed to update the Playlist let etag: String = self.0.etag(&url).await?; // Convert the list of Tracks to a String with comma separated Track IDs - let track_ids: Vec = tracks + let track_ids: Result, _> = tracks .iter() - .map(|track| track.id.expect("Track struct must have an ID").to_string()) + .map(|track| { + track + .id + .ok_or_else(|| { + crate::error::TidalError::Config("Track ID is required".to_string()) + }) + .map(|id| id.to_string()) + }) .collect(); - let track_ids: String = track_ids.join(","); + let track_ids: String = track_ids?.join(","); let on_dupes: String = if add_dupes { "ADD".to_owned() @@ -68,7 +71,7 @@ impl Playlists<'_> { "FAIL".to_owned() }; - let mut form: HashMap<&str, &str> = HashMap::new(); + let mut form: HashMap<&str, &str> = HashMap::with_capacity(2); form.insert("trackIds", &track_ids); form.insert("onDupes", &on_dupes); @@ -76,13 +79,14 @@ impl Playlists<'_> { self.0.post(&url, &form, Some(etag)).await?; // Get updated Playlist - self.0.playlist(id).await + self.get(id).await } - pub async fn user_playlists(&self) -> ClientResult> { - let user_id = self.0.user_id(); + pub async fn user_playlists(&self) -> TidalResult> { + let user_id = self.0.user_id()?; let url = format!("/users/{}/playlists", user_id); - let result = self.0.get(&url, &mut HashMap::new()).await?; + let params = HashMap::new(); + let result = self.0.get(&url, ¶ms).await?; let playlists = Tidal::convert_result::>(&result)?.items; Ok(playlists) } @@ -90,134 +94,6 @@ impl Playlists<'_> { #[cfg(test)] mod tests { - use super::*; - use crate::client::tests::{client, mock_request_success_from_file}; - use mockito::{mock, Matcher}; - - #[tokio::test] - async fn get() { - let _mock = mock_request_success_from_file( - "GET", - "/playlists/7ce7df87-6d37-4465-80db-84535a4e44a4", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/playlist.json", - ); - - let result: Playlist = client() - .playlists() - .get("7ce7df87-6d37-4465-80db-84535a4e44a4") - .await - .unwrap(); - let expected_result = Playlist { - uuid: Some("7ce7df87-6d37-4465-80db-84535a4e44a4".to_owned()), - title: Some("Metal - TIDAL Masters".to_owned()), - ..Default::default() - }; - assert_eq!(result.uuid, expected_result.uuid); - assert_eq!(result.title, expected_result.title); - } - - #[tokio::test] - async fn create() { - let _mock = mock_request_success_from_file( - "POST", - "/users/1234/playlists", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/create_playlist.json", - ); - - let result: Playlist = client() - .playlists() - .create("something", "some desc") - .await - .unwrap(); - - assert_eq!(result.title.unwrap(), "something".to_string()); - assert_eq!(result.description.unwrap(), "some desc".to_string()); - } - - #[tokio::test] - async fn user_playlists() { - let _mock = mock_request_success_from_file( - "GET", - "/users/1234/playlists", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/user_playlists.json", - ); - - let result: Vec = client().playlists().user_playlists().await.unwrap(); - let expected_result = Playlist { - uuid: Some("8edf5a89-fec4-4aa3-80ab-9e00a83633a2".to_owned()), - title: Some("roadtrip".to_owned()), - ..Default::default() - }; - assert_eq!(result[0].uuid, expected_result.uuid); - assert_eq!(result[0].title, expected_result.title); - } - #[tokio::test] - async fn tracks() { - let _mock = mock_request_success_from_file( - "GET", - "/playlists/7ce7df87-6d37-4465-80db-84535a4e44a4/tracks", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/playlist_tracks.json", - ); - - let result: Vec = client() - .playlists() - .tracks("7ce7df87-6d37-4465-80db-84535a4e44a4") - .await - .unwrap(); - let expected_first_result = Track { - title: Some("FULL OF HEALTH".to_owned()), - ..Default::default() - }; - assert_eq!(result[0].title, expected_first_result.title); - } - - #[tokio::test] - async fn add_tracks() { - let _mock_reload_playlist = mock_request_success_from_file( - "GET", - "/playlists/7ce7df87-6d37-4465-80db-84535a4e44a4", - vec![Matcher::UrlEncoded("countryCode".into(), "US".into())], - "tests/files/playlist.json", - ); - - let track_1 = Track { - id: Some(79914998), - ..Default::default() - }; - let track_2 = Track { - id: Some(7915000), - ..Default::default() - }; - let tracks = vec![track_1, track_2]; - - let _mock_etag_req = mock( - "GET", - "/playlists/7ce7df87-6d37-4465-80db-84535a4e44a4/items", - ) - .match_query(Matcher::UrlEncoded("countryCode".into(), "US".into())) - .with_body("") - .with_header("etag", "123457689") - .create(); - - let mock_update_playlist = mock( - "POST", - "/playlists/7ce7df87-6d37-4465-80db-84535a4e44a4/items", - ) - .match_query(Matcher::UrlEncoded("countryCode".into(), "US".into())) - .match_header("if-none-match", "123457689") - .with_body(r#"{ "lastUpdated": 1600273268158, "addedItemIds": [ 79914999, 79915000 ] }"#) - .create(); - - let _result: Playlist = client() - .playlists() - .add_tracks("7ce7df87-6d37-4465-80db-84535a4e44a4", tracks, false) - .await - .unwrap(); - mock_update_playlist.assert(); - } + // Tests have been moved to separate test files for better organization } diff --git a/src/endpoints/search.rs b/src/endpoints/search.rs index 1e0b22d..dd63402 100644 --- a/src/endpoints/search.rs +++ b/src/endpoints/search.rs @@ -2,47 +2,25 @@ use std::collections::HashMap; -use crate::client::{ClientResult, Tidal, TidalSearch}; +use crate::client::{Tidal, TidalSearch}; +use crate::config::params; +use crate::error::TidalResult; pub struct Search<'a>(pub &'a Tidal); impl Search<'_> { - pub async fn find(&self, term: &str, limit: Option) -> ClientResult { + pub async fn find(&self, term: &str, limit: Option) -> TidalResult { let url = "/search"; - let limit = if let Some(limit) = limit { limit } else { 10 }; - let mut params: HashMap = HashMap::new(); - params.insert("query".to_owned(), term.to_owned()); - params.insert("limit".to_owned(), limit.to_string()); - let result = self.0.get(&url, &mut params).await?; + let limit = limit.unwrap_or(10); + let mut query_params: HashMap = HashMap::with_capacity(2); + query_params.insert(params::QUERY.to_owned(), term.to_owned()); + query_params.insert(params::LIMIT.to_owned(), limit.to_string()); + let result = self.0.get(&url, &query_params).await?; Tidal::convert_result::(&result) } } #[cfg(test)] mod tests { - use super::*; - use crate::client::tests::{client, mock_request_success_from_file}; - use mockito::Matcher; - - #[tokio::test] - async fn find() { - let _mock = mock_request_success_from_file( - "GET", - "/search", - vec![ - Matcher::UrlEncoded("countryCode".into(), "US".into()), - Matcher::UrlEncoded("query".into(), "trivium".into()), - Matcher::UrlEncoded("limit".into(), "10".into()), - ], - "tests/files/search.json", - ) - .create(); - - let result: TidalSearch = client().searches().find("trivium", None).await.unwrap(); - - assert_eq!(result.artists.items.len(), 10); - assert_eq!(result.albums.items.len(), 10); - assert_eq!(result.tracks.items.len(), 10); - assert_eq!(result.playlists.items.len(), 10); - } + // Tests have been moved to separate test files for better organization } diff --git a/src/endpoints/tracks.rs b/src/endpoints/tracks.rs index 31a171d..188b6e3 100644 --- a/src/endpoints/tracks.rs +++ b/src/endpoints/tracks.rs @@ -1,38 +1,20 @@ -//! Endpoint functions related to playlists +//! Endpoint functions related to tracks -use crate::client::{ClientResult, Tidal}; +use crate::client::Tidal; +use crate::error::TidalResult; use crate::model::track::Track; pub struct Tracks<'a>(pub &'a Tidal); impl Tracks<'_> { - pub async fn search(&self, term: &str, limit: Option) -> ClientResult> { - let tracks = self.0.search(term, limit).await?.tracks.items; + pub async fn search(&self, term: &str, limit: Option) -> TidalResult> { + let tracks = self.0.searches().find(term, limit).await?.tracks.items; Ok(tracks) } } #[cfg(test)] mod tests { - use super::*; - use crate::client::tests::{client, mock_request_success_from_file}; - use mockito::Matcher; - #[tokio::test] - async fn search() { - let _mock = mock_request_success_from_file( - "GET", - "/search", - vec![ - Matcher::UrlEncoded("countryCode".into(), "US".into()), - Matcher::UrlEncoded("query".into(), "trivium".into()), - ], - "tests/files/search.json", - ) - .create(); - - let result: Vec = client().tracks().search("trivium", None).await.unwrap(); - - assert_eq!(result.len(), 10); - } + // Tests have been moved to separate test files for better organization } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..341201a --- /dev/null +++ b/src/error.rs @@ -0,0 +1,204 @@ +//! Comprehensive error handling for the Tidal API client + +use reqwest::{Response, StatusCode}; +use serde::Deserialize; +use thiserror::Error; + +/// Main error type for all Tidal API operations +#[derive(Debug, Error)] +pub enum TidalError { + #[error("Authentication failed: {0}")] + Authentication(#[from] AuthError), + + #[error("Client error: {0}")] + Client(#[from] ClientError), + + #[error("API error: {0}")] + Api(#[from] ApiError), + + #[error("Configuration error: {0}")] + Config(String), + + #[error("Session error: {0}")] + Session(#[from] SessionError), +} + +/// Authentication-related errors +#[derive(Debug, Error)] +pub enum AuthError { + #[error("Authentication request failed")] + RequestFailed(#[from] reqwest::Error), + + #[error("Invalid credentials provided")] + InvalidCredentials, + + #[error("Token is missing or empty")] + MissingToken, + + #[error("Failed to create session")] + SessionCreationFailed, + + #[error("Session has expired")] + SessionExpired, +} + +/// Client-related errors +#[derive(Debug, Error)] +pub enum ClientError { + #[error("HTTP request failed")] + Request(#[from] reqwest::Error), + + #[error("JSON parsing failed: {0}")] + JsonParse(#[from] serde_json::Error), + + #[error("URL encoding failed: {0}")] + UrlEncode(#[from] serde_urlencoded::ser::Error), + + #[error("ETag header parsing failed")] + EtagParse, + + #[error("HTTP status code: {0}")] + StatusCode(StatusCode), + + #[error("Response body is empty")] + EmptyResponse, + + #[error("Invalid response format")] + InvalidResponse, +} + +/// Session-related errors +#[derive(Debug, Error)] +pub enum SessionError { + #[error("No active session found")] + NoSession, + + #[error("Session is invalid")] + InvalidSession, + + #[error("User ID is missing from session")] + MissingUserId, + + #[error("Country code is missing from session")] + MissingCountryCode, +} + +/// API-specific errors returned by Tidal +#[derive(Debug, Error, Deserialize)] +#[serde(untagged)] +pub enum ApiError { + #[error("API error {status}: {message}")] + Standard { + status: u16, + #[serde(alias = "userMessage")] + message: String, + #[serde(alias = "subStatus")] + sub_status: Option, + }, + + #[error("Unknown API error: {0}")] + Unknown(String), +} + +impl ClientError { + /// Create a ClientError from an HTTP response + pub async fn from_response(response: Response) -> TidalError { + let status = response.status(); + match status { + StatusCode::UNAUTHORIZED => TidalError::Authentication(AuthError::InvalidCredentials), + StatusCode::FORBIDDEN | StatusCode::NOT_FOUND => { + // Try to parse as API error first + match response.json::().await { + Ok(api_error) => TidalError::Api(api_error), + Err(_) => TidalError::Client(ClientError::StatusCode(status)), + } + } + status => TidalError::Client(ClientError::StatusCode(status)), + } + } +} + +impl From for ClientError { + fn from(code: StatusCode) -> Self { + Self::StatusCode(code) + } +} + +/// Result type alias for convenience +pub type TidalResult = Result; + +/// Helper functions for error handling +impl TidalError { + /// Check if the error is recoverable (e.g., temporary network issues) + pub fn is_recoverable(&self) -> bool { + match self { + TidalError::Client(ClientError::Request(_)) => true, + TidalError::Client(ClientError::StatusCode(status)) => status.is_server_error(), + _ => false, + } + } + + /// Check if the error requires re-authentication + pub fn requires_reauth(&self) -> bool { + matches!( + self, + TidalError::Authentication(_) + | TidalError::Session(SessionError::InvalidSession) + | TidalError::Session(SessionError::NoSession) + ) + } + + /// Get a user-friendly error message + pub fn user_message(&self) -> String { + match self { + TidalError::Authentication(AuthError::InvalidCredentials) => { + "Please check your username and password".to_string() + } + TidalError::Authentication(AuthError::MissingToken) => { + "Application token is required".to_string() + } + TidalError::Session(SessionError::NoSession) => "Please log in to continue".to_string(), + TidalError::Client(ClientError::Request(_)) => { + "Network connection failed. Please try again".to_string() + } + _ => self.to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_is_recoverable() { + let error = TidalError::Client(ClientError::StatusCode(StatusCode::INTERNAL_SERVER_ERROR)); + assert!(error.is_recoverable()); + + let error = TidalError::Authentication(AuthError::InvalidCredentials); + assert!(!error.is_recoverable()); + } + + #[test] + fn test_error_requires_reauth() { + let error = TidalError::Authentication(AuthError::SessionExpired); + assert!(error.requires_reauth()); + + let error = TidalError::Client(ClientError::JsonParse( + serde_json::from_str::<()>("invalid").unwrap_err(), + )); + assert!(!error.requires_reauth()); + } + + #[test] + fn test_user_friendly_messages() { + let error = TidalError::Authentication(AuthError::InvalidCredentials); + assert_eq!( + error.user_message(), + "Please check your username and password" + ); + + let error = TidalError::Session(SessionError::NoSession); + assert_eq!(error.user_message(), "Please log in to continue"); + } +} diff --git a/src/lib.rs b/src/lib.rs index d324bbb..2218570 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,67 +1,167 @@ -//! Rstidal is a wrapper for the Tidal API. +//! # Rstidal - Asynchronous Tidal API Client //! -//! ## Configuration +//! Rstidal is a modern, fully asynchronous Rust client for the Tidal music streaming API. +//! It provides comprehensive access to Tidal's music catalog including artists, albums, +//! tracks, and playlists with a clean, type-safe interface. +//! +//! ## Features +//! +//! - **Fully Async**: Built with `tokio` and `reqwest` for high-performance async operations +//! - **Type Safety**: Comprehensive error handling with custom error types +//! - **Clean API**: Organized endpoint modules for different resource types +//! - **Robust Error Handling**: Detailed error types with user-friendly messages +//! - **Session Management**: Automatic session handling and authentication +//! - **Testing Support**: Built-in support for mocking and testing +//! +//! ## Quick Start //! //! Add this to your `Cargo.toml`: //! //! ```toml //! [dependencies] //! rstidal = "0.1.2" +//! tokio = { version = "1.0", features = ["full"] } //! ``` //! -//! By default, Rstidal uses asynchronous programming with `asycn` and `await`. +//! ## Authentication //! -//! ## Getting Started +//! All API methods require user authentication. You need: +//! 1. An Application Token (from Tidal Desktop app) +//! 2. Valid Tidal user credentials //! -//! ## Authorization +//! ### Getting an Application Token //! -//! Since all methods required user authentication, you are required to create a -//! session using a Tidal username and password. -//! In order to authenticate a user your application needs an Application Token. +//! 1. Use a debug proxy (Charles, Fiddler, or browser dev tools) +//! 2. Open your Tidal Desktop application +//! 3. Look for requests to `api.tidal.com` +//! 4. Copy the `X-Tidal-Token` header value //! +//! ## Usage Examples //! -//! ## How to get an Application Token +//! ### Basic Usage //! -//! Using a debug proxy (Charles or Fiddler) open your Tidal Desktop application, look for -//! requests to `api.tidal.com` and copy the value it uses in the header `X-Tidal-Token`. +//! ```rust,no_run +//! use rstidal::{TidalCredentials, Tidal, TidalResult}; +//! use std::env; //! -//! ### Examples +//! #[tokio::main] +//! async fn main() -> TidalResult<()> { +//! // Get credentials from environment +//! let token = env::var("TIDAL_TOKEN").expect("TIDAL_TOKEN not set"); +//! let username = env::var("TIDAL_USERNAME").expect("TIDAL_USERNAME not set"); +//! let password = env::var("TIDAL_PASSWORD").expect("TIDAL_PASSWORD not set"); +//! +//! // Create session +//! let credentials = TidalCredentials::new(&token) +//! .create_session(&username, &password) +//! .await?; +//! +//! // Initialize client +//! let client = Tidal::new(credentials)?; +//! +//! // Search for an artist +//! let artists = client.artists().search("Radiohead", Some(5)).await?; +//! println!("Found {} artists", artists.len()); +//! +//! // Get artist details +//! if let Some(artist) = artists.first() { +//! if let Some(id) = artist.id { +//! let albums = client.artists().albums(&id.to_string()).await?; +//! println!("Artist has {} albums", albums.len()); +//! } +//! } //! -//! ```toml -//! [dependencies] -//! rstidal = { version = "0.1.0" } -//! tokio = { version = "0.2", feeatures = ["full"] } +//! Ok(()) +//! } //! ``` //! -//! ```rust -//! use rstidal::client::Tidal; -//! use rstidal::auth::TidalCredentials; -//! use dotenv::dotenv; -//! use std::env; +//! ### Advanced Usage with Error Handling +//! +//! ```rust,no_run +//! use rstidal::{TidalCredentials, Tidal, TidalError, TidalResult}; //! //! #[tokio::main] //! async fn main() { -//! { -//! dotenv().ok(); +//! match run_example().await { +//! Ok(_) => println!("Success!"), +//! Err(e) => { +//! eprintln!("Error: {}", e.user_message()); +//! if e.requires_reauth() { +//! eprintln!("Please check your credentials and try again."); +//! } +//! } +//! } +//! } +//! +//! async fn run_example() -> TidalResult<()> { +//! let credentials = TidalCredentials::new("your-token") +//! .create_session("username", "password") +//! .await?; +//! +//! let client = Tidal::new(credentials)?; +//! +//! // Get user's playlists +//! let playlists = client.playlists().user_playlists().await?; +//! +//! for playlist in playlists { +//! if let Some(title) = &playlist.title { +//! println!("Playlist: {}", title); +//! +//! // Get tracks in playlist +//! if let Some(uuid) = &playlist.uuid { +//! let tracks = client.playlists().tracks(uuid).await?; +//! println!(" {} tracks", tracks.len()); +//! } +//! } //! } //! -//! // Set the token aquired by inspecting your Tidal Desktop application. -//! let token = env::var("RSTIDAL_APP_TOKEN").unwrap(); -//! let credentials = TidalCredentials::new(&token); +//! Ok(()) +//! } +//! ``` //! -//! // Create a session using your user credentials. -//! let username = env::var("RSTIDAL_USERNAME").unwrap(); -//! let password = env::var("RSTIDAL_PASSWORD").unwrap(); -//! let credentials = credentials.create_session(&username, &password).await; +//! ## API Structure //! -//! // Use the credentials to start the client -//! let client = Tidal::new(credentials); -//! let artist = client.artists().get("37312").await; -//! println!("{:?}", artist.unwrap()); +//! The API is organized into modules for different resource types: +//! +//! - `client.artists()` - Artist-related operations +//! - `client.albums()` - Album-related operations +//! - `client.tracks()` - Track-related operations +//! - `client.playlists()` - Playlist-related operations +//! - `client.searches()` - Search operations across all types +//! +//! ## Error Handling +//! +//! Rstidal provides comprehensive error handling with the `TidalError` enum: +//! +//! ```rust,no_run +//! use rstidal::{Tidal, TidalCredentials, TidalError}; +//! use rstidal::error::{AuthError, ClientError}; +//! +//! # async fn example() -> Result<(), Box> { +//! # let credentials = TidalCredentials::new("token"); +//! # let client = Tidal::new(credentials)?; +//! match client.artists().get("invalid-id").await { +//! Ok(artist) => println!("Artist: {:?}", artist), +//! Err(TidalError::Authentication(AuthError::InvalidCredentials)) => { +//! println!("Please check your login credentials"); +//! }, +//! Err(TidalError::Client(ClientError::StatusCode(status))) => { +//! println!("HTTP error: {}", status); +//! }, +//! Err(e) => println!("Other error: {}", e), //! } +//! # Ok(()) +//! # } //! ``` pub mod auth; pub mod client; +pub mod config; pub mod endpoints; +pub mod error; pub mod model; + +// Re-export commonly used types for convenience +pub use auth::{Session, TidalCredentials}; +pub use client::Tidal; +pub use error::{TidalError, TidalResult}; diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 0000000..4fc60fb --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,166 @@ +//! Integration tests for the Tidal API client + +use mockito::{mock, Matcher}; +use rstidal::auth::{Session, TidalCredentials}; +use rstidal::client::Tidal; +use rstidal::config::Config; +use rstidal::error::{AuthError, SessionError, TidalError}; + +// Test helper functions +fn test_client() -> Tidal { + let session = Session { + user_id: 1234, + session_id: "test-session-id".to_owned(), + country_code: "US".to_owned(), + }; + + let credentials = TidalCredentials::new("test-token").session(Some(session)); + let config = Config::test(&mockito::server_url()); + + Tidal::with_config(credentials, config).expect("Failed to create test client") +} + +fn mock_request_success_from_file( + method: &str, + path: &str, + query: Vec, + file_path: &str, +) -> mockito::Mock { + mock(method, path) + .match_query(Matcher::AllOf(query)) + .with_status(200) + .with_body_from_file(file_path) + .create() +} + +fn country_code_matcher() -> Matcher { + Matcher::UrlEncoded("countryCode".into(), "US".into()) +} + +fn query_matcher(key: &str, value: &str) -> Matcher { + Matcher::UrlEncoded(key.into(), value.into()) +} + +#[tokio::test] +async fn test_client_creation_with_session() { + let session = Session { + user_id: 123, + session_id: "test-session".to_owned(), + country_code: "US".to_owned(), + }; + let credentials = TidalCredentials::new("test-token").session(Some(session)); + let client = Tidal::new(credentials); + assert!(client.is_ok()); +} + +#[tokio::test] +async fn test_client_creation_without_session() { + let credentials = TidalCredentials::new("test-token"); + let client = Tidal::new(credentials); + match client { + Err(TidalError::Session(SessionError::NoSession)) => { + // This is expected + } + _ => panic!("Expected SessionError::NoSession"), + } +} + +#[tokio::test] +async fn test_user_id_retrieval() { + let client = test_client(); + let user_id = client.user_id().unwrap(); + assert_eq!(user_id, 1234); +} + +#[tokio::test] +async fn test_session_retrieval() { + let client = test_client(); + let session = client.session().unwrap(); + assert_eq!(session.user_id, 1234); + assert_eq!(session.country_code, "US"); +} + +#[tokio::test] +async fn test_artist_get() { + let _mock = mock_request_success_from_file( + "GET", + "/artists/37312", + vec![country_code_matcher()], + "tests/files/artist.json", + ); + + let client = test_client(); + let result = client.artists().get("37312").await.unwrap(); + + assert_eq!(result.id, Some(37312)); + assert_eq!(result.name, Some("myband".to_owned())); +} + +#[tokio::test] +async fn test_search_functionality() { + let _mock = mock_request_success_from_file( + "GET", + "/search", + vec![ + country_code_matcher(), + query_matcher("query", "trivium"), + query_matcher("limit", "10"), + ], + "tests/files/search.json", + ); + + let client = test_client(); + let result = client.searches().find("trivium", None).await.unwrap(); + + assert_eq!(result.artists.items.len(), 10); + assert_eq!(result.albums.items.len(), 10); + assert_eq!(result.tracks.items.len(), 10); + assert_eq!(result.playlists.items.len(), 10); +} + +#[tokio::test] +async fn test_error_handling_unauthorized() { + let _mock = mock("GET", "/artists/37312") + .match_query(country_code_matcher()) + .with_status(401) + .with_body(r#"{"status": 401, "userMessage": "Unauthorized"}"#) + .create(); + + let client = test_client(); + let result = client.artists().get("37312").await; + + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + TidalError::Authentication(AuthError::InvalidCredentials) + )); +} + +#[tokio::test] +async fn test_etag_functionality() { + let _mock = mock("GET", "/test-endpoint") + .match_query(country_code_matcher()) + .with_status(200) + .with_header("etag", "test-etag-value") + .with_body("") + .create(); + + let client = test_client(); + let etag = client.etag("/test-endpoint").await.unwrap(); + + assert_eq!(etag, "test-etag-value"); +} + +// Authentication tests have been moved to unit tests in the auth module +// since they require special mock server configuration + +#[tokio::test] +async fn test_empty_token_error() { + let credentials = TidalCredentials::new(""); + let result = credentials.create_session("user", "pass").await; + + assert!(matches!( + result.unwrap_err(), + TidalError::Authentication(AuthError::MissingToken) + )); +}