diff --git a/Cargo.lock b/Cargo.lock index 473af06..4580e22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,36 +2,15 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -43,9 +22,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -58,44 +37,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "async-stream" @@ -127,24 +106,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" @@ -154,13 +118,13 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.7.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bourso-cli" -version = "0.3.2" +version = "1.0.0" dependencies = [ "anyhow", "bourso_api", @@ -172,6 +136,7 @@ dependencies = [ "serde_json", "tokio", "tracing", + "tracing-appender", "tracing-subscriber", ] @@ -184,6 +149,7 @@ dependencies = [ "chrono", "clap", "cookie_store", + "derive_more", "futures-util", "lazy_static", "regex", @@ -191,48 +157,49 @@ dependencies = [ "reqwest_cookie_store", "serde", "serde_json", + "thiserror 2.0.17", "tracing", ] [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.9" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -271,15 +238,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "cookie" @@ -326,34 +293,69 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "directories" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -369,9 +371,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -387,18 +389,18 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -407,6 +409,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "fnv" version = "1.0.7" @@ -430,9 +438,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -491,9 +499,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -501,16 +509,22 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.31.1" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] [[package]] name = "h2" -version = "0.4.7" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -527,9 +541,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "heck" @@ -539,9 +553,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -560,12 +574,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -573,25 +587,27 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.5.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -599,11 +615,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", @@ -632,33 +647,41 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ + "base64", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -674,21 +697,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -697,104 +721,66 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -803,9 +789,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -813,9 +799,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown", @@ -823,27 +809,37 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -857,15 +853,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags", "libc", @@ -873,29 +869,28 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.7.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" -version = "0.4.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -916,9 +911,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -936,31 +931,22 @@ dependencies = [ "unicase", ] -[[package]] -name = "miniz_oxide" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" -dependencies = [ - "adler2", -] - [[package]] name = "mio" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "wasi", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -979,7 +965,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -998,25 +984,22 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.36.7" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "once_cell" -version = "1.20.2" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl" -version = "0.10.68" +version = "0.10.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ "bitflags", "cfg-if", @@ -1040,15 +1023,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" dependencies = [ "cc", "libc", @@ -1064,9 +1047,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1074,22 +1057,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -1105,9 +1088,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] [[package]] name = "powerfmt" @@ -1117,9 +1109,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -1142,38 +1134,44 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 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 = "redox_syscall" -version = "0.5.8" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom", + "getrandom 0.2.16", "libredox", - "thiserror", + "thiserror 2.0.17", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -1183,9 +1181,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -1194,15 +1192,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.12" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", @@ -1219,37 +1217,34 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", "mime_guess", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-registry", ] [[package]] name = "reqwest_cookie_store" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b36498c7452f11b1833900f31fbb01fc46be20992a50269c88cf59d79f54e9" +checksum = "2314c325724fea278d44c13a525ebf60074e33c05f13b4345c076eb65b2446b3" dependencies = [ "bytes", "cookie_store", @@ -1259,64 +1254,57 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.16", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rpassword" -version = "7.3.1" +version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" dependencies = [ "libc", "rtoolbox", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] name = "rtoolbox" -version = "0.0.2" +version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustix" -version = "0.38.43" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "rustls-pki-types", @@ -1326,25 +1314,19 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "2.2.0" +name = "rustls-pki-types" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ - "rustls-pki-types", + "zeroize", ] -[[package]] -name = "rustls-pki-types" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" - [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -1353,23 +1335,23 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1393,9 +1375,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -1403,18 +1385,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1423,14 +1415,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -1462,49 +1455,40 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strsim" @@ -1520,9 +1504,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.96" +version = "2.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" dependencies = [ "proc-macro2", "quote", @@ -1540,9 +1524,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -1572,16 +1556,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.15.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "cfg-if", "fastrand", - "getrandom", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1590,7 +1573,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", ] [[package]] @@ -1604,6 +1596,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -1615,9 +1618,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -1630,15 +1633,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -1646,9 +1649,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -1656,11 +1659,10 @@ dependencies = [ [[package]] name = "tokio" -version = "1.43.0" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "libc", "mio", @@ -1669,14 +1671,14 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -1695,20 +1697,19 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", - "rustls-pki-types", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -1732,6 +1733,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -1755,6 +1774,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.30" @@ -1768,9 +1799,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -1832,9 +1863,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "untrusted" @@ -1844,21 +1875,16 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -1900,41 +1926,37 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasm-bindgen" -version = "0.2.100" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" +name = "wasm-bindgen" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -1945,9 +1967,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1955,31 +1977,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -1987,50 +2009,96 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-registry" -version = "0.2.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-result", - "windows-strings", - "windows-targets 0.52.6", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-targets 0.52.6", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-result", - "windows-targets 0.52.6", + "windows-link 0.1.3", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-targets 0.48.5", + "windows-link 0.2.1", ] [[package]] @@ -2052,18 +2120,21 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 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", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -2075,7 +2146,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "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", @@ -2083,10 +2154,21 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" +name = "windows-targets" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] [[package]] name = "windows_aarch64_gnullvm" @@ -2095,10 +2177,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" +name = "windows_aarch64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -2107,10 +2189,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_i686_gnu" -version = "0.48.5" +name = "windows_aarch64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -2118,6 +2200,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" @@ -2125,10 +2213,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_msvc" -version = "0.48.5" +name = "windows_i686_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -2137,10 +2225,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +name = "windows_i686_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -2149,10 +2237,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +name = "windows_x86_64_gnu" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -2161,10 +2249,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "windows_x86_64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -2173,24 +2261,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "write16" -version = "1.0.0" +name = "windows_x86_64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -2198,9 +2291,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -2210,18 +2303,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", @@ -2231,15 +2324,26 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -2248,9 +2352,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index c1164b1..4f976c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bourso-cli" -version = "0.3.2" +version = "1.0.0" edition = "2021" authors = ["@azerpas"] description = "BoursoBank/Boursorama CLI" @@ -10,16 +10,20 @@ repository = "https://github.com/azerpas/bourso-api" [dependencies] bourso_api = { path = "./src/bourso_api" } -tokio = { version = "1.33.0", features = ["full"] } -anyhow = { version = "1.0.75" } +tokio = { version = "1.48.0", features = ["full"] } +anyhow = { version = "1.0.100" } clap = { version = "4.5.51", features = ["derive"] } -rpassword = { version = "7.2.0" } -directories = { version = "5.0.1" } +rpassword = { version = "7.4.0" } +directories = { version = "6.0.0" } serde = { version = "1.0.189", features = ["derive"] } -serde_json = { version = "1.0.107" } +serde_json = { version = "1.0.145" } tracing = { version = "0.1.41" } +tracing-appender = { version = "0.2.3" } tracing-subscriber = { version = "0.3.20", features = ["fmt", "env-filter", "json"] } futures-util = { version = "0.3.31" } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } + +[workspace] +members = [".", "src/bourso_api"] diff --git a/src/bourso_api/Cargo.toml b/src/bourso_api/Cargo.toml index 7365fb7..83f21da 100644 --- a/src/bourso_api/Cargo.toml +++ b/src/bourso_api/Cargo.toml @@ -19,6 +19,8 @@ chrono = { version = "0.4.39" } tracing = { version = "0.1.41" } futures-util = { version = "0.3.31" } async-stream = { version = "0.3.6" } +thiserror = { version = "2.0.17" } +derive_more = { version = "2.0.1", features = ["from", "into", "as_ref"] } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } diff --git a/src/bourso_api/src/client/trade/order.rs b/src/bourso_api/src/client/trade/order.rs index 9df813c..41745e4 100644 --- a/src/bourso_api/src/client/trade/order.rs +++ b/src/bourso_api/src/client/trade/order.rs @@ -6,6 +6,7 @@ use tracing::{debug, info}; use crate::{ account::{Account, AccountKind}, client::config::Config, + types::OrderSide, }; use super::{get_trading_base_url, BoursoWebClient}; @@ -526,15 +527,6 @@ pub enum OrderKind { TradeAtLast, } -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default, clap::ValueEnum)] -pub enum OrderSide { - #[default] - #[serde(rename = "B")] - Buy, - #[serde(rename = "S")] - Sell, -} - /// Order data submitted to the `/ordersimple/check` endpoint #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] pub struct OrderData { diff --git a/src/bourso_api/src/lib.rs b/src/bourso_api/src/lib.rs index 7914627..68f0513 100644 --- a/src/bourso_api/src/lib.rs +++ b/src/bourso_api/src/lib.rs @@ -1,8 +1,9 @@ pub mod account; pub mod client; pub mod constants; +pub mod types; #[cfg(not(tarpaulin_include))] pub fn get_client() -> client::BoursoWebClient { client::BoursoWebClient::new() -} \ No newline at end of file +} diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs new file mode 100644 index 0000000..d6af432 --- /dev/null +++ b/src/bourso_api/src/types.rs @@ -0,0 +1,254 @@ +use clap::ValueEnum; +use derive_more::{AsRef, From, Into}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ValueError { + #[error("invalid client number: must be 8 digits (0-9)")] + ClientNumber, + #[error("invalid account id: must be 32 hexadecimal characters (0-9, a-f)")] + AccountId, + #[error("invalid symbol id: must be 6-12 alphanumeric characters (0-9, a-z, A-Z)")] + SymbolId, + #[error("invalid order quantity: must be a positive, non-zero integer")] + OrderQuantity, + #[error("invalid money amount: must be a positive, up to 2 decimal places float")] + MoneyAmount, + #[error("invalid transfer reason: must be 0-50 letters only (a-z, A-Z)")] + TransferReason, + #[error("invalid quote period: must be 0")] + QuotePeriod, + #[error("invalid mfa code: must be 6-12 digits (0-9)")] + MfaCode, + #[error("invalid password: must be a non-empty string")] + Password, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, AsRef, From, Into)] +#[serde(try_from = "String", into = "String")] +pub struct ClientNumber(String); +impl ClientNumber { + pub fn new(s: &str) -> Result { + let t = s.trim(); + if t.len() == 8 && t.chars().all(|c| c.is_ascii_digit()) { + Ok(Self(t.into())) + } else { + Err(ValueError::ClientNumber) + } + } +} +impl FromStr for ClientNumber { + type Err = ValueError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, From, Into)] +pub struct AccountId(String); +impl AccountId { + pub fn new(s: &str) -> Result { + let t = s.trim(); + if t.len() == 32 && t.chars().all(|c| c.is_ascii_hexdigit()) { + Ok(Self(t.into())) + } else { + Err(ValueError::AccountId) + } + } +} +impl FromStr for AccountId { + type Err = ValueError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, From, Into)] +pub struct SymbolId(String); +impl SymbolId { + pub fn new(s: &str) -> Result { + let t = s.trim(); + if (6..=12).contains(&t.len()) && t.chars().all(|c| c.is_ascii_alphanumeric()) { + Ok(Self(t.into())) + } else { + Err(ValueError::SymbolId) + } + } +} +impl FromStr for SymbolId { + type Err = ValueError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, AsRef, From, Into)] +pub struct OrderQuantity(u64); +impl OrderQuantity { + pub fn new(v: u64) -> Result { + if v >= 1 { + Ok(Self(v)) + } else { + Err(ValueError::OrderQuantity) + } + } + pub fn get(self) -> u64 { + self.0 + } +} +impl FromStr for OrderQuantity { + type Err = ValueError; + fn from_str(s: &str) -> Result { + let v: u64 = s.parse().map_err(|_| ValueError::OrderQuantity)?; + Self::new(v) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct MoneyAmount(f64); +impl MoneyAmount { + pub fn new(v: f64) -> Result { + if v > 0.0 && v.fract().abs() <= 0.02 { + Ok(Self(v)) + } else { + Err(ValueError::MoneyAmount) + } + } + pub fn get(self) -> f64 { + self.0 + } +} +impl FromStr for MoneyAmount { + type Err = ValueError; + fn from_str(s: &str) -> Result { + let v: f64 = s.parse().map_err(|_| ValueError::MoneyAmount)?; + Self::new(v) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, From, Into)] +pub struct TransferReason(String); +impl TransferReason { + pub fn new(s: &str) -> Result { + let t = s.trim(); + if t.len() > 50 { + return Err(ValueError::TransferReason); + } + if !t.chars().all(|c| c.is_ascii_alphabetic()) { + return Err(ValueError::TransferReason); + } + Ok(Self(t.into())) + } +} +impl FromStr for TransferReason { + type Err = ValueError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)] +pub enum QuoteLength { + #[value(name = "1")] + D1, + #[value(name = "5")] + D5, + #[value(name = "30")] + D30, + #[value(name = "90")] + D90, + #[value(name = "180")] + D180, + #[value(name = "365")] + D365, + #[value(name = "1825")] + D1825, + #[value(name = "3650")] + D3650, +} +impl QuoteLength { + pub fn days(self) -> i64 { + match self { + QuoteLength::D1 => 1, + QuoteLength::D5 => 5, + QuoteLength::D30 => 30, + QuoteLength::D90 => 90, + QuoteLength::D180 => 180, + QuoteLength::D365 => 365, + QuoteLength::D1825 => 1825, + QuoteLength::D3650 => 3650, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, ValueEnum)] +pub enum OrderSide { + #[serde(rename = "B")] + Buy, + #[serde(rename = "S")] + Sell, +} + +// TODO: support only 0 period for now, add support for other periods +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct QuotePeriod(i64); +impl QuotePeriod { + pub fn new(v: i64) -> Result { + if v == 0 { + Ok(Self(v)) + } else { + Err(ValueError::QuotePeriod) + } + } + pub fn value(self) -> i64 { + self.0 + } +} +impl FromStr for QuotePeriod { + type Err = ValueError; + fn from_str(s: &str) -> Result { + let v: i64 = s.trim().parse().map_err(|_| ValueError::QuotePeriod)?; + Self::new(v) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, From, Into)] +pub struct MfaCode(String); +impl MfaCode { + pub fn new(s: &str) -> Result { + let t = s.trim(); + if (6..=12).contains(&t.len()) && t.chars().all(|c| c.is_ascii_digit()) { + Ok(Self(t.into())) + } else { + Err(ValueError::MfaCode) + } + } +} +impl FromStr for MfaCode { + type Err = ValueError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, AsRef, From, Into)] +#[serde(try_from = "String", into = "String")] +pub struct Password(String); +impl Password { + pub fn new(s: &str) -> Result { + let t = s.trim(); + if !t.is_empty() { + Ok(Self(t.into())) + } else { + Err(ValueError::Password) + } + } +} +impl FromStr for Password { + type Err = ValueError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} diff --git a/src/cli.rs b/src/cli.rs index 875d752..ea90595 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,15 +1,20 @@ -use bourso_api::client::trade::order::OrderSide; use clap::{value_parser, Args, Parser, Subcommand}; +use std::path::PathBuf; + +use bourso_api::types::{ + AccountId, ClientNumber, MoneyAmount, OrderQuantity, OrderSide, QuoteLength, QuotePeriod, + SymbolId, TransferReason, +}; // TODO: add debug option -// TODO: add type to fix primitive obsession (AccountId w/ FromStr impl) and value_parser +// TODO: add type to fix primitive obsession and value_parser (AccountId, QuoteInterval, QuoteLength, ...) #[derive(Parser)] #[command(version, author, about, long_about = None)] pub struct Cli { /// Optional path to credentials JSON file - #[arg(short, long, value_name = "FILE")] - pub credentials: Option, + #[arg(short, long, value_name = "FILE", value_parser = value_parser!(PathBuf))] + pub credentials: Option, #[command(subcommand)] pub command: Commands, @@ -35,9 +40,9 @@ pub enum Commands { #[derive(Args)] pub struct ConfigArgs { - /// Your customer ID - #[arg(short, long, value_name = "ID")] - pub username: String, + /// Your client number + #[arg(short, long, value_name = "ID", value_parser = value_parser!(ClientNumber))] + pub client_number: ClientNumber, } #[derive(Args)] @@ -95,20 +100,20 @@ pub struct OrderListArgs {} #[derive(Args)] pub struct OrderNewArgs { /// Account to use by its ID (32 hex chars), you can get it with the `bourso accounts` command - #[arg(short, long, value_name = "ID", value_parser = parse_account_id)] - pub account: String, + #[arg(short, long, value_name = "ID", value_parser = value_parser!(AccountId))] + pub account: AccountId, - /// Side of the order (buy/sell) - #[arg(long, value_parser = clap::value_parser!(OrderSide))] + /// Side of the order + #[arg(long, default_value = "buy")] pub side: OrderSide, /// Symbol ID of the order (e.g: "1rTCW8") - #[arg(long, value_name = "ID")] - pub symbol: String, + #[arg(long, value_name = "ID", value_parser = value_parser!(SymbolId))] + pub symbol: SymbolId, /// Quantity of the order (e.g: 1) - #[arg(short, long, value_parser = value_parser!(u64).range(1..))] - pub quantity: u64, + #[arg(short, long, value_parser = value_parser!(OrderQuantity))] + pub quantity: OrderQuantity, } #[derive(Args)] @@ -117,20 +122,16 @@ pub struct OrderCancelArgs {} #[derive(Args)] pub struct QuoteArgs { /// Symbol ID of the stock (e.g: "1rTCW8") - #[arg(long, value_name = "ID")] - pub symbol: String, + #[arg(long, value_name = "ID", value_parser = value_parser!(SymbolId))] + pub symbol: SymbolId, - /// Length period of the stock (1, 5, 30, 90, 180, 365, 1825, 3650) - #[arg( - long, - default_value = "30", - value_parser = ["1","5","30","90","180","365","1825","3650"] - )] - pub length: String, + /// Length period of the stock + #[arg(long, default_value = "30")] + pub length: QuoteLength, - /// Interval of the stock (use "0" for default) - #[arg(long, default_value = "0", value_parser = ["0"])] - pub interval: String, + /// Period of the stock + #[arg(long, default_value = "0", value_parser = value_parser!(QuotePeriod))] + pub period: QuotePeriod, #[command(subcommand)] pub view: Option, @@ -157,27 +158,18 @@ pub enum QuoteView { #[derive(Args)] pub struct TransferArgs { /// Source account ID (32 hex chars), you can get it with the `bourso accounts` command - #[arg(long = "from", value_name = "ID", value_parser = parse_account_id)] - pub from_account: String, + #[arg(long = "from", value_name = "ID", value_parser = value_parser!(AccountId))] + pub from_account: AccountId, /// Destination account ID (32 hex chars), you can get it with the `bourso accounts` command - #[arg(long = "to", value_name = "ID", value_parser = parse_account_id)] - pub to_account: String, + #[arg(long = "to", value_name = "ID", value_parser = value_parser!(AccountId))] + pub to_account: AccountId, /// Amount to transfer - #[arg(long)] - pub amount: String, + #[arg(long, value_parser = value_parser!(MoneyAmount))] + pub amount: MoneyAmount, /// Reason for the transfer (max 50 chars) - #[arg(long)] - pub reason: Option, -} - -fn parse_account_id(s: &str) -> Result { - let t = s.trim(); - if t.len() == 32 && t.chars().all(|c| c.is_ascii_hexdigit()) { - Ok(t.to_owned()) - } else { - Err("Account ID must be 32 hex characters (0-9, a-f)".into()) - } + #[arg(long, value_parser = value_parser!(TransferReason))] + pub reason: Option, } diff --git a/src/commands/accounts.rs b/src/commands/accounts.rs new file mode 100644 index 0000000..aad6f31 --- /dev/null +++ b/src/commands/accounts.rs @@ -0,0 +1,31 @@ +use anyhow::Result; +use tracing::info; + +use crate::{cli::AccountsArgs, services::AuthService, AppCtx}; + +use bourso_api::account::{Account, AccountKind}; + +pub async fn handle(args: AccountsArgs, ctx: &AppCtx) -> Result<()> { + let auth_service = AuthService::with_defaults(ctx.settings_store.as_ref()); + + let Some(client) = auth_service.login().await? else { + return Ok(()); + }; + + let kind = if args.banking { + Some(AccountKind::Banking) + } else if args.saving { + Some(AccountKind::Savings) + } else if args.trading { + Some(AccountKind::Trading) + } else if args.loans { + Some(AccountKind::Loans) + } else { + None + }; + + let accounts: Vec = client.get_accounts(kind).await?; + info!("Found {} accounts", accounts.len()); + println!("{:#?}", accounts); + Ok(()) +} diff --git a/src/commands/config.rs b/src/commands/config.rs new file mode 100644 index 0000000..5d97fa1 --- /dev/null +++ b/src/commands/config.rs @@ -0,0 +1,13 @@ +use anyhow::Result; +use tracing::info; + +use crate::{cli::ConfigArgs, settings::Settings, AppCtx}; + +pub async fn handle(args: ConfigArgs, ctx: &AppCtx) -> Result<()> { + ctx.settings_store.save(&Settings { + client_number: Some(args.client_number), + password: None, + })?; + info!("Configuration saved successfully ✅"); + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..9b6829e --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,5 @@ +pub mod accounts; +pub mod config; +pub mod quote; +pub mod trade; +pub mod transfer; diff --git a/src/commands/quote.rs b/src/commands/quote.rs new file mode 100644 index 0000000..9d0d379 --- /dev/null +++ b/src/commands/quote.rs @@ -0,0 +1,48 @@ +use anyhow::Result; +use tracing::info; + +use crate::cli::{QuoteArgs, QuoteView}; + +pub async fn handle(args: QuoteArgs) -> Result<()> { + info!("Fetching quotes ..."); + + let client = bourso_api::get_client(); + let quotes = client + .get_ticks( + args.symbol.as_ref(), + args.length.days(), + args.period.value(), + ) + .await?; + + match args.view { + Some(QuoteView::Highest) => { + let highest = quotes.d.get_highest_value(); + info!(?highest, "Highest quote"); + } + Some(QuoteView::Lowest) => { + let lowest = quotes.d.get_lowest_value(); + info!(?lowest, "Lowest quote"); + } + Some(QuoteView::Average) => { + let average = quotes.d.get_average_value(); + info!(?average, "Average quote"); + } + Some(QuoteView::Volume) => { + let volume = quotes.d.get_volume(); + info!(?volume, "Volume"); + } + Some(QuoteView::Last) => { + let last = quotes.d.get_last_quote(); + info!(?last, "Last quote"); + } + None => { + info!("No view specified, displaying all quotes"); + for quote in quotes.d.get_quotes() { + info!(?quote, "Quote"); + } + } + } + + Ok(()) +} diff --git a/src/commands/trade/mod.rs b/src/commands/trade/mod.rs new file mode 100644 index 0000000..f32bbec --- /dev/null +++ b/src/commands/trade/mod.rs @@ -0,0 +1,14 @@ +use anyhow::Result; + +use crate::{ + cli::{TradeArgs, TradeCommands}, + AppCtx, +}; + +pub mod order; + +pub async fn handle(args: TradeArgs, ctx: &AppCtx) -> Result<()> { + match args.command { + TradeCommands::Order(o) => order::handle(o, ctx).await, + } +} diff --git a/src/commands/trade/order.rs b/src/commands/trade/order.rs new file mode 100644 index 0000000..7c92171 --- /dev/null +++ b/src/commands/trade/order.rs @@ -0,0 +1,51 @@ +use anyhow::{Context, Result}; +use tracing::{info, warn}; + +use crate::{ + cli::{OrderArgs, OrderNewArgs, OrderSubcommands}, + services::AuthService, + AppCtx, +}; + +use bourso_api::account::AccountKind; +use bourso_api::types::OrderSide; + +pub async fn handle(args: OrderArgs, ctx: &AppCtx) -> Result<()> { + match args.command { + OrderSubcommands::New(n) => new_order(n, ctx).await, + OrderSubcommands::List(_) => { + warn!("Listing orders is coming soon."); + Ok(()) + } + OrderSubcommands::Cancel(_) => { + warn!("Cancel order is coming soon."); + Ok(()) + } + } +} + +async fn new_order(args: OrderNewArgs, ctx: &AppCtx) -> Result<()> { + let auth = AuthService::with_defaults(ctx.settings_store.as_ref()); + + let Some(client) = auth.login().await? else { + return Ok(()); + }; + + // Choose a trading account and place the order + let accounts = client.get_accounts(Some(AccountKind::Trading)).await?; + let account = accounts + .iter() + .find(|a| a.id == args.account.as_ref().as_str()) // TODO: compare AccountId instead of String + .context("Account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; + + let side: OrderSide = args.side; + let quantity: usize = args.quantity.get() as usize; + let symbol = args.symbol; + + let _ = client + .order(side, account, symbol.as_ref(), quantity, None) + .await?; + + info!("Order submitted ✅"); + Ok(()) +} diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs new file mode 100644 index 0000000..2e23808 --- /dev/null +++ b/src/commands/transfer.rs @@ -0,0 +1,54 @@ +use anyhow::{Context, Result}; +use futures_util::{pin_mut, StreamExt}; +use tracing::info; + +use crate::{cli::TransferArgs, services::AuthService, ux::progress::TextProgressBar, AppCtx}; + +use bourso_api::client::transfer::TransferProgress; + +pub async fn handle(args: TransferArgs, ctx: &AppCtx) -> Result<()> { + let auth_service = AuthService::with_defaults(ctx.settings_store.as_ref()); + + let Some(client) = auth_service.login().await? else { + return Ok(()); + }; + + let accounts = client.get_accounts(None).await?; + + let from_account = accounts + .iter() + .find(|a| a.id == args.from_account.as_ref().as_str()) // TODO: compare AccountId instead of String + .context("From account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; + + let to_account = accounts + .iter() + .find(|a| a.id == args.to_account.as_ref().as_str()) // TODO: compare AccountId instead of String + .context("To account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; + + let stream = client.transfer_funds( + args.amount.get(), + from_account.clone(), + to_account.clone(), + args.reason.map(|r| r.as_ref().to_string()), + ); + + let bar = TextProgressBar::new(30usize); + pin_mut!(stream); + while let Some(progress_result) = stream.next().await { + let progress = progress_result?; + let step = progress.step_number() as usize; + let total = TransferProgress::total_steps() as usize; + + bar.render(step, total, progress.description()); + } + bar.finish(); + + info!( + "Transfer of {} from account {} to account {} successful ✅", + args.amount.get(), + from_account.id, + to_account.id + ); + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index bd8466f..e553c0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,343 +1,36 @@ -use anyhow::{Context, Result}; -use bourso_api::{ - account::{Account, AccountKind}, - client::{ - trade::{order::OrderSide, tick::QuoteTab}, - transfer::TransferProgress, - BoursoWebClient, - }, - get_client, -}; -use clap::ArgMatches; -use futures_util::{pin_mut, StreamExt}; -use tracing::{debug, info, warn}; +use anyhow::Result; pub mod cli; +pub mod commands; +pub mod services; pub mod settings; +pub mod ux; -use settings::{get_settings, save_settings, Settings}; +pub use services::AuthService; +pub use settings::{init_logger, FsSettingsStore, Settings, SettingsStore}; +pub use ux::TextProgressBar; -#[cfg(not(tarpaulin_include))] -pub async fn parse_matches(matches: ArgMatches) -> Result<()> { - let settings = match matches.get_one::("credentials") { - Some(credentials_path) => Settings::load(credentials_path)?, - None => get_settings()?, - }; - - info!("Welcome to BoursoBank CLI 👋"); - info!( - "â„šī¸ - Version {}. Make sure you're running the latest version: {}", - env!("CARGO_PKG_VERSION"), - env!("CARGO_PKG_REPOSITORY") - ); - println!(""); - - match matches.subcommand() { - // These matches do not require authentication - Some(("config", config_matches)) => { - let customer_id = config_matches - .get_one::("username") - .map(|s| s.as_str()) - .unwrap(); - save_settings(&Settings { - customer_id: Some(customer_id.to_string()), - password: None, - })?; - info!("Configuration saved ✅"); - return Ok(()); - } - Some(("quote", quote_matches)) => { - info!("Fetching quotes..."); - - let symbol = quote_matches - .get_one::("symbol") - .map(|s| s.as_str()) - .unwrap(); - let length = quote_matches - .get_one::("length") - .map(|s| s.as_str()) - .unwrap(); - let interval = quote_matches - .get_one::("interval") - .map(|s| s.as_str()) - .unwrap(); - let web_client: BoursoWebClient = get_client(); - - let quotes = web_client - .get_ticks(symbol, length.parse()?, interval.parse()?) - .await?; - - match quote_matches.subcommand() { - Some(("highest", _)) => { - let highest_quote = quotes.d.get_highest_value(); - info!(highest_quote, "Highest quote: {:#?}", highest_quote); - } - Some(("lowest", _)) => { - let lowest_quote = quotes.d.get_lowest_value(); - info!(lowest_quote, "Lowest quote: {:#?}", lowest_quote); - } - Some(("volume", _)) => { - let volume = quotes.d.get_volume(); - info!(volume, "Volume: {:#?}", volume); - } - Some(("average", _)) => { - let average_quote = quotes.d.get_average_value(); - info!(average_quote, "Average quote: {:#?}", average_quote); - } - Some(("last", _)) => { - let quote: QuoteTab; - - let last_quote = quotes.d.get_last_quote(); - if last_quote.is_some() { - quote = last_quote.unwrap(); - } else { - quote = quotes.d.quote_tab.last().unwrap().clone(); - } - - info!( - close = quote.close, open = quote.open, high = quote.high, low = quote.low, volume = quote.volume, - "Last quote: current: {:#?}, open: {:#?}, high: {:#?}, low: {:#?}, volume: {:#?}", - quote.close, quote.open, quote.high, quote.low, quote.volume - ); - } - _ => { - info!("Quotes:"); - for quote in quotes.d.quote_tab.iter() { - info!( - date = quote.date, close = quote.close, open = quote.open, high = quote.high, low = quote.low, volume = quote.volume, - "Quote day {:#?}: Close: {:#?}, Open: {:#?}, High: {:#?}, Low: {:#?}, Volume: {:#?}", - quote.date, quote.close, quote.open, quote.high, quote.low, quote.volume, - ); - } - } - } - - return Ok(()); - } - // These matches require authentication - Some(("accounts", _)) - | Some(("transactions", _)) - | Some(("balance", _)) - | Some(("trade", _)) - | Some(("transfer", _)) => (), - _ => unreachable!(), - } - - if settings.customer_id.is_none() { - warn!("Please configure your customer id with `bourso config --username `"); - return Ok(()); - } - let customer_id = settings.customer_id.unwrap(); +pub struct AppCtx { + pub settings_store: Box, +} - info!( - "We'll try to log you in with your customer id: {}", - customer_id - ); - info!("If you want to change it, run `bourso config --username `"); - println!(""); - info!("We'll need your password to log you in. It will not be stored anywhere and will be asked everytime you run a command. The password will be hidden while typing."); +pub async fn run(cli: cli::Cli) -> Result<()> { + let cli::Cli { + credentials, + command, + } = cli; - // Get password from stdin - let password = match settings.password { - Some(password) => password, - None => rpassword::prompt_password("Enter your password: ") - .context("Failed to read password")? - .trim() - .to_string(), + let settings_store: Box = match credentials { + Some(path) => Box::new(FsSettingsStore::from_path(path)), + None => Box::new(FsSettingsStore::from_default_config_dir()?), }; - - let mut web_client: BoursoWebClient = get_client(); - web_client.init_session().await?; - match web_client.login(&customer_id, &password).await { - Ok(_) => { - info!("Login successful ✅"); - } - Err(e) => match e.downcast_ref() { - Some(bourso_api::client::error::ClientError::MfaRequired) => { - let mut mfa_required = true; - let mut mfa_count = 0; - while mfa_required { - // If MFA is passed twice, it means the user has passed an sms and email mfa - // which should clear the IP. We just need to reinitialize the session - // and login again to access the account. - if mfa_count == 2 { - warn!("MFA thresold reached. Trying to login again by reinitalizing the session."); - web_client = get_client(); - web_client.init_session().await?; - match web_client.login(&customer_id, &password).await { - Ok(_) => { - info!("Login successful ✅"); - break; - } - Err(e) => { - debug!("{:#?}", e); - return Err(e); - } - } - } - warn!("An MFA is required."); - - let (otp_id, token, mfa_type) = web_client.request_mfa().await?; - let code = rpassword::prompt_password("Enter your MFA code: ") - .context("Failed to read MFA code")? - .trim() - .to_string(); - match web_client.submit_mfa(mfa_type, otp_id, code, token).await { - Ok(_) => { - mfa_required = false; - } - Err(e) => match e.downcast_ref() { - Some(bourso_api::client::error::ClientError::MfaRequired) => { - mfa_count += 1; - } - _ => { - debug!("{:#?}", e); - return Err(e); - } - }, - } - } - - info!("MFA successful ✅"); - } - _ => { - debug!("{:#?}", e); - return Err(e); - } - }, + let ctx = AppCtx { settings_store }; + + match command { + cli::Commands::Config(args) => commands::config::handle(args, &ctx).await, + cli::Commands::Accounts(args) => commands::accounts::handle(args, &ctx).await, + cli::Commands::Trade(args) => commands::trade::handle(args, &ctx).await, + cli::Commands::Quote(args) => commands::quote::handle(args).await, + cli::Commands::Transfer(args) => commands::transfer::handle(args, &ctx).await, } - - let accounts: Vec; - - match matches.subcommand() { - Some(("accounts", sub_matches)) => { - if sub_matches.get_flag("banking") { - accounts = web_client.get_accounts(Some(AccountKind::Banking)).await?; - } else if sub_matches.get_flag("saving") { - accounts = web_client.get_accounts(Some(AccountKind::Savings)).await?; - } else if sub_matches.get_flag("trading") { - accounts = web_client.get_accounts(Some(AccountKind::Trading)).await?; - } else if sub_matches.get_flag("loans") { - accounts = web_client.get_accounts(Some(AccountKind::Loans)).await?; - } else { - accounts = web_client.get_accounts(None).await?; - } - - info!("Found {} accounts", accounts.len()); - println!("{:#?}", accounts); - } - - Some(("trade", trade_matches)) => { - accounts = web_client.get_accounts(Some(AccountKind::Trading)).await?; - - match trade_matches.subcommand() { - Some(("order", order_matches)) => { - match order_matches.subcommand() { - Some(("new", new_order_matches)) => { - let account_id = new_order_matches - .get_one::("account") - .map(|s| s.as_str()) - .unwrap(); - - // Get account from previously fetched accounts - let account = accounts - .iter() - .find(|a| a.id == account_id) - .context("Account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; - - let side = new_order_matches.get_one::("side").unwrap(); - let quantity = new_order_matches.get_one::("quantity").unwrap(); - let symbol = new_order_matches - .get_one::("symbol") - .map(|s| s.as_str()) - .unwrap(); - - let _ = web_client - .order(side.to_owned(), account, symbol, quantity.to_owned(), None) - .await?; - } - _ => unreachable!(), - } - } - _ => unreachable!(), - } - } - - Some(("transfer", transfer_matches)) => { - accounts = web_client.get_accounts(None).await?; - - let from_account_id = transfer_matches - .get_one::("from_account") - .map(|s| s.as_str()) - .unwrap(); - let to_account_id = transfer_matches - .get_one::("to_account") - .map(|s| s.as_str()) - .unwrap(); - let amount = transfer_matches - .get_one::("amount") - .map(|s| s.parse::().unwrap()) - .unwrap(); - let reason = transfer_matches - .get_one::("reason") - .map(|s| s.as_str()); - - // Get from_account from previously fetched accounts - let from_account = accounts - .iter() - .find(|a| a.id == from_account_id) - .context("From account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; - - // Get to_account from previously fetched accounts - let to_account = accounts - .iter() - .find(|a| a.id == to_account_id) - .context("To account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; - - let stream = web_client.transfer_funds( - amount, - from_account.clone(), - to_account.clone(), - reason.map(|s| s.to_string()), - ); - - pin_mut!(stream); - - // Track progress and update display - while let Some(progress_result) = stream.next().await { - let progress = progress_result?; - let step = progress.step_number(); - let total = TransferProgress::total_steps(); - let percentage = (step as f32 / total as f32 * 100.0) as u8; - - // Create a simple progress bar - let bar_length = 30; - let filled = (bar_length as f32 * step as f32 / total as f32) as usize; - let bar: String = "█".repeat(filled) + &"░".repeat(bar_length - filled); - - // Use ANSI escape code to clear the line before printing - // \x1B[2K clears the entire line, \r returns cursor to start - print!( - "\x1B[2K\r[{}] {:3}% - {}/{} - {}", - bar, - percentage, - step, - total, - progress.description() - ); - use std::io::Write; - std::io::stdout().flush().unwrap(); - } - println!(); // New line after progress is complete - - info!( - "Transfer of {} from account {} to account {} successful ✅", - amount, from_account.id, to_account.id - ); - } - - _ => unreachable!(), - } - - Ok(()) } diff --git a/src/main.rs b/src/main.rs index 254b533..45811e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ use anyhow::Result; -use clap::CommandFactory; +use clap::Parser; + +use bourso_cli::cli::Cli; +use bourso_cli::{init_logger, run}; #[tokio::main] async fn main() -> Result<()> { - bourso_cli::settings::init_logger()?; - - let matches = bourso_cli::cli::Cli::command().get_matches(); - - bourso_cli::parse_matches(matches).await + init_logger()?; + let cli = Cli::parse(); + run(cli).await?; + Ok(()) } diff --git a/src/services/auth.rs b/src/services/auth.rs new file mode 100644 index 0000000..0a8bba7 --- /dev/null +++ b/src/services/auth.rs @@ -0,0 +1,154 @@ +use anyhow::Result; +use std::io::{stdout, Write}; +use tracing::{info, warn}; + +use crate::settings::SettingsStore; +use bourso_api::{ + client::{error::ClientError, BoursoWebClient}, + types::{ClientNumber, MfaCode, Password}, +}; + +// TODO: does it make sense to have MFA handling in the CLI? + +pub trait CredentialsProvider { + fn read_password(&self) -> Result; + fn read_mfa_code(&self) -> Result; +} +pub struct StdinCredentialsProvider; +impl CredentialsProvider for StdinCredentialsProvider { + fn read_password(&self) -> Result { + print!("\nEnter your password (hidden): "); + let _ = stdout().flush(); + let password = Password::new(&rpassword::read_password()?)?; + println!(); + Ok(password) + } + fn read_mfa_code(&self) -> Result { + print!("\nEnter your MFA code (hidden): "); + let _ = stdout().flush(); + let mfa_code = MfaCode::new(&rpassword::read_password()?)?; + println!(); + Ok(mfa_code) + } +} + +pub trait ClientFactory { + fn new_client(&self) -> BoursoWebClient; +} +pub struct DefaultClientFactory; +impl ClientFactory for DefaultClientFactory { + fn new_client(&self) -> BoursoWebClient { + bourso_api::get_client() + } +} + +pub struct AuthService<'a> { + settings_store: &'a dyn SettingsStore, + credentials_provider: Box, + client_factory: Box, +} + +impl<'a> AuthService<'a> { + pub fn new( + settings_store: &'a dyn SettingsStore, + credentials_provider: Box, + client_factory: Box, + ) -> Self { + Self { + settings_store, + credentials_provider, + client_factory, + } + } + + pub fn with_defaults(settings_store: &'a dyn SettingsStore) -> Self { + Self::new( + settings_store, + Box::new(StdinCredentialsProvider), + Box::new(DefaultClientFactory), + ) + } + + pub async fn login(&self) -> Result> { + let settings = self.settings_store.load()?; + let Some(client_number) = settings.client_number.as_ref() else { + warn!("No client number found in settings, please run `bourso config` to set it"); + return Ok(None); + }; + + info!( + "We'll try to log you in with your customer id: {:?}", + client_number.as_ref() + ); + info!("If you want to change it, you can run `bourso config` to set it"); + println!(); + + let password = match settings.password.as_ref() { + Some(password) => password, + None => { + info!("We'll need your password to log you in. It will not be stored."); + &self.credentials_provider.read_password()? + } + }; + + let mut client = self.client_factory.new_client(); + client.init_session().await?; + match client + .login(client_number.as_ref(), password.as_ref()) + .await + { + Ok(_) => { + info!("Login successful ✅"); + Ok(Some(client)) + } + Err(e) => { + if let Some(ClientError::MfaRequired) = e.downcast_ref::() { + self.handle_mfa(client, client_number, password).await + } else { + Err(e) + } + } + } + } + + async fn handle_mfa( + &self, + mut client: BoursoWebClient, + client_number: &ClientNumber, + password: &Password, + ) -> Result> { + let mut mfa_count = 0usize; + loop { + if mfa_count == 2 { + warn!("MFA threshold reached. Reinitializing session and logging in again."); + client.init_session().await?; + client + .login(client_number.as_ref(), password.as_ref()) + .await?; + info!("Login successful ✅"); + return Ok(Some(client)); + } + + let (otp_id, token_form, mfa_type) = client.request_mfa().await?; + let code = &self.credentials_provider.read_mfa_code()?; + + match client + .submit_mfa(mfa_type, otp_id, code.as_ref().to_string(), token_form) + .await + { + Ok(_) => { + info!("MFA successfully submitted ✅"); + return Ok(Some(client)); + } + Err(e) => { + if let Some(ClientError::MfaRequired) = e.downcast_ref::() { + mfa_count += 1; + continue; + } else { + return Err(e); + } + } + } + } + } +} diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 0000000..b9325a5 --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1,3 @@ +pub mod auth; + +pub use auth::AuthService; diff --git a/src/settings.rs b/src/settings.rs deleted file mode 100644 index 580fd7c..0000000 --- a/src/settings.rs +++ /dev/null @@ -1,136 +0,0 @@ -use anyhow::{Context, Result}; -use directories::UserDirs; -use serde::{Deserialize, Serialize}; -use std::fs; -use std::io::prelude::*; - -#[derive(Serialize, Deserialize, Debug)] -pub struct Settings { - #[serde(rename = "clientId")] - pub customer_id: Option, - #[serde(rename = "password")] - pub password: Option, -} - -#[cfg(not(tarpaulin_include))] -impl Settings { - pub fn load(path: &str) -> Result { - let file_content = match fs::read_to_string(path) { - Ok(data) => data, - Err(_) => { - return Err(anyhow::anyhow!("Failed to read settings file")); - } - }; - - let settings: Settings = serde_json::from_str(&file_content).map_err(|e| { - anyhow::anyhow!( - "Failed to deserialize settings: {}\nPlease make sure the settings file is valid.", - e - ) - })?; - - Ok(settings) - } -} - -#[cfg(not(tarpaulin_include))] -pub fn get_settings() -> Result { - let user_dirs = UserDirs::new().context("Failed to get user directories")?; - let mut path = user_dirs.home_dir().to_path_buf(); - path = path.join(".bourso/settings.json"); - let file_content = match fs::read_to_string(&path) { - Ok(data) => data, - Err(_) => { - // Create the settings file if it doesn't exist - save_settings(&Settings { - customer_id: None, - password: None, - })?; - return Ok(Settings { - customer_id: None, - password: None, - }); - } - }; - - let settings: Settings = serde_json::from_str(&file_content).map_err(|e| { - anyhow::anyhow!( - "Failed to deserialize settings: {}\nPlease make sure the settings file is valid.", - e - ) - })?; - Ok(settings) -} - -/// Save the settings to the settings file, if it doesn't exist, create it -#[cfg(not(tarpaulin_include))] -pub fn save_settings(settings: &Settings) -> Result<()> { - let user_dirs = UserDirs::new().context("Failed to get user directories")?; - let mut path = user_dirs.home_dir().to_path_buf(); - // Create the .bourso directory if it doesn't exist - path = path.join(".bourso"); - fs::create_dir_all(&path)?; - path = path.join("settings.json"); - let mut file = fs::File::create(&path).context("Failed to create settings file")?; - let json = serde_json::to_string_pretty(settings).context("Failed to serialize settings")?; - file.write_all(json.as_bytes()) - .context("Failed to write settings file")?; - Ok(()) -} - -pub fn init_logger() -> Result<()> { - use std::io::IsTerminal; - use std::{fs, io}; - use tracing_subscriber::filter::LevelFilter; - use tracing_subscriber::{fmt, prelude::*, EnvFilter}; - - let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); - - // Create ~/.bourso/bourso.log if it doesn't exist - let user_dirs = UserDirs::new().context("Failed to get user directories")?; - let mut path = user_dirs.home_dir().to_path_buf(); - path.push(".bourso"); - fs::create_dir_all(&path)?; - path.push("bourso.log"); - - // Pretty console (stderr), filtered by RUST_LOG - let console_layer = fmt::layer() - .with_writer(io::stderr) - .with_ansi(IsTerminal::is_terminal(&io::stderr())) - .with_level(true) - .with_target(true) - .without_time() - .compact() - .fmt_fields({ - fmt::format::debug_fn(move |writer, field, value| { - if field.name() == "message" { - write!(writer, "{:?}", value)?; - } - Ok(()) - }) - }) - .with_filter(env_filter.clone()); - - // JSON file (capture everything) - let log_path = path.clone(); - let json_layer = fmt::layer() - .with_writer(move || { - fs::OpenOptions::new() - .create(true) - .append(true) - .open(&log_path) - .expect("open ~/.bourso/bourso.log") - }) - .json() - .with_target(true) - .with_level(true) - .flatten_event(true) - .with_filter(LevelFilter::TRACE); - - tracing_subscriber::registry() - .with(console_layer) - .with(json_layer) - .init(); - - Ok(()) -} diff --git a/src/settings/constants.rs b/src/settings/constants.rs new file mode 100644 index 0000000..5803a56 --- /dev/null +++ b/src/settings/constants.rs @@ -0,0 +1,8 @@ +pub const APP_QUALIFIER: &str = ""; +pub const APP_ORGANIZATION: &str = "bourso"; +pub const APP_NAME: &str = "bourso-cli"; + +pub const SETTINGS_FILE: &str = "settings.json"; +pub const LOG_FILE: &str = "bourso.log"; + +pub const DEFAULT_LOG_LEVEL: &str = "info"; diff --git a/src/settings/logging.rs b/src/settings/logging.rs new file mode 100644 index 0000000..9d8db4b --- /dev/null +++ b/src/settings/logging.rs @@ -0,0 +1,58 @@ +use anyhow::{anyhow, Result}; +use directories::ProjectDirs; +use std::{ + fs::create_dir_all, + io::{stderr, IsTerminal}, +}; +use tracing_appender::rolling; +use tracing_subscriber::{ + filter::LevelFilter, + fmt::{self, format::debug_fn}, + prelude::*, + registry, EnvFilter, +}; + +use crate::settings::constants::{ + APP_NAME, APP_ORGANIZATION, APP_QUALIFIER, DEFAULT_LOG_LEVEL, LOG_FILE, +}; + +pub fn init_logger() -> Result<()> { + let project_dirs = ProjectDirs::from(APP_QUALIFIER, APP_ORGANIZATION, APP_NAME) + .ok_or_else(|| anyhow!("Could not determine project directories"))?; + + let directory = project_dirs.data_dir(); + create_dir_all(directory)?; + + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(DEFAULT_LOG_LEVEL)); + + let file_appender = rolling::never(directory, LOG_FILE); + + let console_layer = fmt::layer() + .with_writer(stderr) + .with_ansi(IsTerminal::is_terminal(&stderr())) + .with_level(true) + .without_time() + .compact() + .fmt_fields({ + debug_fn(move |writer, field, value| { + if field.name() == "message" { + write!(writer, "{:?}", value)?; + } + Ok(()) + }) + }) + .with_filter(env_filter); + + let json_layer = fmt::layer() + .json() + .with_writer(file_appender) + .with_target(true) + .with_level(true) + .flatten_event(true) + .with_filter(LevelFilter::TRACE); + + registry().with(console_layer).with(json_layer).init(); + + Ok(()) +} diff --git a/src/settings/mod.rs b/src/settings/mod.rs new file mode 100644 index 0000000..e1fdc86 --- /dev/null +++ b/src/settings/mod.rs @@ -0,0 +1,6 @@ +mod constants; +mod logging; +mod store; + +pub use logging::init_logger; +pub use store::{FsSettingsStore, Settings, SettingsStore}; diff --git a/src/settings/store.rs b/src/settings/store.rs new file mode 100644 index 0000000..fcbbc11 --- /dev/null +++ b/src/settings/store.rs @@ -0,0 +1,83 @@ +use anyhow::{anyhow, Context, Result}; +use bourso_api::types::{ClientNumber, Password}; +use directories::ProjectDirs; +use serde::{Deserialize, Serialize}; +use serde_json::{from_str, to_string_pretty}; +use std::{ + fs::{create_dir_all, read_to_string, write}, + io::ErrorKind, + path::PathBuf, +}; + +use crate::settings::constants::{APP_NAME, APP_ORGANIZATION, APP_QUALIFIER, SETTINGS_FILE}; + +#[derive(Serialize, Deserialize, Default)] +pub struct Settings { + #[serde(rename = "clientNumber")] + pub client_number: Option, + #[serde(rename = "password")] + pub password: Option, +} + +pub trait SettingsStore { + fn load(&self) -> Result; + fn save(&self, settings: &Settings) -> Result<()>; +} + +pub struct FsSettingsStore { + path: PathBuf, + create_if_missing: bool, +} + +impl FsSettingsStore { + /// Default location (XDG / platform config dir + SETTINGS_FILE) + pub fn from_default_config_dir() -> Result { + let project_dirs = ProjectDirs::from(APP_QUALIFIER, APP_ORGANIZATION, APP_NAME) + .ok_or_else(|| anyhow!("Could not determine project directories"))?; + + Ok(Self { + path: project_dirs.config_dir().join(SETTINGS_FILE), + create_if_missing: true, + }) + } + + /// Arbitrary path (e.g. provided via CLI) + pub fn from_path(path: PathBuf) -> Self { + Self { + path, + create_if_missing: false, + } + } + + fn ensure_directory(&self) -> Result<()> { + if let Some(directory) = self.path.parent() { + create_dir_all(directory).context("Failed to create settings directory")?; + } + Ok(()) + } +} + +impl SettingsStore for FsSettingsStore { + fn load(&self) -> Result { + self.ensure_directory()?; + + match read_to_string(&self.path) { + Ok(content) => from_str(&content).context("Failed to deserialize settings"), + + Err(e) if self.create_if_missing && e.kind() == ErrorKind::NotFound => { + // Only for "default config" mode AND only if the file is missing + let defaults = Settings::default(); + self.save(&defaults)?; + Ok(defaults) + } + + Err(e) => Err(e).context("Failed to read settings file"), + } + } + + fn save(&self, settings: &Settings) -> Result<()> { + self.ensure_directory()?; + + write(&self.path, to_string_pretty(settings)?).context("Failed to persist settings file") + } +} diff --git a/src/ux/mod.rs b/src/ux/mod.rs new file mode 100644 index 0000000..0f257a9 --- /dev/null +++ b/src/ux/mod.rs @@ -0,0 +1,3 @@ +pub mod progress; + +pub use progress::TextProgressBar; diff --git a/src/ux/progress.rs b/src/ux/progress.rs new file mode 100644 index 0000000..ee52d79 --- /dev/null +++ b/src/ux/progress.rs @@ -0,0 +1,33 @@ +use std::io::{stdout, Write}; + +pub struct TextProgressBar { + width: usize, +} + +impl TextProgressBar { + pub fn new(width: usize) -> Self { + Self { width } + } + + pub fn render(&self, step: usize, total: usize, description: &str) { + let (percentage, filled) = if total > 0 { + let percentage = (step as f32 / total as f32 * 100.0).clamp(0.0, 100.0); + let filled = ((self.width as f32) * (step as f32 / total as f32)) as usize; + (percentage, filled) + } else { + (0.0, 0usize) + }; + + let bar = format!("{}{}", "█".repeat(filled), "░".repeat(self.width - filled)); + + print!( + "\x1B[2K\r[{}] {:3.0}% - {}/{} - {}", + bar, percentage, step, total, description + ); + let _ = stdout().flush(); + } + + pub fn finish(&self) { + println!(); + } +}