diff --git a/Cargo.lock b/Cargo.lock index ce0a719e6..8e04c126f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[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", ] @@ -49,9 +49,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -64,29 +64,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "assert_cmd" @@ -103,11 +103,17 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -167,15 +173,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ "aws-lc-sys", "zeroize", @@ -183,9 +189,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" dependencies = [ "cc", "cmake", @@ -201,35 +207,41 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.4" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" -version = "0.10.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" dependencies = [ "memchr", "regex-automata", - "serde", + "serde_core", ] [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" @@ -239,9 +251,9 @@ checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "camino" -version = "1.2.2" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" dependencies = [ "serde_core", ] @@ -273,9 +285,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "jobserver", @@ -283,12 +295,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -303,13 +309,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.0", + "cpufeatures", + "rand_core 0.10.1", ] [[package]] @@ -323,7 +329,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -372,24 +378,24 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -403,9 +409,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "compression-core", "flate2", @@ -414,9 +420,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "console" @@ -431,6 +437,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "core-foundation" version = "0.9.4" @@ -457,15 +469,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -501,12 +504,11 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "generic-array", - "typenum", + "hybrid-array", ] [[package]] @@ -516,14 +518,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe7ed1d93f4553003e20b629abe9085e1e81b1429520f897f8f8860bc6dfc21" [[package]] -name = "deranged" -version = "0.5.4" +name = "defmt" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" dependencies = [ - "powerfmt", + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", ] +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "difflib" version = "0.4.0" @@ -532,19 +563,20 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "digest" -version = "0.10.7" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer", + "const-oid", "crypto-common", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -553,9 +585,9 @@ dependencies = [ [[package]] name = "dropshot" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "409eb76c7ea1623d270393248ff55ec436841fcd724c2e1c9de294291edd35f5" +checksum = "803c4a57fd2a611df2ddd7069a62a565253368ee96ad4e5ac2e74e625dcf8430" dependencies = [ "async-compression", "async-stream", @@ -578,7 +610,7 @@ dependencies = [ "openapiv3", "paste", "percent-encoding", - "rustls 0.22.4", + "rustls", "rustls-pemfile", "schemars", "scopeguard", @@ -595,9 +627,9 @@ dependencies = [ "slog-term", "thiserror 2.0.18", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls", "tokio-util", - "toml 1.0.7+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "uuid", "version_check", "waitgroup", @@ -605,9 +637,9 @@ dependencies = [ [[package]] name = "dropshot_endpoint" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "906c3adfd4472030607130ed763e9af1b85f7e18832dd22998379d42ff81c28d" +checksum = "5f176851eb52822c728ad7a1c5aea0e3c95e68d9876cfa06ae32d5018730acb4" dependencies = [ "heck", "proc-macro2", @@ -647,9 +679,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "1.0.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", @@ -657,9 +689,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "anstream", "anstyle", @@ -765,9 +797,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "find-msvc-tools" @@ -800,12 +832,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -915,21 +941,11 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -947,30 +963,28 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", - "rand_core 0.10.0", - "wasip2", - "wasip3", + "r-efi 6.0.0", + "rand_core 0.10.1", ] [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -985,15 +999,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -1002,14 +1007,14 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -1025,11 +1030,11 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1051,7 +1056,7 @@ checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -1099,6 +1104,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.10.1" @@ -1123,17 +1137,16 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", - "rustls 0.23.32", - "rustls-pki-types", + "rustls", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower-service", ] @@ -1189,12 +1202,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1202,9 +1216,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1215,11 +1229,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1230,42 +1243,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1273,12 +1282,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "idna" version = "1.1.0" @@ -1292,9 +1295,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1307,56 +1310,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] [[package]] name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.8" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[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.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" dependencies = [ + "defmt", "jiff-static", "log", "portable-atomic", @@ -1366,9 +1360,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" dependencies = [ "proc-macro2", "quote", @@ -1377,25 +1371,52 @@ dependencies = [ [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", + "jni-macros", "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror 2.0.18", "walkdir", - "windows-sys 0.45.0", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", ] [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] [[package]] name = "jobserver" @@ -1409,27 +1430,20 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.93" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797146bb2677299a1eb6b7b50a890f4c361b29ef967addf5b2fa45dae1bb6d7d" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" -version = "0.2.184" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" @@ -1445,15 +1459,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -1466,9 +1480,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -1484,9 +1498,9 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "mime" @@ -1494,6 +1508,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minicov" version = "0.3.8" @@ -1516,9 +1540,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -1568,9 +1592,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -1593,15 +1617,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oorandom" @@ -1622,9 +1646,9 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "parking_lot" @@ -1646,7 +1670,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -1663,30 +1687,30 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -1708,9 +1732,9 @@ dependencies = [ [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -1722,15 +1746,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -1746,6 +1770,28 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1780,6 +1826,7 @@ dependencies = [ name = "progenitor-client" version = "0.14.0" dependencies = [ + "assert_matches", "bytes", "futures-core", "percent-encoding", @@ -1839,9 +1886,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -1849,7 +1896,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.32", + "rustls", "socket2", "thiserror 2.0.18", "tokio", @@ -1859,18 +1906,18 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", - "rustls 0.23.32", + "rustls", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -1908,14 +1955,20 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -1925,8 +1978,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", - "getrandom 0.4.1", - "rand_core 0.10.0", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -1936,23 +1989,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "redox_syscall" @@ -1960,7 +2013,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.0", ] [[package]] @@ -1977,9 +2030,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2023,10 +2076,11 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.32", + "rustls", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -2034,7 +2088,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -2054,7 +2108,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2062,9 +2116,18 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] [[package]] name = "rustfmt-wrapper" @@ -2085,7 +2148,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2094,50 +2157,37 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.13.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring", - "rustls-pki-types", - "rustls-webpki 0.102.8", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls" -version = "0.23.32" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", + "log", "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.7", + "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2156,9 +2206,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -2166,19 +2216,19 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", "once_cell", - "rustls 0.23.32", + "rustls", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.7", + "rustls-webpki", "security-framework", "security-framework-sys", "webpki-root-certs", @@ -2193,20 +2243,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.7" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -2222,9 +2261,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -2237,9 +2276,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2278,11 +2317,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.13.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -2291,9 +2330,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2385,9 +2424,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -2431,35 +2470,52 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "similar" @@ -2469,9 +2525,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slog" @@ -2537,15 +2593,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2612,7 +2668,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags", + "bitflags 2.13.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2635,22 +2691,22 @@ checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", - "rustix 1.1.2", + "rustix 1.1.4", "windows-sys 0.61.2", ] [[package]] name = "term" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2111ef44dae28680ae9752bb89409e7310ca33a8c621ebe7b106cf5c928b3ac0" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ "windows-sys 0.61.2", ] @@ -2712,12 +2768,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" dependencies = [ "deranged", - "itoa", "libc", "num-conv", "num_threads", @@ -2729,15 +2784,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" dependencies = [ "num-conv", "time-core", @@ -2745,9 +2800,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -2755,9 +2810,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -2796,32 +2851,21 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.32", + "rustls", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2844,17 +2888,17 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.7+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 1.0.1+spec-1.1.0", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.0", + "winnow 1.0.3", ] [[package]] @@ -2868,9 +2912,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.1+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -2886,16 +2930,16 @@ dependencies = [ "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow 0.7.13", + "winnow 0.7.15", ] [[package]] name = "toml_parser" -version = "1.0.10+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.0", + "winnow 1.0.3", ] [[package]] @@ -2906,9 +2950,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.7+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "toolchain_find" @@ -2925,9 +2969,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -2940,20 +2984,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags", + "bitflags 2.13.0", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -2970,9 +3014,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-core", @@ -2980,9 +3024,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -2995,9 +3039,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "typify" @@ -3046,6 +3090,12 @@ dependencies = [ "typify-impl", ] +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -3054,9 +3104,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -3064,12 +3114,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -3108,11 +3152,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.3" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -3169,27 +3213,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.116" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dc0882f7b5bb01ae8c5215a1230832694481c1a4be062fd410e12ea3da5b631" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -3200,9 +3235,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.66" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19280959e2844181895ef62f065c63e0ca07ece4771b53d89bfdb967d97cbf05" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -3210,9 +3245,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.116" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75973d3066e01d035dbedaad2864c398df42f8dd7b1ea057c35b8407c015b537" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3220,9 +3255,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.116" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91af5e4be765819e0bcfee7322c14374dc821e35e72fa663a830bbc7dc199eac" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -3233,18 +3268,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.116" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9bf0406a78f02f336bf1e451799cca198e8acde4ffa278f0fb20487b150a633" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.66" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea88331fc76766356287e79bb0bc032157feea8eff8f2c3f1d9ea4b94255ae1c" +checksum = "2a0d555ca874445df8d314f94f5c948a4e74e5418f332c89f660a3d8310a96f4" dependencies = [ "async-trait", "cast", @@ -3264,9 +3299,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.66" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92437fa87f58743befb3003c4f4e3e9010dd50c6935561be7645981c0de05dfd" +checksum = "94eb68555b95bcea5e8cf4abe280b529049479fa995bfc23734af96a6aedc120" dependencies = [ "proc-macro2", "quote", @@ -3275,31 +3310,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-shared" -version = "0.2.116" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10091e48e3231b0f567b098ddb9a107310eb2629ae0eaa7c98dd746d5e80ee78" - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] +checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920" [[package]] name = "wasm-streams" @@ -3314,23 +3327,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.93" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "749466a37ee189057f54748b200186b59a03417a117267baf3fd89cecc9fb837" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -3348,9 +3349,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" dependencies = [ "rustls-pki-types", ] @@ -3394,9 +3395,9 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -3421,12 +3422,6 @@ dependencies = [ "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" @@ -3435,22 +3430,13 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-result" -version = "0.3.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -3459,16 +3445,7 @@ 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.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -3477,16 +3454,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", + "windows-link", ] [[package]] @@ -3522,22 +3490,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-link", ] [[package]] @@ -3562,7 +3515,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -3573,12 +3526,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3591,12 +3538,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3609,12 +3550,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3639,12 +3574,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3657,12 +3586,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3675,12 +3598,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3693,12 +3610,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3713,120 +3624,37 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] name = "winnow" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3834,9 +3662,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -3846,18 +3674,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", @@ -3866,18 +3694,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -3887,15 +3715,15 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3904,9 +3732,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3915,9 +3743,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -3926,6 +3754,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index c13a25722..862b49bb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ quote = "1.0.45" rand = "0.10.1" regex = "1.12.3" regress = "0.11.1" -reqwest = { version = "0.13.4", default-features = false, features = ["json", "query", "stream"] } +reqwest = { version = "0.13.4", default-features = false, features = ["json", "query", "stream", "multipart"] } rustfmt-wrapper = "0.2.1" schemars = "0.8.22" semver = "1.0.28" @@ -56,11 +56,12 @@ serde_urlencoded = "0.7.1" serde_yaml = "0.9" syn = { version = "2.0.117", features = ["parsing"] } thiserror = "2.0.18" -tokio = { version = "1.52.3", features = ["rt", "net"] } +tokio = { version = "1.52.3", features = ["macros", "rt", "net"] } typify = { version = "0.7.0" } url = "2.5.8" unicode-ident = "1.0.24" uuid = { version = "1.23.2", features = ["serde", "v4"] } +assert_matches = "1.5.0" #[patch.crates-io] #typify = { path = "../typify/typify" } diff --git a/cargo-progenitor/src/main.rs b/cargo-progenitor/src/main.rs index b87909a46..4c8ce061d 100644 --- a/cargo-progenitor/src/main.rs +++ b/cargo-progenitor/src/main.rs @@ -6,7 +6,7 @@ use std::{ path::{Path, PathBuf}, }; -use anyhow::{Result, bail}; +use anyhow::{bail, Result}; use clap::{Parser, ValueEnum}; use openapiv3::OpenAPI; use progenitor::{GenerationSettings, Generator, InterfaceStyle, TagStyle}; @@ -243,14 +243,8 @@ pub fn dependencies(builder: Generator, include_client: bool) -> Vec { let mut deps = vec![ format!("bytes = \"{}\"", DEPENDENCIES.bytes), format!("futures-core = \"{}\"", DEPENDENCIES.futures), - format!( - "reqwest = {{ version = \"{}\", default-features=false, features = [\"json\", \"query\", \"stream\"] }}", - DEPENDENCIES.reqwest, - ), - format!( - "serde = {{ version = \"{}\", features = [\"derive\"] }}", - DEPENDENCIES.serde, - ), + format!("reqwest = {{ version = \"{}\", default-features=false, features = [\"json\", \"stream\", \"multipart\"] }}", DEPENDENCIES.reqwest), + format!("serde = {{ version = \"{}\", features = [\"derive\"] }}", DEPENDENCIES.serde), format!("serde_urlencoded = \"{}\"", DEPENDENCIES.serde_urlencoded), ]; diff --git a/progenitor-client/Cargo.toml b/progenitor-client/Cargo.toml index 26f8e66af..959ffa134 100644 --- a/progenitor-client/Cargo.toml +++ b/progenitor-client/Cargo.toml @@ -17,5 +17,6 @@ serde_json = { workspace = true } serde_urlencoded = { workspace = true } [dev-dependencies] +assert_matches = { workspace = true } url = { workspace = true } uuid = { workspace = true } diff --git a/progenitor-client/src/progenitor_client.rs b/progenitor-client/src/progenitor_client.rs index db6728537..ce3277a4c 100644 --- a/progenitor-client/src/progenitor_client.rs +++ b/progenitor-client/src/progenitor_client.rs @@ -8,8 +8,8 @@ use std::ops::{Deref, DerefMut}; use bytes::Bytes; use futures_core::Stream; -use reqwest::RequestBuilder; -use serde::{Serialize, de::DeserializeOwned, ser::SerializeStruct}; +use reqwest::{multipart::Part, Body, RequestBuilder}; +use serde::{de::DeserializeOwned, ser::SerializeStruct, Serialize}; #[cfg(not(target_arch = "wasm32"))] type InnerByteStream = std::pin::Pin> + Send + Sync>>; @@ -17,6 +17,404 @@ type InnerByteStream = std::pin::Pin>>>; +/// A validated filename for form part uploads. +/// +/// Filenames are validated to not contain path separators or null bytes, +/// preventing path traversal attacks. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Filename(String); + +impl Filename { + /// Create a new filename, validating that it doesn't contain path separators. + /// + /// # Errors + /// Returns an error if the filename contains `/`, `\`, or null bytes. + pub fn new(name: impl Into) -> Result { + let name = name.into(); + if name.contains('/') || name.contains('\\') || name.contains('\0') { + Err(FilenameError::InvalidCharacter) + } else if name.is_empty() { + Err(FilenameError::Empty) + } else { + Ok(Self(name)) + } + } + + /// Create a filename without validation. + /// + /// # Safety + /// The caller must ensure the filename doesn't contain path separators. + /// This is useful when the filename comes from a trusted source. + pub fn new_unchecked(name: impl Into) -> Self { + Self(name.into()) + } + + /// Get the filename as a string slice. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Consume the Filename and return the inner String. + pub fn into_string(self) -> String { + self.0 + } +} + +impl AsRef for Filename { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for Filename { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Error type for invalid filenames. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FilenameError { + /// Filename contains path separators (`/`, `\`) or null bytes. + InvalidCharacter, + /// Filename is empty. + Empty, +} + +impl std::fmt::Display for FilenameError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FilenameError::InvalidCharacter => { + write!(f, "filename contains invalid characters (/, \\, or null)") + } + FilenameError::Empty => write!(f, "filename cannot be empty"), + } + } +} + +impl std::error::Error for FilenameError {} + +/// A validated MIME content-type for form parts. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ContentType(String); + +impl ContentType { + /// Create a new content-type, validating the format. + /// + /// # Errors + /// Returns an error if the content-type doesn't follow `type/subtype` format. + pub fn new(content_type: impl Into) -> Result { + let content_type = content_type.into(); + let Some((type_part, rest)) = content_type.split_once('/') else { + return Err(ContentTypeError::InvalidFormat); + }; + let subtype_part = rest.split_once(';').map_or(rest, |(s, _)| s); + if type_part.trim().is_empty() + || subtype_part.trim().is_empty() + || subtype_part.contains('/') + { + return Err(ContentTypeError::InvalidFormat); + } + Ok(Self(content_type)) + } + + /// Create a content-type without validation. + /// + /// # Safety + /// The caller must ensure the content-type is in valid `type/subtype` format. + pub fn new_unchecked(content_type: impl Into) -> Self { + Self(content_type.into()) + } + + /// Get the content-type as a string slice. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Consume the ContentType and return the inner String. + pub fn into_string(self) -> String { + self.0 + } + + /// Create `application/json` content type. + pub fn json() -> Self { + Self("application/json".to_string()) + } + + /// Create `application/octet-stream` content type. + pub fn octet_stream() -> Self { + Self("application/octet-stream".to_string()) + } + + /// Create an `application/{subtype}` content type. + pub fn application(subtype: &str) -> Self { + Self(format!("application/{}", subtype)) + } + + /// Create an `image/{subtype}` content type. + pub fn image(subtype: &str) -> Self { + Self(format!("image/{}", subtype)) + } +} + +impl AsRef for ContentType { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for ContentType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Error type for invalid content-types. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ContentTypeError { + /// Content-type doesn't follow the `type/subtype` format. + InvalidFormat, +} + +impl std::fmt::Display for ContentTypeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "content-type must be in 'type/subtype' format") + } +} + +impl std::error::Error for ContentTypeError {} + +/// Binary form part data with optional filename and content-type. +#[derive(Debug, Clone)] +pub struct BinaryFormPart { + /// The binary data + pub data: Bytes, + /// Optional filename for the part + pub filename: Option, + /// Optional content-type override + pub content_type: Option, +} + +impl BinaryFormPart { + /// Create a new binary form part from bytes. + pub fn new(data: impl Into) -> Self { + Self { + data: data.into(), + filename: None, + content_type: None, + } + } + + /// Create a binary form part with a filename. + pub fn with_filename(data: impl Into, filename: Filename) -> Self { + Self { + data: data.into(), + filename: Some(filename), + content_type: None, + } + } + + /// Create a binary form part with filename and content-type. + pub fn with_metadata( + data: impl Into, + filename: Option, + content_type: Option, + ) -> Self { + Self { + data: data.into(), + filename, + content_type, + } + } + + /// Create a builder for a binary form part. + pub fn builder(data: impl Into) -> BinaryFormPartBuilder { + BinaryFormPartBuilder { + data: data.into(), + filename: None, + content_type: None, + } + } +} + +impl From for FormPart { + fn from(part: BinaryFormPart) -> Self { + FormPart::Binary(part) + } +} + +/// Text form part data with optional content-type. +#[derive(Debug, Clone)] +pub struct TextFormPart { + /// The text value + pub value: String, + /// Optional content-type override + pub content_type: Option, +} + +impl TextFormPart { + /// Create a new text form part. + pub fn new(value: impl Into) -> Self { + Self { + value: value.into(), + content_type: None, + } + } + + /// Create a text form part with a specific content-type. + pub fn with_content_type(value: impl Into, content_type: ContentType) -> Self { + Self { + value: value.into(), + content_type: Some(content_type), + } + } + + /// Create a JSON text form part. + pub fn json(value: &T) -> Result { + Ok(Self { + value: serde_json::to_string(value)?, + content_type: Some(ContentType::json()), + }) + } + + /// Create a builder for a text form part. + pub fn builder(value: impl Into) -> TextFormPartBuilder { + TextFormPartBuilder { + value: value.into(), + content_type: None, + } + } +} + +impl From for FormPart { + fn from(part: TextFormPart) -> Self { + FormPart::Text(part) + } +} + +/// A part of a multipart form, either binary data or text. +#[derive(Debug, Clone)] +pub enum FormPart { + /// Binary data (e.g., file contents) with optional filename and content-type + Binary(BinaryFormPart), + /// Text data (will be sent as a text field) with optional content-type + Text(TextFormPart), +} + +impl FormPart { + /// Create a binary form part from bytes + pub fn binary(data: impl Into) -> Self { + Self::Binary(BinaryFormPart::new(data)) + } + + /// Create a binary form part with a filename + pub fn binary_with_filename(data: impl Into, filename: Filename) -> Self { + Self::Binary(BinaryFormPart::with_filename(data, filename)) + } + + /// Create a binary form part with filename and content-type + pub fn binary_with_metadata( + data: impl Into, + filename: Option, + content_type: Option, + ) -> Self { + Self::Binary(BinaryFormPart::with_metadata(data, filename, content_type)) + } + + /// Create a text form part from a string + pub fn text(data: impl Into) -> Self { + Self::Text(TextFormPart::new(data)) + } + + /// Create a text form part with a specific content-type + pub fn text_with_content_type(data: impl Into, content_type: ContentType) -> Self { + Self::Text(TextFormPart::with_content_type(data, content_type)) + } + + /// Create a JSON text form part + pub fn json(value: &T) -> Result { + Ok(Self::Text(TextFormPart::json(value)?)) + } + + /// Create a builder for a binary form part + pub fn binary_builder(data: impl Into) -> BinaryFormPartBuilder { + BinaryFormPart::builder(data) + } + + /// Create a builder for a text form part + pub fn text_builder(value: impl Into) -> TextFormPartBuilder { + TextFormPart::builder(value) + } +} + +/// Builder for binary form parts. +/// +/// Created via [`FormPart::binary_builder`] or [`BinaryFormPart::builder`]. +#[derive(Debug, Clone)] +pub struct BinaryFormPartBuilder { + data: Bytes, + filename: Option, + content_type: Option, +} + +impl BinaryFormPartBuilder { + /// Set the filename for this part. + pub fn filename(mut self, filename: Filename) -> Self { + self.filename = Some(filename); + self + } + + /// Set the content-type for this part. + pub fn content_type(mut self, content_type: ContentType) -> Self { + self.content_type = Some(content_type); + self + } + + /// Build the BinaryFormPart. + pub fn build(self) -> BinaryFormPart { + BinaryFormPart { + data: self.data, + filename: self.filename, + content_type: self.content_type, + } + } + + /// Build directly into a FormPart. + pub fn into_form_part(self) -> FormPart { + FormPart::Binary(self.build()) + } +} + +/// Builder for text form parts. +/// +/// Created via [`FormPart::text_builder`] or [`TextFormPart::builder`]. +#[derive(Debug, Clone)] +pub struct TextFormPartBuilder { + value: String, + content_type: Option, +} + +impl TextFormPartBuilder { + /// Set the content-type for this part. + pub fn content_type(mut self, content_type: ContentType) -> Self { + self.content_type = Some(content_type); + self + } + + /// Build the TextFormPart. + pub fn build(self) -> TextFormPart { + TextFormPart { + value: self.value, + content_type: self.content_type, + } + } + + /// Build directly into a FormPart. + pub fn into_form_part(self) -> FormPart { + FormPart::Text(self.build()) + } +} + /// Untyped byte stream used for both success and error responses. pub struct ByteStream(InnerByteStream); @@ -571,8 +969,16 @@ pub fn encode_path(pc: &str) -> String { } #[doc(hidden)] -pub trait RequestBuilderExt { +pub trait RequestBuilderExt +where + Self: Sized, +{ fn form_urlencoded(self, body: &T) -> Result>; + + fn form_from_parts, I: Sized + IntoIterator>( + self, + iter: I, + ) -> Result>; } impl RequestBuilderExt for RequestBuilder { @@ -587,6 +993,53 @@ impl RequestBuilderExt for RequestBuilder { .map_err(|_| Error::InvalidRequest("failed to serialize body".to_string()))?, )) } + + fn form_from_parts, I: Sized + IntoIterator>( + self, + iter: I, + ) -> Result> { + use reqwest::multipart::Form; + + let mut form = Form::new(); + for (name, part) in iter { + let name = name.as_ref().to_owned(); + form = match part { + FormPart::Binary(BinaryFormPart { + data, + filename, + content_type, + }) => { + let body = Body::from(data); + let mut p = Part::stream(body); + if let Some(fname) = filename { + p = p.file_name(fname.into_string()); + } + if let Some(ct) = content_type { + p = p.mime_str(ct.as_str()).map_err(|e| { + Error::InvalidRequest(format!("invalid content-type: {}", e)) + })?; + } + form.part(name, p) + } + FormPart::Text(TextFormPart { + value, + content_type, + }) => { + if let Some(ct) = content_type { + let p = Part::text(value).mime_str(ct.as_str()).map_err(|e| { + Error::InvalidRequest(format!("invalid content-type: {}", e)) + })?; + form.part(name, p) + } else { + form.text(name, value) + } + } + }; + } + // Note: reqwest's .multipart() automatically sets the Content-Type header + // with the correct boundary, so we don't set it manually here. + Ok(self.multipart(form)) + } } #[doc(hidden)] diff --git a/progenitor-client/tests/client_test.rs b/progenitor-client/tests/client_test.rs index 96fd118eb..7fd7b2077 100644 --- a/progenitor-client/tests/client_test.rs +++ b/progenitor-client/tests/client_test.rs @@ -219,3 +219,366 @@ fn test_query_option() { let result = encode_query_param("paramName", &value).unwrap(); assert_eq!(result, "paramName=42"); } + +mod form_part_tests { + use assert_matches::assert_matches; + use bytes::Bytes; + use progenitor_client::{BinaryFormPart, ContentType, Filename, FormPart, TextFormPart}; + use serde::Serialize; + + #[test] + fn test_form_part_binary() { + let data = vec![1u8, 2, 3, 4]; + let part = FormPart::binary(data.clone()); + + assert_matches!(part, FormPart::Binary(BinaryFormPart { data: d, filename: None, content_type: None }) => { + assert_eq!(d.as_ref(), &data[..]); + }); + } + + #[test] + fn test_form_part_binary_from_bytes() { + let data = Bytes::from_static(b"hello world"); + let part = FormPart::binary(data.clone()); + + assert_matches!(part, FormPart::Binary(BinaryFormPart { data: d, .. }) => { + assert_eq!(d, data); + }); + } + + #[test] + fn test_form_part_binary_with_filename() { + let part = + FormPart::binary_with_filename(vec![1u8, 2, 3], Filename::new("test.bin").unwrap()); + + assert_matches!(part, FormPart::Binary(BinaryFormPart { filename: Some(f), content_type: None, .. }) => { + assert_eq!(f.as_str(), "test.bin"); + }); + } + + #[test] + fn test_form_part_binary_with_metadata() { + let part = FormPart::binary_with_metadata( + vec![1u8, 2, 3], + Some(Filename::new("document.pdf").unwrap()), + Some(ContentType::application("pdf")), + ); + + assert_matches!(part, FormPart::Binary(BinaryFormPart { filename: Some(f), content_type: Some(ct), .. }) => { + assert_eq!(f.as_str(), "document.pdf"); + assert_eq!(ct.as_str(), "application/pdf"); + }); + } + + #[test] + fn test_form_part_binary_with_metadata_none() { + let part = FormPart::binary_with_metadata(vec![1u8, 2, 3], None, None); + assert_matches!( + part, + FormPart::Binary(BinaryFormPart { + filename: None, + content_type: None, + .. + }) + ); + } + + #[test] + fn test_form_part_text() { + let part = FormPart::text("hello world"); + + assert_matches!(part, FormPart::Text(TextFormPart { value, content_type: None }) => { + assert_eq!(value, "hello world"); + }); + } + + #[test] + fn test_form_part_text_from_string() { + let part = FormPart::text(String::from("hello world")); + + assert_matches!(part, FormPart::Text(TextFormPart { value, .. }) => { + assert_eq!(value, "hello world"); + }); + } + + #[test] + fn test_form_part_text_with_content_type() { + let part = FormPart::text_with_content_type("{\"key\": \"value\"}", ContentType::json()); + + assert_matches!(part, FormPart::Text(TextFormPart { value, content_type: Some(ct) }) => { + assert_eq!(value, "{\"key\": \"value\"}"); + assert_eq!(ct.as_str(), "application/json"); + }); + } + + #[test] + fn test_form_part_json() { + #[derive(Serialize)] + struct TestData { + name: String, + count: i32, + } + + let data = TestData { + name: "test".to_string(), + count: 42, + }; + let part = FormPart::json(&data).unwrap(); + + assert_matches!(part, FormPart::Text(TextFormPart { value, content_type: Some(ct) }) => { + assert_eq!(value, r#"{"name":"test","count":42}"#); + assert_eq!(ct.as_str(), "application/json"); + }); + } + + #[test] + fn test_form_part_json_array() { + let part = FormPart::json(&vec![1, 2, 3, 4, 5]).unwrap(); + + assert_matches!(part, FormPart::Text(TextFormPart { value, content_type: Some(ct) }) => { + assert_eq!(value, "[1,2,3,4,5]"); + assert_eq!(ct.as_str(), "application/json"); + }); + } + + #[test] + fn test_form_part_json_nested() { + #[derive(Serialize)] + struct Inner { + value: String, + } + + #[derive(Serialize)] + struct Outer { + inner: Inner, + tags: Vec, + } + + let data = Outer { + inner: Inner { + value: "nested".to_string(), + }, + tags: vec!["a".to_string(), "b".to_string()], + }; + let part = FormPart::json(&data).unwrap(); + + assert_matches!(part, FormPart::Text(TextFormPart { value, .. }) => { + assert!(value.contains("\"inner\"")); + assert!(value.contains("\"nested\"")); + assert!(value.contains("\"tags\"")); + }); + } + + #[test] + fn test_form_part_clone() { + let part1 = + FormPart::binary_with_filename(vec![1, 2, 3], Filename::new("test.bin").unwrap()); + let part2 = part1.clone(); + + assert_matches!( + (part1, part2), + (FormPart::Binary(BinaryFormPart { data: d1, filename: f1, .. }), FormPart::Binary(BinaryFormPart { data: d2, filename: f2, .. })) => { + assert_eq!(d1, d2); + assert_eq!(f1, f2); + } + ); + } + + #[test] + fn test_form_part_debug() { + let part = FormPart::text("test"); + let debug_str = format!("{:?}", part); + assert!(debug_str.contains("Text")); + assert!(debug_str.contains("test")); + } + + // Tests for the new type-safe newtypes + #[test] + fn test_filename_validation() { + // Valid filenames + assert!(Filename::new("test.bin").is_ok()); + assert!(Filename::new("my-file_123.txt").is_ok()); + assert!(Filename::new("file with spaces.pdf").is_ok()); + + // Invalid filenames (path separators) + assert!(Filename::new("path/to/file.txt").is_err()); + assert!(Filename::new("path\\to\\file.txt").is_err()); + assert!(Filename::new("file\0name.txt").is_err()); + assert!(Filename::new("").is_err()); + } + + #[test] + fn test_filename_unchecked() { + // new_unchecked bypasses validation (useful for trusted sources) + let f = Filename::new_unchecked("any/path/works"); + assert_eq!(f.as_str(), "any/path/works"); + } + + #[test] + fn test_content_type_json() { + assert_eq!(ContentType::json().as_str(), "application/json"); + } + + #[test] + fn test_content_type_new_unchecked() { + assert_eq!( + ContentType::new_unchecked("text/html").as_str(), + "text/html" + ); + } + + #[test] + fn test_content_type_application() { + assert_eq!(ContentType::application("pdf").as_str(), "application/pdf"); + assert_eq!(ContentType::application("xml").as_str(), "application/xml"); + } + + #[test] + fn test_content_type_image() { + assert_eq!(ContentType::image("png").as_str(), "image/png"); + assert_eq!(ContentType::image("jpeg").as_str(), "image/jpeg"); + } + + #[test] + fn test_content_type_octet_stream() { + assert_eq!( + ContentType::octet_stream().as_str(), + "application/octet-stream" + ); + } + + #[test] + fn test_filename_display() { + let f = Filename::new("test.bin").unwrap(); + assert_eq!(format!("{}", f), "test.bin"); + } + + #[test] + fn test_content_type_display() { + let ct = ContentType::json(); + assert_eq!(format!("{}", ct), "application/json"); + } + + #[test] + fn test_filename_as_ref() { + let f = Filename::new("test.bin").unwrap(); + let s: &str = f.as_ref(); + assert_eq!(s, "test.bin"); + } + + #[test] + fn test_content_type_as_ref() { + let ct = ContentType::json(); + let s: &str = ct.as_ref(); + assert_eq!(s, "application/json"); + } + + // Builder pattern tests + #[test] + fn test_binary_builder_minimal() { + let part = FormPart::binary_builder(vec![1u8, 2, 3]).build(); + assert_matches!( + part, + BinaryFormPart { + filename: None, + content_type: None, + .. + } + ); + } + + #[test] + fn test_binary_builder_with_filename() { + let part = FormPart::binary_builder(vec![1u8, 2, 3]) + .filename(Filename::new("test.bin").unwrap()) + .build(); + + assert_matches!(part, BinaryFormPart { filename: Some(f), content_type: None, .. } => { + assert_eq!(f.as_str(), "test.bin"); + }); + } + + #[test] + fn test_binary_builder_with_content_type() { + let part = FormPart::binary_builder(vec![1u8, 2, 3]) + .content_type(ContentType::image("png")) + .build(); + + assert_matches!(part, BinaryFormPart { filename: None, content_type: Some(ct), .. } => { + assert_eq!(ct.as_str(), "image/png"); + }); + } + + #[test] + fn test_binary_builder_full() { + let part = FormPart::binary_builder(vec![1u8, 2, 3]) + .filename(Filename::new("document.pdf").unwrap()) + .content_type(ContentType::application("pdf")) + .build(); + + assert_matches!(part, BinaryFormPart { data, filename: Some(f), content_type: Some(ct) } => { + assert_eq!(data.as_ref(), &[1u8, 2, 3]); + assert_eq!(f.as_str(), "document.pdf"); + assert_eq!(ct.as_str(), "application/pdf"); + }); + } + + #[test] + fn test_text_builder_minimal() { + let part = FormPart::text_builder("hello").build(); + + assert_matches!(part, TextFormPart { value, content_type: None } => { + assert_eq!(value, "hello"); + }); + } + + #[test] + fn test_text_builder_with_content_type() { + let part = FormPart::text_builder("{}") + .content_type(ContentType::json()) + .build(); + + assert_matches!(part, TextFormPart { value, content_type: Some(ct) } => { + assert_eq!(value, "{}"); + assert_eq!(ct.as_str(), "application/json"); + }); + } + + #[test] + fn test_builder_into_form_part() { + // Test that builders can be converted directly to FormPart + let part: FormPart = FormPart::binary_builder(vec![1u8, 2, 3]) + .filename(Filename::new("test.bin").unwrap()) + .into_form_part(); + assert_matches!( + part, + FormPart::Binary(BinaryFormPart { + filename: Some(_), + .. + }) + ); + + let part: FormPart = FormPart::text_builder("hello") + .content_type(ContentType::json()) + .into_form_part(); + assert_matches!( + part, + FormPart::Text(TextFormPart { + content_type: Some(_), + .. + }) + ); + } + + #[test] + fn test_inner_types_into_form_part() { + // Test that BinaryFormPart and TextFormPart can be converted to FormPart via From + let binary = BinaryFormPart::new(vec![1u8, 2, 3]); + let part: FormPart = binary.into(); + assert_matches!(part, FormPart::Binary(_)); + + let text = TextFormPart::new("hello"); + let part: FormPart = text.into(); + assert_matches!(part, FormPart::Text(_)); + } +} diff --git a/progenitor-impl/src/cli.rs b/progenitor-impl/src/cli.rs index 735b41d9d..a36c9bd36 100644 --- a/progenitor-impl/src/cli.rs +++ b/progenitor-impl/src/cli.rs @@ -450,7 +450,8 @@ impl Generator { // are currently... OperationParameterType::RawBody => None, - OperationParameterType::Type(body_type_id) => Some(body_type_id), + OperationParameterType::Type(body_type_id) + | OperationParameterType::Form(body_type_id) => Some(body_type_id), }); if let Some(body_type_id) = maybe_body_type_id { diff --git a/progenitor-impl/src/httpmock.rs b/progenitor-impl/src/httpmock.rs index 2c58ba599..9a94c63e9 100644 --- a/progenitor-impl/src/httpmock.rs +++ b/progenitor-impl/src/httpmock.rs @@ -157,7 +157,8 @@ impl Generator { description: _, }| { let arg_type_name = match typ { - OperationParameterType::Type(arg_type_id) => self + OperationParameterType::Type(arg_type_id) + | OperationParameterType::Form(arg_type_id) => self .type_space .get_type(arg_type_id) .unwrap() @@ -227,7 +228,7 @@ impl Generator { }, ), OperationParameterKind::Body(body_content_type) => match typ { - OperationParameterType::Type(_) => ( + OperationParameterType::Type(_) | OperationParameterType::Form(_) => ( true, quote! { Self(self.0.json_body_obj(value)) diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index 4d5a96a6d..a83c0c91b 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -6,12 +6,13 @@ use std::collections::{BTreeMap, HashMap, HashSet}; +use indexmap::IndexMap; use openapiv3::OpenAPI; use proc_macro2::TokenStream; use quote::quote; use serde::Deserialize; use thiserror::Error; -use typify::{TypeSpace, TypeSpaceSettings}; +use typify::{TypeDetails, TypeId, TypeSpace, TypeSpaceSettings}; use crate::to_schema::ToSchema; @@ -50,11 +51,33 @@ pub type Result = std::result::Result; /// OpenAPI generator. pub struct Generator { type_space: TypeSpace, + /// Maps form type IDs to their field metadata (field name -> is_binary) + forms: IndexMap, settings: GenerationSettings, uses_futures: bool, uses_websockets: bool, } +/// Information about form fields for multipart/form-data generation +#[derive(Debug, Clone, Default)] +pub(crate) struct FormFieldsInfo { + /// Maps field names to their metadata + pub fields: IndexMap, +} + +/// Metadata about a single form field +#[derive(Debug, Clone, Default)] +pub(crate) struct FormFieldMeta { + /// The original API name for the field (used in form field name) + pub api_name: String, + /// Whether the field is binary (format: binary) + pub is_binary: bool, + /// Whether the field is a complex type (array or object) requiring JSON serialization + pub needs_json: bool, + /// Content-type override from the encoding object + pub content_type: Option, +} + /// Settings for [Generator]. #[derive(Default, Clone)] pub struct GenerationSettings { @@ -261,6 +284,7 @@ impl Default for Generator { fn default() -> Self { Self { type_space: TypeSpace::new(TypeSpaceSettings::default().with_type_mod("types")), + forms: Default::default(), settings: Default::default(), uses_futures: Default::default(), uses_websockets: Default::default(), @@ -312,6 +336,7 @@ impl Generator { Self { type_space: TypeSpace::new(&type_settings), + forms: Default::default(), settings: settings.clone(), uses_futures: false, uses_websockets: false, @@ -374,6 +399,91 @@ impl Generator { let types = self.type_space.to_stream(); + // Generate as_form() implementations for form data types. + // Each form type gets a method that returns an iterator of (name, FormPart) pairs. + let extra_impl = TokenStream::from_iter(self.forms.iter().map(|(type_id, field_info)| { + let typ = self.get_type_space().get_type(type_id).unwrap(); + let td = typ.details(); + let TypeDetails::Struct(tstru) = td else { + unreachable!() + }; + + // Generate field accessors that convert each field to the appropriate FormPart + let field_conversions = tstru.properties().filter_map(|(prop_name, _prop_id)| { + let meta = field_info.fields.get(prop_name)?; + let ident = quote::format_ident!("{}", prop_name); + // Use the original API name for the form field name + let api_name = &meta.api_name; + let content_type = meta + .content_type + .as_ref() + .map(|ct| { + quote! { Some(progenitor_client::ContentType::new_unchecked(#ct)) } + }) + .unwrap_or_else(|| quote! { None }); + + if meta.is_binary { + // Binary fields: Option -> FormPart::Binary + // Use .into() to support custom binary types via with_conversion + Some(quote! { + if let Some(ref val) = self.#ident { + parts.push((#api_name, progenitor_client::FormPart::Binary( + progenitor_client::BinaryFormPart { + data: val.clone().into(), + filename: None, + content_type: #content_type, + } + ))); + } + }) + } else if meta.needs_json { + // Complex types (array/object): JSON serialize + let json_content_type = if meta.content_type.is_some() { + content_type.clone() + } else { + quote! { Some(progenitor_client::ContentType::json()) } + }; + Some(quote! { + if let Some(ref val) = self.#ident { + parts.push((#api_name, progenitor_client::FormPart::Text( + progenitor_client::TextFormPart { + value: serde_json::to_string(val) + .expect("multipart form field serialization should not fail"), + content_type: #json_content_type, + } + ))); + } + }) + } else { + // Simple text fields: serialize to string + Some(quote! { + if let Some(ref val) = self.#ident { + parts.push((#api_name, progenitor_client::FormPart::Text( + progenitor_client::TextFormPart { + value: val.to_string(), + content_type: #content_type, + } + ))); + } + }) + } + }); + + let form_name = quote::format_ident!("{}", typ.name()); + + quote! { + impl #form_name { + /// Convert this form into an iterator of `(field_name, field_value)` pairs + /// suitable for `multipart/form-data` encoding. + pub fn as_form(&self) -> Vec<(&'static str, progenitor_client::FormPart)> { + let mut parts = Vec::new(); + #(#field_conversions)* + parts + } + } + } + })); + let (inner_type, inner_fn_value) = match self.settings.inner_type.as_ref() { Some(inner_type) => (inner_type.clone(), quote! { &self.inner }), None => (quote! { () }, quote! { &() }), @@ -440,6 +550,8 @@ impl Generator { #[allow(clippy::all)] pub mod types { #types + + #extra_impl } #[derive(Clone, Debug)] diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index 6752823c0..2136c897c 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -6,6 +6,8 @@ use std::{ str::FromStr, }; +use indexmap::IndexMap; + use openapiv3::{Components, Parameter, ReferenceOr, Response, StatusCode}; use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote}; @@ -102,9 +104,10 @@ pub struct OperationParameter { pub kind: OperationParameterKind, } -#[derive(Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq)] pub enum OperationParameterType { Type(TypeId), + Form(TypeId), RawBody, } @@ -137,6 +140,7 @@ pub enum BodyContentType { OctetStream, Json, FormUrlencoded, + FormData, Text(String), } @@ -149,6 +153,7 @@ impl FromStr for BodyContentType { "application/octet-stream" => Ok(Self::OctetStream), "application/json" => Ok(Self::Json), "application/x-www-form-urlencoded" => Ok(Self::FormUrlencoded), + "form-data" | "multipart/form-data" => Ok(Self::FormData), "text/plain" | "text/x-markdown" => Ok(Self::Text(String::from(&s[..offset]))), _ => Err(Error::UnexpectedFormat(format!( "unexpected content type: {}", @@ -164,6 +169,7 @@ impl std::fmt::Display for BodyContentType { Self::OctetStream => "application/octet-stream", Self::Json => "application/json", Self::FormUrlencoded => "application/x-www-form-urlencoded", + Self::FormData => "multipart/form-data", Self::Text(typ) => typ, }) } @@ -571,12 +577,20 @@ impl Generator { .map(|param| { let name = format_ident!("{}", param.name); let typ = match (¶m.typ, param.kind.is_optional()) { - (OperationParameterType::Type(type_id), false) => self + ( + OperationParameterType::Type(type_id) + | OperationParameterType::Form(type_id), + false, + ) => self .type_space .get_type(type_id) .unwrap() .parameter_ident_with_lifetime("a"), - (OperationParameterType::Type(type_id), true) => { + ( + OperationParameterType::Type(type_id) + | OperationParameterType::Form(type_id), + true, + ) => { let t = self .type_space .get_type(type_id) @@ -927,6 +941,16 @@ impl Generator { // returns an error in the case of a serialization failure. .form_urlencoded(&body)? }), + ( + OperationParameterKind::Body(BodyContentType::FormData), + OperationParameterType::Form(_), + ) => { + Some(quote! { + // This uses `progenitor_client::RequestBuilderExt` which + // builds a multipart form from the form parts + .form_from_parts(body.as_form())? + }) + } (OperationParameterKind::Body(_), _) => { unreachable!("invalid body kind/type combination") } @@ -1445,7 +1469,7 @@ impl Generator { .params .iter() .map(|param| match ¶m.typ { - OperationParameterType::Type(type_id) => { + OperationParameterType::Type(type_id) | OperationParameterType::Form(type_id) => { let ty = self.type_space.get_type(type_id)?; // For body parameters only, if there's a builder we'll @@ -1478,7 +1502,7 @@ impl Generator { .params .iter() .map(|param| match ¶m.typ { - OperationParameterType::Type(type_id) => { + OperationParameterType::Type(type_id) | OperationParameterType::Form(type_id) => { let ty = self.type_space.get_type(type_id)?; // Fill in the appropriate initial value for the @@ -1508,7 +1532,7 @@ impl Generator { .params .iter() .map(|param| match ¶m.typ { - OperationParameterType::Type(type_id) => { + OperationParameterType::Type(type_id) | OperationParameterType::Form(type_id) => { let ty = self.type_space.get_type(type_id)?; if ty.builder().is_some() { let type_name = ty.ident(); @@ -1532,7 +1556,8 @@ impl Generator { .map(|param| { let param_name = format_ident!("{}", param.name); match ¶m.typ { - OperationParameterType::Type(type_id) => { + OperationParameterType::Type(type_id) + | OperationParameterType::Form(type_id) => { let ty = self.type_space.get_type(type_id)?; match (ty.builder(), param.kind.is_optional()) { // TODO right now optional body parameters are not @@ -2140,6 +2165,95 @@ impl Generator { }?; OperationParameterType::RawBody } + BodyContentType::FormData => { + // For multipart/form-data, we accept an object schema with various property types: + // - type: string, format: binary -> Binary file data (bytes::Bytes) + // - type: string -> Text field + // - type: integer/number/boolean -> Text field (serialized) + // - type: object -> JSON-serialized text field + // - type: array -> JSON-serialized text field + // + // We track field metadata to generate appropriate as_form() implementations. + // The encoding object can override content-types for individual fields. + + let field_info = match schema.item(components)? { + openapiv3::Schema { + schema_kind: + openapiv3::SchemaKind::Type(openapiv3::Type::Object( + openapiv3::ObjectType { properties, .. }, + )), + .. + } => { + let mut fields = IndexMap::new(); + for (name, property) in properties { + let (is_binary, needs_json) = match property { + ReferenceOr::Item(prop) => { + let is_binary = matches!( + &prop.schema_kind, + openapiv3::SchemaKind::Type(openapiv3::Type::String( + openapiv3::StringType { + format: openapiv3::VariantOrUnknownOrEmpty::Item( + openapiv3::StringFormat::Binary, + ), + .. + }, + )) + ); + let needs_json = matches!( + &prop.schema_kind, + openapiv3::SchemaKind::Type(openapiv3::Type::Array(_)) + | openapiv3::SchemaKind::Type(openapiv3::Type::Object( + _ + )) + ); + (is_binary, needs_json) + } + ReferenceOr::Reference { .. } => { + // References are assumed to be complex types requiring JSON + (false, true) + } + }; + + // Check for encoding overrides (content-type per field) + let content_type = media_type + .encoding + .get(name) + .and_then(|enc| enc.content_type.clone()); + + // Use the Rust-ized (snake_case) name as the key since + // typify's properties() returns Rust names, but store the + // original API name for use in the form field name. + let rust_name = sanitize(name, Case::Snake); + fields.insert( + rust_name, + crate::FormFieldMeta { + api_name: name.clone(), + is_binary, + needs_json, + content_type, + }, + ); + } + crate::FormFieldsInfo { fields } + } + _ => { + return Err(Error::UnexpectedFormat(format!( + "multipart/form-data requires an object schema, got: {:?}", + schema + ))); + } + }; + + let form_name = sanitize( + &format!("{}-form", operation.operation_id.as_ref().unwrap(),), + Case::Pascal, + ); + let type_id = self + .type_space + .add_type_with_name(&schema.to_schema(), Some(form_name))?; + self.forms.insert(type_id.clone(), field_info); + OperationParameterType::Form(type_id) + } BodyContentType::Json | BodyContentType::FormUrlencoded => { // TODO it would be legal to have the encoding field set for // application/x-www-form-urlencoded content, but I'm not sure diff --git a/progenitor-impl/tests/output/src/example_multipart_builder.rs b/progenitor-impl/tests/output/src/example_multipart_builder.rs new file mode 100644 index 000000000..771857a80 --- /dev/null +++ b/progenitor-impl/tests/output/src/example_multipart_builder.rs @@ -0,0 +1,325 @@ +#[allow(unused_imports)] +use progenitor_client::{encode_path, ClientHooks, OperationInfo, RequestBuilderExt}; +#[allow(unused_imports)] +pub use progenitor_client::{ByteStream, ClientInfo, Error, ResponseValue}; +/// Types used as operation parameters and responses. +#[allow(clippy::all)] +pub mod types { + /// Error types. + pub mod error { + /// Error from a `TryFrom` or `FromStr` implementation. + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + + impl ::std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } + } + + ///`UploadForm` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "file", + /// "name" + /// ], + /// "properties": { + /// "file": { + /// "type": "string", + /// "format": "binary" + /// }, + /// "name": { + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive( + :: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, schemars :: JsonSchema, + )] + pub struct UploadForm { + pub file: ::std::string::String, + pub name: ::std::string::String, + } + + impl UploadForm { + pub fn builder() -> builder::UploadForm { + Default::default() + } + } + + /// Types for composing complex structures. + pub mod builder { + #[derive(Clone, Debug)] + pub struct UploadForm { + file: ::std::result::Result<::std::string::String, ::std::string::String>, + name: ::std::result::Result<::std::string::String, ::std::string::String>, + } + + impl ::std::default::Default for UploadForm { + fn default() -> Self { + Self { + file: Err("no value supplied for file".to_string()), + name: Err("no value supplied for name".to_string()), + } + } + } + + impl UploadForm { + pub fn file(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.file = value + .try_into() + .map_err(|e| format!("error converting supplied value for file: {e}")); + self + } + pub fn name(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.name = value + .try_into() + .map_err(|e| format!("error converting supplied value for name: {e}")); + self + } + } + + impl ::std::convert::TryFrom for super::UploadForm { + type Error = super::error::ConversionError; + fn try_from( + value: UploadForm, + ) -> ::std::result::Result { + Ok(Self { + file: value.file?, + name: value.name?, + }) + } + } + + impl ::std::convert::From for UploadForm { + fn from(value: super::UploadForm) -> Self { + Self { + file: Ok(value.file), + name: Ok(value.name), + } + } + } + } + + impl UploadForm { + /// Convert this form into an iterator of `(field_name, field_value)` + /// pairs + /// suitable for `multipart/form-data` encoding. + pub fn as_form(&self) -> Vec<(&'static str, progenitor_client::FormPart)> { + let mut parts = Vec::new(); + if let Some(ref val) = self.file { + parts.push(( + "file", + progenitor_client::FormPart::Binary(progenitor_client::BinaryFormPart { + data: val.clone().into(), + filename: None, + content_type: None, + }), + )); + } + if let Some(ref val) = self.name { + parts.push(( + "name", + progenitor_client::FormPart::Text(progenitor_client::TextFormPart { + value: val.to_string(), + content_type: None, + }), + )); + } + parts + } + } +} + +#[derive(Clone, Debug)] +///Client for Multipart Example +/// +///Version: 1.0.0 +pub struct Client { + pub(crate) baseurl: String, + pub(crate) client: reqwest::Client, +} + +impl Client { + /// Create a new client. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new(baseurl: &str) -> Self { + #[cfg(not(target_arch = "wasm32"))] + let client = { + let dur = ::std::time::Duration::from_secs(15u64); + reqwest::ClientBuilder::new() + .connect_timeout(dur) + .timeout(dur) + }; + #[cfg(target_arch = "wasm32")] + let client = reqwest::ClientBuilder::new(); + Self::new_with_client(baseurl, client.build().unwrap()) + } + + /// Construct a new client with an existing `reqwest::Client`, + /// allowing more control over its configuration. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { + Self { + baseurl: baseurl.to_string(), + client, + } + } +} + +impl ClientInfo<()> for Client { + fn api_version() -> &'static str { + "1.0.0" + } + + fn baseurl(&self) -> &str { + self.baseurl.as_str() + } + + fn client(&self) -> &reqwest::Client { + &self.client + } + + fn inner(&self) -> &() { + &() + } +} + +impl ClientHooks<()> for &Client {} +impl Client { + ///Sends a `POST` request to `/upload` + /// + ///```ignore + /// let response = client.upload() + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn upload(&self) -> builder::Upload<'_> { + builder::Upload::new(self) + } +} + +/// Types for composing operation parameters. +#[allow(clippy::all)] +pub mod builder { + use super::types; + #[allow(unused_imports)] + use super::{ + encode_path, ByteStream, ClientHooks, ClientInfo, Error, OperationInfo, RequestBuilderExt, + ResponseValue, + }; + ///Builder for [`Client::upload`] + /// + ///[`Client::upload`]: super::Client::upload + #[derive(Debug, Clone)] + pub struct Upload<'a> { + client: &'a super::Client, + body: Result, + } + + impl<'a> Upload<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + body: Ok(::std::default::Default::default()), + } + } + + pub fn body(mut self, value: V) -> Self + where + V: std::convert::TryInto, + >::Error: std::fmt::Display, + { + self.body = value + .try_into() + .map(From::from) + .map_err(|s| format!("conversion to `UploadForm` for body failed: {}", s)); + self + } + + pub fn body_map(mut self, f: F) -> Self + where + F: std::ops::FnOnce(types::builder::UploadForm) -> types::builder::UploadForm, + { + self.body = self.body.map(f); + self + } + + ///Sends a `POST` request to `/upload` + pub async fn send(self) -> Result, Error<()>> { + let Self { client, body } = self; + let body = body + .and_then(|v| types::UploadForm::try_from(v).map_err(|e| e.to_string())) + .map_err(Error::InvalidRequest)?; + let url = format!("{}/upload", client.baseurl,); + let mut header_map = ::reqwest::header::HeaderMap::with_capacity(1usize); + header_map.append( + ::reqwest::header::HeaderName::from_static("api-version"), + ::reqwest::header::HeaderValue::from_static(super::Client::api_version()), + ); + #[allow(unused_mut)] + let mut request = client + .client + .post(url) + .form_from_parts(body.as_form())? + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload", + }; + client.pre(&mut request, &info).await?; + let result = client.exec(request, &info).await; + client.post(&result, &info).await?; + let response = result?; + match response.status().as_u16() { + 200u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } +} + +/// Items consumers will typically use such as the Client. +pub mod prelude { + pub use self::super::Client; +} diff --git a/progenitor-impl/tests/output/src/example_multipart_builder_tagged.rs b/progenitor-impl/tests/output/src/example_multipart_builder_tagged.rs new file mode 100644 index 000000000..8df04a315 --- /dev/null +++ b/progenitor-impl/tests/output/src/example_multipart_builder_tagged.rs @@ -0,0 +1,325 @@ +#[allow(unused_imports)] +use progenitor_client::{encode_path, ClientHooks, OperationInfo, RequestBuilderExt}; +#[allow(unused_imports)] +pub use progenitor_client::{ByteStream, ClientInfo, Error, ResponseValue}; +/// Types used as operation parameters and responses. +#[allow(clippy::all)] +pub mod types { + /// Error types. + pub mod error { + /// Error from a `TryFrom` or `FromStr` implementation. + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + + impl ::std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } + } + + ///`UploadForm` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "file", + /// "name" + /// ], + /// "properties": { + /// "file": { + /// "type": "string", + /// "format": "binary" + /// }, + /// "name": { + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct UploadForm { + pub file: ::std::string::String, + pub name: ::std::string::String, + } + + impl UploadForm { + pub fn builder() -> builder::UploadForm { + Default::default() + } + } + + /// Types for composing complex structures. + pub mod builder { + #[derive(Clone, Debug)] + pub struct UploadForm { + file: ::std::result::Result<::std::string::String, ::std::string::String>, + name: ::std::result::Result<::std::string::String, ::std::string::String>, + } + + impl ::std::default::Default for UploadForm { + fn default() -> Self { + Self { + file: Err("no value supplied for file".to_string()), + name: Err("no value supplied for name".to_string()), + } + } + } + + impl UploadForm { + pub fn file(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.file = value + .try_into() + .map_err(|e| format!("error converting supplied value for file: {e}")); + self + } + pub fn name(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.name = value + .try_into() + .map_err(|e| format!("error converting supplied value for name: {e}")); + self + } + } + + impl ::std::convert::TryFrom for super::UploadForm { + type Error = super::error::ConversionError; + fn try_from( + value: UploadForm, + ) -> ::std::result::Result { + Ok(Self { + file: value.file?, + name: value.name?, + }) + } + } + + impl ::std::convert::From for UploadForm { + fn from(value: super::UploadForm) -> Self { + Self { + file: Ok(value.file), + name: Ok(value.name), + } + } + } + } + + impl UploadForm { + /// Convert this form into an iterator of `(field_name, field_value)` + /// pairs + /// suitable for `multipart/form-data` encoding. + pub fn as_form(&self) -> Vec<(&'static str, progenitor_client::FormPart)> { + let mut parts = Vec::new(); + if let Some(ref val) = self.file { + parts.push(( + "file", + progenitor_client::FormPart::Binary(progenitor_client::BinaryFormPart { + data: val.clone().into(), + filename: None, + content_type: None, + }), + )); + } + if let Some(ref val) = self.name { + parts.push(( + "name", + progenitor_client::FormPart::Text(progenitor_client::TextFormPart { + value: val.to_string(), + content_type: None, + }), + )); + } + parts + } + } +} + +#[derive(Clone, Debug)] +///Client for Multipart Example +/// +///Version: 1.0.0 +pub struct Client { + pub(crate) baseurl: String, + pub(crate) client: reqwest::Client, +} + +impl Client { + /// Create a new client. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new(baseurl: &str) -> Self { + #[cfg(not(target_arch = "wasm32"))] + let client = { + let dur = ::std::time::Duration::from_secs(15u64); + reqwest::ClientBuilder::new() + .connect_timeout(dur) + .timeout(dur) + }; + #[cfg(target_arch = "wasm32")] + let client = reqwest::ClientBuilder::new(); + Self::new_with_client(baseurl, client.build().unwrap()) + } + + /// Construct a new client with an existing `reqwest::Client`, + /// allowing more control over its configuration. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { + Self { + baseurl: baseurl.to_string(), + client, + } + } +} + +impl ClientInfo<()> for Client { + fn api_version() -> &'static str { + "1.0.0" + } + + fn baseurl(&self) -> &str { + self.baseurl.as_str() + } + + fn client(&self) -> &reqwest::Client { + &self.client + } + + fn inner(&self) -> &() { + &() + } +} + +impl ClientHooks<()> for &Client {} +impl Client { + ///Sends a `POST` request to `/upload` + /// + ///```ignore + /// let response = client.upload() + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn upload(&self) -> builder::Upload<'_> { + builder::Upload::new(self) + } +} + +/// Types for composing operation parameters. +#[allow(clippy::all)] +pub mod builder { + use super::types; + #[allow(unused_imports)] + use super::{ + encode_path, ByteStream, ClientHooks, ClientInfo, Error, OperationInfo, RequestBuilderExt, + ResponseValue, + }; + ///Builder for [`Client::upload`] + /// + ///[`Client::upload`]: super::Client::upload + #[derive(Debug, Clone)] + pub struct Upload<'a> { + client: &'a super::Client, + body: Result, + } + + impl<'a> Upload<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + body: Ok(::std::default::Default::default()), + } + } + + pub fn body(mut self, value: V) -> Self + where + V: std::convert::TryInto, + >::Error: std::fmt::Display, + { + self.body = value + .try_into() + .map(From::from) + .map_err(|s| format!("conversion to `UploadForm` for body failed: {}", s)); + self + } + + pub fn body_map(mut self, f: F) -> Self + where + F: std::ops::FnOnce(types::builder::UploadForm) -> types::builder::UploadForm, + { + self.body = self.body.map(f); + self + } + + ///Sends a `POST` request to `/upload` + pub async fn send(self) -> Result, Error<()>> { + let Self { client, body } = self; + let body = body + .and_then(|v| types::UploadForm::try_from(v).map_err(|e| e.to_string())) + .map_err(Error::InvalidRequest)?; + let url = format!("{}/upload", client.baseurl,); + let mut header_map = ::reqwest::header::HeaderMap::with_capacity(1usize); + header_map.append( + ::reqwest::header::HeaderName::from_static("api-version"), + ::reqwest::header::HeaderValue::from_static(super::Client::api_version()), + ); + #[allow(unused_mut)] + let mut request = client + .client + .post(url) + .form_from_parts(body.as_form())? + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload", + }; + client.pre(&mut request, &info).await?; + let result = client.exec(request, &info).await; + client.post(&result, &info).await?; + let response = result?; + match response.status().as_u16() { + 200u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } +} + +/// Items consumers will typically use such as the Client and +/// extension traits. +pub mod prelude { + #[allow(unused_imports)] + pub use super::Client; +} diff --git a/progenitor-impl/tests/output/src/example_multipart_cli.rs b/progenitor-impl/tests/output/src/example_multipart_cli.rs new file mode 100644 index 000000000..26c02c3e7 --- /dev/null +++ b/progenitor-impl/tests/output/src/example_multipart_cli.rs @@ -0,0 +1,136 @@ +use crate::example_multipart_builder::*; +use anyhow::Context as _; +pub struct Cli { + client: Client, + config: T, +} + +impl Cli { + pub fn new(client: Client, config: T) -> Self { + Self { client, config } + } + + pub fn get_command(cmd: CliCommand) -> ::clap::Command { + match cmd { + CliCommand::Upload => Self::cli_upload(), + } + } + + pub fn cli_upload() -> ::clap::Command { + ::clap::Command::new("") + .arg( + ::clap::Arg::new("file") + .long("file") + .value_parser(::clap::value_parser!(::std::string::String)) + .required_unless_present("json-body"), + ) + .arg( + ::clap::Arg::new("name") + .long("name") + .value_parser(::clap::value_parser!(::std::string::String)) + .required_unless_present("json-body"), + ) + .arg( + ::clap::Arg::new("json-body") + .long("json-body") + .value_name("JSON-FILE") + .required(false) + .value_parser(::clap::value_parser!(std::path::PathBuf)) + .help("Path to a file that contains the full json body."), + ) + .arg( + ::clap::Arg::new("json-body-template") + .long("json-body-template") + .action(::clap::ArgAction::SetTrue) + .help("XXX"), + ) + } + + pub async fn execute( + &self, + cmd: CliCommand, + matches: &::clap::ArgMatches, + ) -> anyhow::Result<()> { + match cmd { + CliCommand::Upload => self.execute_upload(matches).await, + } + } + + pub async fn execute_upload(&self, matches: &::clap::ArgMatches) -> anyhow::Result<()> { + let mut request = self.client.upload(); + if let Some(value) = matches.get_one::<::std::string::String>("file") { + request = request.body_map(|body| body.file(value.clone())) + } + + if let Some(value) = matches.get_one::<::std::string::String>("name") { + request = request.body_map(|body| body.name(value.clone())) + } + + if let Some(value) = matches.get_one::("json-body") { + let body_txt = std::fs::read_to_string(value) + .with_context(|| format!("failed to read {}", value.display()))?; + let body_value = serde_json::from_str::(&body_txt) + .with_context(|| format!("failed to parse {}", value.display()))?; + request = request.body(body_value); + } + + self.config.execute_upload(matches, &mut request)?; + let result = request.send().await; + match result { + Ok(r) => { + self.config.success_no_item(&r); + Ok(()) + } + Err(r) => { + self.config.error(&r); + Err(anyhow::Error::new(r)) + } + } + } +} + +pub trait CliConfig { + fn success_item(&self, value: &ResponseValue) + where + T: std::clone::Clone + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn success_no_item(&self, value: &ResponseValue<()>); + fn error(&self, value: &Error) + where + T: std::clone::Clone + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn list_start(&self) + where + T: std::clone::Clone + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn list_item(&self, value: &T) + where + T: std::clone::Clone + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn list_end_success(&self) + where + T: std::clone::Clone + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn list_end_error(&self, value: &Error) + where + T: std::clone::Clone + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn execute_upload( + &self, + matches: &::clap::ArgMatches, + request: &mut builder::Upload, + ) -> anyhow::Result<()> { + Ok(()) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum CliCommand { + Upload, +} + +impl CliCommand { + pub fn iter() -> impl Iterator { + vec![CliCommand::Upload].into_iter() + } + + pub fn operation_id(&self) -> &'static str { + match self { + CliCommand::Upload => "upload", + } + } +} diff --git a/progenitor-impl/tests/output/src/example_multipart_httpmock.rs b/progenitor-impl/tests/output/src/example_multipart_httpmock.rs new file mode 100644 index 000000000..4446f7669 --- /dev/null +++ b/progenitor-impl/tests/output/src/example_multipart_httpmock.rs @@ -0,0 +1,63 @@ +pub mod operations { + #![doc = r" [`When`](::httpmock::When) and [`Then`](::httpmock::Then)"] + #![doc = r" wrappers for each operation. Each can be converted to"] + #![doc = r" its inner type with a call to `into_inner()`. This can"] + #![doc = r" be used to explicitly deviate from permitted values."] + use crate::example_multipart_builder::*; + pub struct UploadWhen(::httpmock::When); + impl UploadWhen { + pub fn new(inner: ::httpmock::When) -> Self { + Self( + inner + .method(::httpmock::Method::POST) + .path_matches(regex::Regex::new("^/upload$").unwrap()), + ) + } + + pub fn into_inner(self) -> ::httpmock::When { + self.0 + } + + pub fn body(self, value: &types::UploadForm) -> Self { + Self(self.0.json_body_obj(value)) + } + } + + pub struct UploadThen(::httpmock::Then); + impl UploadThen { + pub fn new(inner: ::httpmock::Then) -> Self { + Self(inner) + } + + pub fn into_inner(self) -> ::httpmock::Then { + self.0 + } + + pub fn ok(self) -> Self { + Self(self.0.status(200u16)) + } + } +} + +#[doc = r" An extension trait for [`MockServer`](::httpmock::MockServer) that"] +#[doc = r" adds a method for each operation. These are the equivalent of"] +#[doc = r" type-checked [`mock()`](::httpmock::MockServer::mock) calls."] +pub trait MockServerExt { + fn upload(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadWhen, operations::UploadThen); +} + +impl MockServerExt for ::httpmock::MockServer { + fn upload(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadWhen, operations::UploadThen), + { + self.mock(|when, then| { + config_fn( + operations::UploadWhen::new(when), + operations::UploadThen::new(then), + ) + }) + } +} diff --git a/progenitor-impl/tests/output/src/example_multipart_positional.rs b/progenitor-impl/tests/output/src/example_multipart_positional.rs new file mode 100644 index 000000000..8d5553aa1 --- /dev/null +++ b/progenitor-impl/tests/output/src/example_multipart_positional.rs @@ -0,0 +1,196 @@ +#[allow(unused_imports)] +use progenitor_client::{encode_path, ClientHooks, OperationInfo, RequestBuilderExt}; +#[allow(unused_imports)] +pub use progenitor_client::{ByteStream, ClientInfo, Error, ResponseValue}; +/// Types used as operation parameters and responses. +#[allow(clippy::all)] +pub mod types { + /// Error types. + pub mod error { + /// Error from a `TryFrom` or `FromStr` implementation. + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + + impl ::std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } + } + + ///`UploadForm` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "file", + /// "name" + /// ], + /// "properties": { + /// "file": { + /// "type": "string", + /// "format": "binary" + /// }, + /// "name": { + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct UploadForm { + pub file: ::std::string::String, + pub name: ::std::string::String, + } + + impl UploadForm { + /// Convert this form into an iterator of `(field_name, field_value)` + /// pairs + /// suitable for `multipart/form-data` encoding. + pub fn as_form(&self) -> Vec<(&'static str, progenitor_client::FormPart)> { + let mut parts = Vec::new(); + if let Some(ref val) = self.file { + parts.push(( + "file", + progenitor_client::FormPart::Binary(progenitor_client::BinaryFormPart { + data: val.clone().into(), + filename: None, + content_type: None, + }), + )); + } + if let Some(ref val) = self.name { + parts.push(( + "name", + progenitor_client::FormPart::Text(progenitor_client::TextFormPart { + value: val.to_string(), + content_type: None, + }), + )); + } + parts + } + } +} + +#[derive(Clone, Debug)] +///Client for Multipart Example +/// +///Version: 1.0.0 +pub struct Client { + pub(crate) baseurl: String, + pub(crate) client: reqwest::Client, +} + +impl Client { + /// Create a new client. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new(baseurl: &str) -> Self { + #[cfg(not(target_arch = "wasm32"))] + let client = { + let dur = ::std::time::Duration::from_secs(15u64); + reqwest::ClientBuilder::new() + .connect_timeout(dur) + .timeout(dur) + }; + #[cfg(target_arch = "wasm32")] + let client = reqwest::ClientBuilder::new(); + Self::new_with_client(baseurl, client.build().unwrap()) + } + + /// Construct a new client with an existing `reqwest::Client`, + /// allowing more control over its configuration. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { + Self { + baseurl: baseurl.to_string(), + client, + } + } +} + +impl ClientInfo<()> for Client { + fn api_version() -> &'static str { + "1.0.0" + } + + fn baseurl(&self) -> &str { + self.baseurl.as_str() + } + + fn client(&self) -> &reqwest::Client { + &self.client + } + + fn inner(&self) -> &() { + &() + } +} + +impl ClientHooks<()> for &Client {} +#[allow(clippy::all)] +impl Client { + ///Sends a `POST` request to `/upload` + pub async fn upload<'a>( + &'a self, + body: &'a types::UploadForm, + ) -> Result, Error<()>> { + let url = format!("{}/upload", self.baseurl,); + let mut header_map = ::reqwest::header::HeaderMap::with_capacity(1usize); + header_map.append( + ::reqwest::header::HeaderName::from_static("api-version"), + ::reqwest::header::HeaderValue::from_static(Self::api_version()), + ); + #[allow(unused_mut)] + let mut request = self + .client + .post(url) + .form_from_parts(body.as_form())? + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload", + }; + self.pre(&mut request, &info).await?; + let result = self.exec(request, &info).await; + self.post(&result, &info).await?; + let response = result?; + match response.status().as_u16() { + 200u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } +} + +/// Items consumers will typically use such as the Client. +pub mod prelude { + #[allow(unused_imports)] + pub use super::Client; +} diff --git a/progenitor-impl/tests/test_output.rs b/progenitor-impl/tests/test_output.rs index 6e6f18b51..299ef7ca1 100644 --- a/progenitor-impl/tests/test_output.rs +++ b/progenitor-impl/tests/test_output.rs @@ -163,6 +163,11 @@ fn test_cli_gen() { verify_apis("cli-gen.json"); } +#[test] +fn test_example_multipart() { + verify_apis("example_multipart.json"); +} + #[test] fn test_nexus_with_different_timeout() { const OPENAPI_FILE: &'static str = "nexus.json"; diff --git a/progenitor-impl/tests/test_specific.rs b/progenitor-impl/tests/test_specific.rs index 1b623904c..33654d980 100644 --- a/progenitor-impl/tests/test_specific.rs +++ b/progenitor-impl/tests/test_specific.rs @@ -355,3 +355,572 @@ async fn test_stream_pagination() { server.close().await.expect("failed to close server"); } + +mod multipart_tests { + use openapiv3::OpenAPI; + use progenitor_impl::{GenerationSettings, Generator, InterfaceStyle}; + + fn make_multipart_spec(operation_id: &str, properties: serde_json::Value) -> OpenAPI { + serde_json::from_value(serde_json::json!({ + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0.0" }, + "paths": { + "/upload": { + "post": { + "operationId": operation_id, + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": properties + } + } + } + }, + "responses": { + "200": { "description": "OK" } + } + } + } + } + })) + .unwrap() + } + + fn make_multipart_spec_with_encoding( + operation_id: &str, + properties: serde_json::Value, + encoding: serde_json::Value, + ) -> OpenAPI { + serde_json::from_value(serde_json::json!({ + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0.0" }, + "paths": { + "/upload": { + "post": { + "operationId": operation_id, + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": properties + }, + "encoding": encoding + } + } + }, + "responses": { + "200": { "description": "OK" } + } + } + } + } + })) + .unwrap() + } + + fn make_multipart_component_ref_spec(operation_id: &str) -> OpenAPI { + serde_json::from_value(serde_json::json!({ + "openapi": "3.0.0", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/resource": { + "post": { + "operationId": operation_id, + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/UploadRequestDto" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" } + }, + "required": ["id"] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "UploadRequestDto": { + "type": "object", + "properties": { + "data": { "type": "string", "format": "binary" }, + "plainString": { "type": "string" }, + "someDate": { "type": "string", "format": "date-time" }, + "flag": { "type": "boolean" }, + "identifier": { "type": "string", "format": "uuid" }, + "extra": { "type": "string", "format": "binary" } + }, + "required": ["data", "plainString", "someDate", "identifier"] + } + } + } + })) + .unwrap() + } + + fn make_builder_generator() -> Generator { + Generator::new(GenerationSettings::default().with_interface(InterfaceStyle::Builder)) + } + + fn make_positional_generator() -> Generator { + Generator::new(GenerationSettings::default().with_interface(InterfaceStyle::Positional)) + } + + fn assert_generates_ok(spec: &OpenAPI, msg: &str) -> String { + let mut generator = make_builder_generator(); + let result = generator.generate_tokens(spec); + assert!(result.is_ok(), "{}: {:?}", msg, result.err()); + result.unwrap().to_string() + } + + fn assert_generates_err(spec: &OpenAPI, expected_err: &str) { + let mut generator = make_builder_generator(); + let result = generator.generate_tokens(spec); + assert!(result.is_err(), "Expected generation to fail"); + let err = result.unwrap_err().to_string(); + assert!( + err.contains(expected_err), + "Error should contain '{}': {}", + expected_err, + err + ); + } + + fn binary_file_prop() -> serde_json::Value { + serde_json::json!({ "type": "string", "format": "binary" }) + } + + fn string_prop() -> serde_json::Value { + serde_json::json!({ "type": "string" }) + } + + fn integer_prop() -> serde_json::Value { + serde_json::json!({ "type": "integer" }) + } + + fn boolean_prop() -> serde_json::Value { + serde_json::json!({ "type": "boolean" }) + } + + fn number_prop() -> serde_json::Value { + serde_json::json!({ "type": "number", "format": "double" }) + } + + fn string_array_prop() -> serde_json::Value { + serde_json::json!({ "type": "array", "items": { "type": "string" } }) + } + + fn integer_array_prop() -> serde_json::Value { + serde_json::json!({ "type": "array", "items": { "type": "integer" } }) + } + + fn simple_object_prop() -> serde_json::Value { + serde_json::json!({ + "type": "object", + "properties": { + "name": { "type": "string" } + } + }) + } + + #[test] + fn test_multipart_binary_only_succeeds() { + let spec = make_multipart_spec( + "upload_file", + serde_json::json!({ + "file": binary_file_prop() + }), + ); + assert_generates_ok(&spec, "Binary-only multipart should be supported"); + } + + #[test] + fn test_multipart_mixed_types_supported() { + let spec = make_multipart_spec( + "upload_with_name", + serde_json::json!({ + "file": binary_file_prop(), + "name": string_prop() + }), + ); + let code = assert_generates_ok(&spec, "Mixed types in multipart should be supported"); + assert!(code.contains("UploadWithNameForm")); + } + + #[test] + fn test_multipart_nested_objects_supported() { + let spec = make_multipart_spec( + "upload_with_metadata", + serde_json::json!({ + "file": binary_file_prop(), + "metadata": { + "type": "object", + "properties": { + "author": { "type": "string" } + } + } + }), + ); + assert_generates_ok(&spec, "Nested objects in multipart should be supported"); + } + + #[test] + fn test_multipart_integer_fields_supported() { + let spec = make_multipart_spec( + "upload_with_count", + serde_json::json!({ + "file": binary_file_prop(), + "count": integer_prop() + }), + ); + assert_generates_ok(&spec, "Integer fields in multipart should be supported"); + } + + #[test] + fn test_multipart_array_fields_supported() { + let spec = make_multipart_spec( + "upload_with_tags", + serde_json::json!({ + "file": binary_file_prop(), + "tags": string_array_prop() + }), + ); + assert_generates_ok(&spec, "Array fields in multipart should be supported"); + } + + #[test] + fn test_multipart_multiple_binary_files_succeeds() { + let spec = make_multipart_spec( + "upload_multiple", + serde_json::json!({ + "primary": binary_file_prop(), + "secondary": binary_file_prop(), + "thumbnail": binary_file_prop() + }), + ); + assert_generates_ok(&spec, "Multiple binary files should be supported"); + } + + #[test] + fn test_multipart_encoding_content_type() { + let spec = make_multipart_spec_with_encoding( + "upload_with_encoding", + serde_json::json!({ + "file": binary_file_prop(), + "metadata": { + "type": "object", + "properties": { + "author": string_prop(), + "tags": string_array_prop() + } + } + }), + serde_json::json!({ + "file": { "contentType": "application/octet-stream" }, + "metadata": { "contentType": "application/json" } + }), + ); + let code = assert_generates_ok(&spec, "Encoding object should be supported"); + assert!(code.contains("UploadWithEncodingForm")); + } + + #[test] + fn test_multipart_array_json_serialization() { + let spec = make_multipart_spec( + "upload_with_array", + serde_json::json!({ + "tags": string_array_prop() + }), + ); + let code = assert_generates_ok(&spec, "Array fields should be supported"); + // TokenStream::to_string() uses spaces between tokens + assert!(code.contains("serde_json :: to_string")); + } + + #[test] + fn test_multipart_object_json_serialization() { + let spec = make_multipart_spec( + "upload_with_object", + serde_json::json!({ + "metadata": simple_object_prop() + }), + ); + let code = assert_generates_ok(&spec, "Object fields should be supported"); + assert!(code.contains("serde_json :: to_string")); + } + + #[test] + fn test_multipart_boolean_fields_supported() { + let spec = make_multipart_spec( + "upload_with_flag", + serde_json::json!({ + "file": binary_file_prop(), + "is_public": boolean_prop() + }), + ); + let code = assert_generates_ok(&spec, "Boolean fields in multipart should be supported"); + assert!(code.contains("UploadWithFlagForm")); + } + + #[test] + fn test_multipart_number_fields_supported() { + let spec = make_multipart_spec( + "upload_with_score", + serde_json::json!({ + "file": binary_file_prop(), + "score": number_prop() + }), + ); + assert_generates_ok(&spec, "Number fields in multipart should be supported"); + } + + #[test] + fn test_multipart_required_fields() { + let spec: OpenAPI = serde_json::from_value(serde_json::json!({ + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0.0" }, + "paths": { + "/upload": { + "post": { + "operationId": "upload_required", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": ["file", "name"], + "properties": { + "file": binary_file_prop(), + "name": string_prop(), + "description": string_prop() + } + } + } + } + }, + "responses": { "200": { "description": "OK" } } + } + } + } + })) + .unwrap(); + + let code = assert_generates_ok(&spec, "Required fields in multipart should be supported"); + assert!(code.contains("UploadRequiredForm")); + } + + #[test] + fn test_multipart_with_schema_reference() { + let spec: OpenAPI = serde_json::from_value(serde_json::json!({ + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0.0" }, + "paths": { + "/upload": { + "post": { + "operationId": "upload_with_ref", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": binary_file_prop(), + "metadata": { "$ref": "#/components/schemas/Metadata" } + } + } + } + } + }, + "responses": { "200": { "description": "OK" } } + } + } + }, + "components": { + "schemas": { + "Metadata": { + "type": "object", + "properties": { + "author": { "type": "string" }, + "version": { "type": "integer" } + } + } + } + } + })) + .unwrap(); + + let code = assert_generates_ok(&spec, "Schema references in multipart should be supported"); + assert!(code.contains("serde_json :: to_string")); + } + + #[test] + fn test_multipart_non_object_schema_fails() { + let spec: OpenAPI = serde_json::from_value(serde_json::json!({ + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0.0" }, + "paths": { + "/upload": { + "post": { + "operationId": "upload_bad", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { "200": { "description": "OK" } } + } + } + } + })) + .unwrap(); + + assert_generates_err(&spec, "object schema"); + } + + #[test] + #[should_panic(expected = "unreachable")] + fn test_multipart_empty_properties_panics() { + let spec = make_multipart_spec("upload_empty", serde_json::json!({})); + let mut generator = make_builder_generator(); + let _ = generator.generate_tokens(&spec); + } + + #[test] + fn test_multipart_array_of_integers() { + let spec = make_multipart_spec( + "upload_ids", + serde_json::json!({ + "ids": integer_array_prop() + }), + ); + let code = assert_generates_ok(&spec, "Array of integers in multipart should be supported"); + assert!(code.contains("serde_json :: to_string")); + } + + #[test] + fn test_multipart_deeply_nested_object() { + let spec = make_multipart_spec( + "upload_nested", + serde_json::json!({ + "config": { + "type": "object", + "properties": { + "settings": { + "type": "object", + "properties": { + "level1": { + "type": "object", + "properties": { + "value": { "type": "string" } + } + } + } + } + } + } + }), + ); + assert_generates_ok( + &spec, + "Deeply nested objects in multipart should be supported", + ); + } + + #[test] + fn test_multipart_multiple_encoding_entries() { + let spec = make_multipart_spec_with_encoding( + "upload_multi_encoding", + serde_json::json!({ + "image": binary_file_prop(), + "document": binary_file_prop(), + "data": simple_object_prop() + }), + serde_json::json!({ + "image": { "contentType": "image/png" }, + "document": { "contentType": "application/pdf" }, + "data": { "contentType": "application/xml" } + }), + ); + let code = assert_generates_ok(&spec, "Multiple encoding entries should be supported"); + assert!(code.contains("image/png") || code.contains("image / png")); + } + + #[test] + fn test_multipart_positional_interface() { + let spec = make_multipart_spec( + "upload_file", + serde_json::json!({ + "file": binary_file_prop(), + "name": string_prop() + }), + ); + + let mut generator = make_positional_generator(); + let result = generator.generate_tokens(&spec); + assert!( + result.is_ok(), + "Multipart should work with positional interface: {:?}", + result.err() + ); + assert!(result.unwrap().to_string().contains("UploadFileForm")); + } + + #[test] + fn test_multipart_enum_string_field() { + let spec = make_multipart_spec( + "upload_with_category", + serde_json::json!({ + "file": binary_file_prop(), + "category": { + "type": "string", + "enum": ["image", "document", "video"] + } + }), + ); + assert_generates_ok(&spec, "Enum string fields in multipart should be supported"); + } + + #[test] + fn test_multipart_component_ref_supports_string_formats_and_api_field_names() { + let spec = make_multipart_component_ref_spec("upload"); + let code = assert_generates_ok( + &spec, + "Multipart component refs with string formats should be supported", + ); + + assert!(code.contains("self . plain_string")); + assert!(code.contains("\"plainString\"")); + assert!(code.contains("self . some_date")); + assert!(code.contains("\"someDate\"")); + assert!(code.contains("self . identifier")); + assert!(code.contains("\"identifier\"")); + assert!(code.contains("self . flag")); + assert!(code.contains("self . extra")); + } +} diff --git a/sample_openapi/example_multipart.json b/sample_openapi/example_multipart.json new file mode 100644 index 000000000..0bcae8a5b --- /dev/null +++ b/sample_openapi/example_multipart.json @@ -0,0 +1,39 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Multipart Example", + "version": "1.0.0" + }, + "paths": { + "/upload": { + "post": { + "operationId": "upload", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": ["file", "name"], + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "name": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + } +}