├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── basic_translation.rs └── upload_document.rs ├── flake.lock ├── flake.nix └── src ├── endpoint ├── document.rs ├── glossary.rs ├── languages.rs ├── mod.rs ├── translate.rs └── usage.rs ├── lang.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.txt 3 | *.docx 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.6.6 - 2025-07-17 4 | 5 | - Add support for choosing different model 6 | - Bump dependencies 7 | 8 | ```text 9 | name old req compatible latest new req 10 | ==== ======= ========== ====== ======= 11 | thiserror 2.0.3 2.0.12 2.0.12 2.0.12 12 | reqwest 0.12.9 0.12.22 0.12.22 0.12.22 13 | serde 1.0.215 1.0.219 1.0.219 1.0.219 14 | serde_json 1.0.133 1.0.140 1.0.140 1.0.140 15 | tokio 1.41.1 1.46.1 1.46.1 1.46.1 16 | tokio-stream 0.1.16 0.1.17 0.1.17 0.1.17 17 | ``` 18 | 19 | ## v0.6.5 - 2024-12-03 20 | 21 | - Refactor translate API with JSON parser 22 | - Bump all dependencies 23 | ```text 24 | name old req latest 25 | ==== ======= ====== 26 | thiserror 1.0.63 2.0.3 27 | reqwest 0.12.7 0.12.9 28 | serde 1.0.208 1.0.215 29 | serde_json 1.0.125 1.0.133 30 | tokio 1.39.3 1.41.1 31 | tokio-stream 0.1.15 0.1.16 32 | ``` 33 | 34 | ## v0.6.4 - 2024-08-21 35 | - Add derive of `hash` for `Lang` 36 | - Add `ZH-HANS` and `ZH-HANT` 37 | - Bump dependencies: 38 | * thiserror: `1.0.35` -> `1.0.63` 39 | * reqwest: `0.12.4` -> `0.12.7` 40 | * serde: `1.0.144` -> `1.0.208` 41 | * tokio: `1.21.1` -> `1.39.3` 42 | * tokio-stream: `0.1.11` -> `0.1.15` 43 | * paste: `1.0.11` -> `1.0.15` 44 | * typed-builder: `0.18` -> `0.19` 45 | 46 | ## v0.6.3 - 2024-04-28 47 | 48 | - Add FromStr and Display trait implementation for Lang 49 | 50 | ## v0.6.2 - 2024-03-19 51 | 52 | - Add support for AR (Arabic) 53 | - Support automatically switch API backend 54 | 55 | ## v0.6.1 - 2024-03-01 56 | 57 | - Wrap panic into Result 58 | 59 | ## v0.6.0 - 2024-02-21 60 | 61 | - Fix incorrect implmentation of glossary API 62 | 63 | ## v0.5.1 - 2024-02-21 64 | 65 | - Add `context` field into translate API 66 | - make all field in glossary API as public 67 | 68 | ## v0.5.0 - 2024-02-21 69 | 70 | - Add type constraint for glossary APIs 71 | 72 | ## v0.4.6 - 2024-02-14 73 | 74 | - Implement all the glossaries related API 75 | 76 | ## v0.4.5 - 2024-01-26 77 | - Re-export the `LangConvertError` struct 78 | 79 | ## v0.4.4 - 2023-11-20 80 | 81 | - Add new `languages` endpoint 82 | - Add `KO` and `NB` language variant 83 | 84 | ## [v0.4.3] - 2023-09-11 85 | 86 | - Improve code document 87 | 88 | ## [v0.4.2] - 2023-06-23 89 | 90 | - Include formality in impl_requester 91 | 92 | ## [v0.4.1] - 2023-02-23 93 | 94 | - Add `Clone` derive for `Lang` 95 | 96 | ## [v0.4.0] - 2023-01-26 97 | 98 | ### Changed 99 | 100 | - (**BREAKING**) Implement auto send for all endpoint 101 | - (**BREAKING**) `DeepLApi` implementation is now separated to multiple endpoint file 102 | - (**BREAKING**) `DeepLApiResponse` is now renamed to `TranslateTextResp` 103 | - (**BREAKING**) `DeepLApi` is now init by `::with()` function and build by `.new()` function 104 | - Using `docx-rs` to parse document content for testing 105 | 106 | ## [v0.3.0] - 2023-01-10 107 | 108 | ### Changed 109 | 110 | - (**BREAKING**) `Lang::from` is now replaced with `Lang::try_from` 111 | 112 | ## [v0.2.0] - 2023-01-08 113 | 114 | ### Added 115 | 116 | - Full API options for endpoint `translate` 117 | - New builder for DeepLApi 118 | - Complete some missing document 119 | 120 | ### Changed 121 | 122 | - Correct all the typo 123 | - `reqwest` crate is re-exported 124 | - (**BREAKING**) `translate` function only accept `TranslateTextProp` now 125 | 126 | ## [v0.1.6] - 2022-12-02 127 | 128 | ### Fixed 129 | 130 | - Fix document download issue 131 | 132 | ### Changed 133 | 134 | - Use `AsRef` as `UploadDocumentProp::file_path` type 135 | - Use only `output` parameter for function `download_document` 136 | 137 | ## [v0.1.3] - 2022-11-29 138 | 139 | ### Added 140 | 141 | - New upload document API 142 | - More language variants implemented 143 | - Allow user using Pro version API 144 | 145 | ## [v0.1.2] - 2022-09-20 146 | 147 | ### Added 148 | 149 | - New API `get_usage()` to get current API usage 150 | 151 | ### Changed 152 | 153 | - Replace Anyhow::Result with custom Error 154 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 | 20 | [[package]] 21 | name = "atomic-waker" 22 | version = "1.1.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 25 | 26 | [[package]] 27 | name = "autocfg" 28 | version = "1.5.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 31 | 32 | [[package]] 33 | name = "backtrace" 34 | version = "0.3.75" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 37 | dependencies = [ 38 | "addr2line", 39 | "cfg-if", 40 | "libc", 41 | "miniz_oxide", 42 | "object", 43 | "rustc-demangle", 44 | "windows-targets 0.52.6", 45 | ] 46 | 47 | [[package]] 48 | name = "base64" 49 | version = "0.13.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 52 | 53 | [[package]] 54 | name = "base64" 55 | version = "0.22.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 58 | 59 | [[package]] 60 | name = "bitflags" 61 | version = "1.3.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 64 | 65 | [[package]] 66 | name = "bitflags" 67 | version = "2.9.1" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 70 | 71 | [[package]] 72 | name = "bumpalo" 73 | version = "3.19.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 76 | 77 | [[package]] 78 | name = "bytemuck" 79 | version = "1.23.1" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" 82 | 83 | [[package]] 84 | name = "byteorder" 85 | version = "1.5.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 88 | 89 | [[package]] 90 | name = "bytes" 91 | version = "1.10.1" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 94 | 95 | [[package]] 96 | name = "cc" 97 | version = "1.2.29" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" 100 | dependencies = [ 101 | "shlex", 102 | ] 103 | 104 | [[package]] 105 | name = "cfg-if" 106 | version = "1.0.1" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 109 | 110 | [[package]] 111 | name = "color_quant" 112 | version = "1.1.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 115 | 116 | [[package]] 117 | name = "core-foundation" 118 | version = "0.9.4" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 121 | dependencies = [ 122 | "core-foundation-sys", 123 | "libc", 124 | ] 125 | 126 | [[package]] 127 | name = "core-foundation-sys" 128 | version = "0.8.7" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 131 | 132 | [[package]] 133 | name = "crc32fast" 134 | version = "1.5.0" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 137 | dependencies = [ 138 | "cfg-if", 139 | ] 140 | 141 | [[package]] 142 | name = "crossbeam-utils" 143 | version = "0.8.21" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 146 | 147 | [[package]] 148 | name = "deepl" 149 | version = "0.6.6" 150 | dependencies = [ 151 | "docx-rs", 152 | "paste", 153 | "reqwest", 154 | "serde", 155 | "serde_json", 156 | "thiserror 2.0.12", 157 | "tokio", 158 | "tokio-stream", 159 | "typed-builder", 160 | ] 161 | 162 | [[package]] 163 | name = "displaydoc" 164 | version = "0.2.5" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 167 | dependencies = [ 168 | "proc-macro2", 169 | "quote", 170 | "syn", 171 | ] 172 | 173 | [[package]] 174 | name = "docx-rs" 175 | version = "0.4.17" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "e593b51d4fe95d69d70fd40da4b314b029736302c986c3c760826e842fd27dc3" 178 | dependencies = [ 179 | "base64 0.13.1", 180 | "image", 181 | "serde", 182 | "serde_json", 183 | "thiserror 1.0.69", 184 | "xml-rs", 185 | "zip", 186 | ] 187 | 188 | [[package]] 189 | name = "encoding_rs" 190 | version = "0.8.35" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 193 | dependencies = [ 194 | "cfg-if", 195 | ] 196 | 197 | [[package]] 198 | name = "equivalent" 199 | version = "1.0.2" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 202 | 203 | [[package]] 204 | name = "errno" 205 | version = "0.3.13" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 208 | dependencies = [ 209 | "libc", 210 | "windows-sys 0.60.2", 211 | ] 212 | 213 | [[package]] 214 | name = "fastrand" 215 | version = "2.3.0" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 218 | 219 | [[package]] 220 | name = "fdeflate" 221 | version = "0.3.7" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 224 | dependencies = [ 225 | "simd-adler32", 226 | ] 227 | 228 | [[package]] 229 | name = "flate2" 230 | version = "1.1.2" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" 233 | dependencies = [ 234 | "crc32fast", 235 | "miniz_oxide", 236 | ] 237 | 238 | [[package]] 239 | name = "fnv" 240 | version = "1.0.7" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 243 | 244 | [[package]] 245 | name = "foreign-types" 246 | version = "0.3.2" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 249 | dependencies = [ 250 | "foreign-types-shared", 251 | ] 252 | 253 | [[package]] 254 | name = "foreign-types-shared" 255 | version = "0.1.1" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 258 | 259 | [[package]] 260 | name = "form_urlencoded" 261 | version = "1.2.1" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 264 | dependencies = [ 265 | "percent-encoding", 266 | ] 267 | 268 | [[package]] 269 | name = "futures-channel" 270 | version = "0.3.31" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 273 | dependencies = [ 274 | "futures-core", 275 | ] 276 | 277 | [[package]] 278 | name = "futures-core" 279 | version = "0.3.31" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 282 | 283 | [[package]] 284 | name = "futures-io" 285 | version = "0.3.31" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 288 | 289 | [[package]] 290 | name = "futures-macro" 291 | version = "0.3.31" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 294 | dependencies = [ 295 | "proc-macro2", 296 | "quote", 297 | "syn", 298 | ] 299 | 300 | [[package]] 301 | name = "futures-sink" 302 | version = "0.3.31" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 305 | 306 | [[package]] 307 | name = "futures-task" 308 | version = "0.3.31" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 311 | 312 | [[package]] 313 | name = "futures-util" 314 | version = "0.3.31" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 317 | dependencies = [ 318 | "futures-core", 319 | "futures-io", 320 | "futures-macro", 321 | "futures-sink", 322 | "futures-task", 323 | "memchr", 324 | "pin-project-lite", 325 | "pin-utils", 326 | "slab", 327 | ] 328 | 329 | [[package]] 330 | name = "getrandom" 331 | version = "0.2.16" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 334 | dependencies = [ 335 | "cfg-if", 336 | "libc", 337 | "wasi 0.11.1+wasi-snapshot-preview1", 338 | ] 339 | 340 | [[package]] 341 | name = "getrandom" 342 | version = "0.3.3" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 345 | dependencies = [ 346 | "cfg-if", 347 | "libc", 348 | "r-efi", 349 | "wasi 0.14.2+wasi-0.2.4", 350 | ] 351 | 352 | [[package]] 353 | name = "gif" 354 | version = "0.13.3" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" 357 | dependencies = [ 358 | "color_quant", 359 | "weezl", 360 | ] 361 | 362 | [[package]] 363 | name = "gimli" 364 | version = "0.31.1" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 367 | 368 | [[package]] 369 | name = "h2" 370 | version = "0.4.11" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" 373 | dependencies = [ 374 | "atomic-waker", 375 | "bytes", 376 | "fnv", 377 | "futures-core", 378 | "futures-sink", 379 | "http", 380 | "indexmap", 381 | "slab", 382 | "tokio", 383 | "tokio-util", 384 | "tracing", 385 | ] 386 | 387 | [[package]] 388 | name = "hashbrown" 389 | version = "0.15.4" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" 392 | 393 | [[package]] 394 | name = "http" 395 | version = "1.3.1" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 398 | dependencies = [ 399 | "bytes", 400 | "fnv", 401 | "itoa", 402 | ] 403 | 404 | [[package]] 405 | name = "http-body" 406 | version = "1.0.1" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 409 | dependencies = [ 410 | "bytes", 411 | "http", 412 | ] 413 | 414 | [[package]] 415 | name = "http-body-util" 416 | version = "0.1.3" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 419 | dependencies = [ 420 | "bytes", 421 | "futures-core", 422 | "http", 423 | "http-body", 424 | "pin-project-lite", 425 | ] 426 | 427 | [[package]] 428 | name = "httparse" 429 | version = "1.10.1" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 432 | 433 | [[package]] 434 | name = "hyper" 435 | version = "1.6.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 438 | dependencies = [ 439 | "bytes", 440 | "futures-channel", 441 | "futures-util", 442 | "h2", 443 | "http", 444 | "http-body", 445 | "httparse", 446 | "itoa", 447 | "pin-project-lite", 448 | "smallvec", 449 | "tokio", 450 | "want", 451 | ] 452 | 453 | [[package]] 454 | name = "hyper-rustls" 455 | version = "0.27.7" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 458 | dependencies = [ 459 | "http", 460 | "hyper", 461 | "hyper-util", 462 | "rustls", 463 | "rustls-pki-types", 464 | "tokio", 465 | "tokio-rustls", 466 | "tower-service", 467 | ] 468 | 469 | [[package]] 470 | name = "hyper-tls" 471 | version = "0.6.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 474 | dependencies = [ 475 | "bytes", 476 | "http-body-util", 477 | "hyper", 478 | "hyper-util", 479 | "native-tls", 480 | "tokio", 481 | "tokio-native-tls", 482 | "tower-service", 483 | ] 484 | 485 | [[package]] 486 | name = "hyper-util" 487 | version = "0.1.15" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" 490 | dependencies = [ 491 | "base64 0.22.1", 492 | "bytes", 493 | "futures-channel", 494 | "futures-core", 495 | "futures-util", 496 | "http", 497 | "http-body", 498 | "hyper", 499 | "ipnet", 500 | "libc", 501 | "percent-encoding", 502 | "pin-project-lite", 503 | "socket2", 504 | "system-configuration", 505 | "tokio", 506 | "tower-service", 507 | "tracing", 508 | "windows-registry", 509 | ] 510 | 511 | [[package]] 512 | name = "icu_collections" 513 | version = "2.0.0" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 516 | dependencies = [ 517 | "displaydoc", 518 | "potential_utf", 519 | "yoke", 520 | "zerofrom", 521 | "zerovec", 522 | ] 523 | 524 | [[package]] 525 | name = "icu_locale_core" 526 | version = "2.0.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 529 | dependencies = [ 530 | "displaydoc", 531 | "litemap", 532 | "tinystr", 533 | "writeable", 534 | "zerovec", 535 | ] 536 | 537 | [[package]] 538 | name = "icu_normalizer" 539 | version = "2.0.0" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 542 | dependencies = [ 543 | "displaydoc", 544 | "icu_collections", 545 | "icu_normalizer_data", 546 | "icu_properties", 547 | "icu_provider", 548 | "smallvec", 549 | "zerovec", 550 | ] 551 | 552 | [[package]] 553 | name = "icu_normalizer_data" 554 | version = "2.0.0" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 557 | 558 | [[package]] 559 | name = "icu_properties" 560 | version = "2.0.1" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 563 | dependencies = [ 564 | "displaydoc", 565 | "icu_collections", 566 | "icu_locale_core", 567 | "icu_properties_data", 568 | "icu_provider", 569 | "potential_utf", 570 | "zerotrie", 571 | "zerovec", 572 | ] 573 | 574 | [[package]] 575 | name = "icu_properties_data" 576 | version = "2.0.1" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 579 | 580 | [[package]] 581 | name = "icu_provider" 582 | version = "2.0.0" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 585 | dependencies = [ 586 | "displaydoc", 587 | "icu_locale_core", 588 | "stable_deref_trait", 589 | "tinystr", 590 | "writeable", 591 | "yoke", 592 | "zerofrom", 593 | "zerotrie", 594 | "zerovec", 595 | ] 596 | 597 | [[package]] 598 | name = "idna" 599 | version = "1.0.3" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 602 | dependencies = [ 603 | "idna_adapter", 604 | "smallvec", 605 | "utf8_iter", 606 | ] 607 | 608 | [[package]] 609 | name = "idna_adapter" 610 | version = "1.2.1" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 613 | dependencies = [ 614 | "icu_normalizer", 615 | "icu_properties", 616 | ] 617 | 618 | [[package]] 619 | name = "image" 620 | version = "0.24.9" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" 623 | dependencies = [ 624 | "bytemuck", 625 | "byteorder", 626 | "color_quant", 627 | "gif", 628 | "jpeg-decoder", 629 | "num-traits", 630 | "png", 631 | "tiff", 632 | ] 633 | 634 | [[package]] 635 | name = "indexmap" 636 | version = "2.10.0" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 639 | dependencies = [ 640 | "equivalent", 641 | "hashbrown", 642 | ] 643 | 644 | [[package]] 645 | name = "io-uring" 646 | version = "0.7.8" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" 649 | dependencies = [ 650 | "bitflags 2.9.1", 651 | "cfg-if", 652 | "libc", 653 | ] 654 | 655 | [[package]] 656 | name = "ipnet" 657 | version = "2.11.0" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 660 | 661 | [[package]] 662 | name = "iri-string" 663 | version = "0.7.8" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 666 | dependencies = [ 667 | "memchr", 668 | "serde", 669 | ] 670 | 671 | [[package]] 672 | name = "itoa" 673 | version = "1.0.15" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 676 | 677 | [[package]] 678 | name = "jpeg-decoder" 679 | version = "0.3.2" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" 682 | 683 | [[package]] 684 | name = "js-sys" 685 | version = "0.3.77" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 688 | dependencies = [ 689 | "once_cell", 690 | "wasm-bindgen", 691 | ] 692 | 693 | [[package]] 694 | name = "libc" 695 | version = "0.2.174" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 698 | 699 | [[package]] 700 | name = "linux-raw-sys" 701 | version = "0.9.4" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 704 | 705 | [[package]] 706 | name = "litemap" 707 | version = "0.8.0" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 710 | 711 | [[package]] 712 | name = "log" 713 | version = "0.4.27" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 716 | 717 | [[package]] 718 | name = "memchr" 719 | version = "2.7.5" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 722 | 723 | [[package]] 724 | name = "mime" 725 | version = "0.3.17" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 728 | 729 | [[package]] 730 | name = "mime_guess" 731 | version = "2.0.5" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 734 | dependencies = [ 735 | "mime", 736 | "unicase", 737 | ] 738 | 739 | [[package]] 740 | name = "miniz_oxide" 741 | version = "0.8.9" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 744 | dependencies = [ 745 | "adler2", 746 | "simd-adler32", 747 | ] 748 | 749 | [[package]] 750 | name = "mio" 751 | version = "1.0.4" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 754 | dependencies = [ 755 | "libc", 756 | "wasi 0.11.1+wasi-snapshot-preview1", 757 | "windows-sys 0.59.0", 758 | ] 759 | 760 | [[package]] 761 | name = "native-tls" 762 | version = "0.2.14" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 765 | dependencies = [ 766 | "libc", 767 | "log", 768 | "openssl", 769 | "openssl-probe", 770 | "openssl-sys", 771 | "schannel", 772 | "security-framework", 773 | "security-framework-sys", 774 | "tempfile", 775 | ] 776 | 777 | [[package]] 778 | name = "num-traits" 779 | version = "0.2.19" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 782 | dependencies = [ 783 | "autocfg", 784 | ] 785 | 786 | [[package]] 787 | name = "object" 788 | version = "0.36.7" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 791 | dependencies = [ 792 | "memchr", 793 | ] 794 | 795 | [[package]] 796 | name = "once_cell" 797 | version = "1.21.3" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 800 | 801 | [[package]] 802 | name = "openssl" 803 | version = "0.10.73" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 806 | dependencies = [ 807 | "bitflags 2.9.1", 808 | "cfg-if", 809 | "foreign-types", 810 | "libc", 811 | "once_cell", 812 | "openssl-macros", 813 | "openssl-sys", 814 | ] 815 | 816 | [[package]] 817 | name = "openssl-macros" 818 | version = "0.1.1" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 821 | dependencies = [ 822 | "proc-macro2", 823 | "quote", 824 | "syn", 825 | ] 826 | 827 | [[package]] 828 | name = "openssl-probe" 829 | version = "0.1.6" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 832 | 833 | [[package]] 834 | name = "openssl-sys" 835 | version = "0.9.109" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 838 | dependencies = [ 839 | "cc", 840 | "libc", 841 | "pkg-config", 842 | "vcpkg", 843 | ] 844 | 845 | [[package]] 846 | name = "paste" 847 | version = "1.0.15" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 850 | 851 | [[package]] 852 | name = "percent-encoding" 853 | version = "2.3.1" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 856 | 857 | [[package]] 858 | name = "pin-project-lite" 859 | version = "0.2.16" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 862 | 863 | [[package]] 864 | name = "pin-utils" 865 | version = "0.1.0" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 868 | 869 | [[package]] 870 | name = "pkg-config" 871 | version = "0.3.32" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 874 | 875 | [[package]] 876 | name = "png" 877 | version = "0.17.16" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 880 | dependencies = [ 881 | "bitflags 1.3.2", 882 | "crc32fast", 883 | "fdeflate", 884 | "flate2", 885 | "miniz_oxide", 886 | ] 887 | 888 | [[package]] 889 | name = "potential_utf" 890 | version = "0.1.2" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 893 | dependencies = [ 894 | "zerovec", 895 | ] 896 | 897 | [[package]] 898 | name = "proc-macro2" 899 | version = "1.0.95" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 902 | dependencies = [ 903 | "unicode-ident", 904 | ] 905 | 906 | [[package]] 907 | name = "quote" 908 | version = "1.0.40" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 911 | dependencies = [ 912 | "proc-macro2", 913 | ] 914 | 915 | [[package]] 916 | name = "r-efi" 917 | version = "5.3.0" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 920 | 921 | [[package]] 922 | name = "reqwest" 923 | version = "0.12.22" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" 926 | dependencies = [ 927 | "base64 0.22.1", 928 | "bytes", 929 | "encoding_rs", 930 | "futures-core", 931 | "futures-util", 932 | "h2", 933 | "http", 934 | "http-body", 935 | "http-body-util", 936 | "hyper", 937 | "hyper-rustls", 938 | "hyper-tls", 939 | "hyper-util", 940 | "js-sys", 941 | "log", 942 | "mime", 943 | "mime_guess", 944 | "native-tls", 945 | "percent-encoding", 946 | "pin-project-lite", 947 | "rustls-pki-types", 948 | "serde", 949 | "serde_json", 950 | "serde_urlencoded", 951 | "sync_wrapper", 952 | "tokio", 953 | "tokio-native-tls", 954 | "tokio-util", 955 | "tower", 956 | "tower-http", 957 | "tower-service", 958 | "url", 959 | "wasm-bindgen", 960 | "wasm-bindgen-futures", 961 | "wasm-streams", 962 | "web-sys", 963 | ] 964 | 965 | [[package]] 966 | name = "ring" 967 | version = "0.17.14" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 970 | dependencies = [ 971 | "cc", 972 | "cfg-if", 973 | "getrandom 0.2.16", 974 | "libc", 975 | "untrusted", 976 | "windows-sys 0.52.0", 977 | ] 978 | 979 | [[package]] 980 | name = "rustc-demangle" 981 | version = "0.1.25" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" 984 | 985 | [[package]] 986 | name = "rustix" 987 | version = "1.0.8" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 990 | dependencies = [ 991 | "bitflags 2.9.1", 992 | "errno", 993 | "libc", 994 | "linux-raw-sys", 995 | "windows-sys 0.60.2", 996 | ] 997 | 998 | [[package]] 999 | name = "rustls" 1000 | version = "0.23.29" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" 1003 | dependencies = [ 1004 | "once_cell", 1005 | "rustls-pki-types", 1006 | "rustls-webpki", 1007 | "subtle", 1008 | "zeroize", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "rustls-pki-types" 1013 | version = "1.12.0" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 1016 | dependencies = [ 1017 | "zeroize", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "rustls-webpki" 1022 | version = "0.103.4" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" 1025 | dependencies = [ 1026 | "ring", 1027 | "rustls-pki-types", 1028 | "untrusted", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "rustversion" 1033 | version = "1.0.21" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 1036 | 1037 | [[package]] 1038 | name = "ryu" 1039 | version = "1.0.20" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1042 | 1043 | [[package]] 1044 | name = "schannel" 1045 | version = "0.1.27" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1048 | dependencies = [ 1049 | "windows-sys 0.59.0", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "security-framework" 1054 | version = "2.11.1" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1057 | dependencies = [ 1058 | "bitflags 2.9.1", 1059 | "core-foundation", 1060 | "core-foundation-sys", 1061 | "libc", 1062 | "security-framework-sys", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "security-framework-sys" 1067 | version = "2.14.0" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 1070 | dependencies = [ 1071 | "core-foundation-sys", 1072 | "libc", 1073 | ] 1074 | 1075 | [[package]] 1076 | name = "serde" 1077 | version = "1.0.219" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1080 | dependencies = [ 1081 | "serde_derive", 1082 | ] 1083 | 1084 | [[package]] 1085 | name = "serde_derive" 1086 | version = "1.0.219" 1087 | source = "registry+https://github.com/rust-lang/crates.io-index" 1088 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1089 | dependencies = [ 1090 | "proc-macro2", 1091 | "quote", 1092 | "syn", 1093 | ] 1094 | 1095 | [[package]] 1096 | name = "serde_json" 1097 | version = "1.0.140" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1100 | dependencies = [ 1101 | "itoa", 1102 | "memchr", 1103 | "ryu", 1104 | "serde", 1105 | ] 1106 | 1107 | [[package]] 1108 | name = "serde_urlencoded" 1109 | version = "0.7.1" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1112 | dependencies = [ 1113 | "form_urlencoded", 1114 | "itoa", 1115 | "ryu", 1116 | "serde", 1117 | ] 1118 | 1119 | [[package]] 1120 | name = "shlex" 1121 | version = "1.3.0" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1124 | 1125 | [[package]] 1126 | name = "simd-adler32" 1127 | version = "0.3.7" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 1130 | 1131 | [[package]] 1132 | name = "slab" 1133 | version = "0.4.10" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" 1136 | 1137 | [[package]] 1138 | name = "smallvec" 1139 | version = "1.15.1" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1142 | 1143 | [[package]] 1144 | name = "socket2" 1145 | version = "0.5.10" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 1148 | dependencies = [ 1149 | "libc", 1150 | "windows-sys 0.52.0", 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "stable_deref_trait" 1155 | version = "1.2.0" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1158 | 1159 | [[package]] 1160 | name = "subtle" 1161 | version = "2.6.1" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1164 | 1165 | [[package]] 1166 | name = "syn" 1167 | version = "2.0.104" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 1170 | dependencies = [ 1171 | "proc-macro2", 1172 | "quote", 1173 | "unicode-ident", 1174 | ] 1175 | 1176 | [[package]] 1177 | name = "sync_wrapper" 1178 | version = "1.0.2" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1181 | dependencies = [ 1182 | "futures-core", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "synstructure" 1187 | version = "0.13.2" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1190 | dependencies = [ 1191 | "proc-macro2", 1192 | "quote", 1193 | "syn", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "system-configuration" 1198 | version = "0.6.1" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 1201 | dependencies = [ 1202 | "bitflags 2.9.1", 1203 | "core-foundation", 1204 | "system-configuration-sys", 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "system-configuration-sys" 1209 | version = "0.6.0" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 1212 | dependencies = [ 1213 | "core-foundation-sys", 1214 | "libc", 1215 | ] 1216 | 1217 | [[package]] 1218 | name = "tempfile" 1219 | version = "3.20.0" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 1222 | dependencies = [ 1223 | "fastrand", 1224 | "getrandom 0.3.3", 1225 | "once_cell", 1226 | "rustix", 1227 | "windows-sys 0.59.0", 1228 | ] 1229 | 1230 | [[package]] 1231 | name = "thiserror" 1232 | version = "1.0.69" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1235 | dependencies = [ 1236 | "thiserror-impl 1.0.69", 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "thiserror" 1241 | version = "2.0.12" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1244 | dependencies = [ 1245 | "thiserror-impl 2.0.12", 1246 | ] 1247 | 1248 | [[package]] 1249 | name = "thiserror-impl" 1250 | version = "1.0.69" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1253 | dependencies = [ 1254 | "proc-macro2", 1255 | "quote", 1256 | "syn", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "thiserror-impl" 1261 | version = "2.0.12" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1264 | dependencies = [ 1265 | "proc-macro2", 1266 | "quote", 1267 | "syn", 1268 | ] 1269 | 1270 | [[package]] 1271 | name = "tiff" 1272 | version = "0.9.1" 1273 | source = "registry+https://github.com/rust-lang/crates.io-index" 1274 | checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" 1275 | dependencies = [ 1276 | "flate2", 1277 | "jpeg-decoder", 1278 | "weezl", 1279 | ] 1280 | 1281 | [[package]] 1282 | name = "tinystr" 1283 | version = "0.8.1" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 1286 | dependencies = [ 1287 | "displaydoc", 1288 | "zerovec", 1289 | ] 1290 | 1291 | [[package]] 1292 | name = "tokio" 1293 | version = "1.46.1" 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" 1295 | checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" 1296 | dependencies = [ 1297 | "backtrace", 1298 | "bytes", 1299 | "io-uring", 1300 | "libc", 1301 | "mio", 1302 | "pin-project-lite", 1303 | "slab", 1304 | "socket2", 1305 | "tokio-macros", 1306 | "windows-sys 0.52.0", 1307 | ] 1308 | 1309 | [[package]] 1310 | name = "tokio-macros" 1311 | version = "2.5.0" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1314 | dependencies = [ 1315 | "proc-macro2", 1316 | "quote", 1317 | "syn", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "tokio-native-tls" 1322 | version = "0.3.1" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1325 | dependencies = [ 1326 | "native-tls", 1327 | "tokio", 1328 | ] 1329 | 1330 | [[package]] 1331 | name = "tokio-rustls" 1332 | version = "0.26.2" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 1335 | dependencies = [ 1336 | "rustls", 1337 | "tokio", 1338 | ] 1339 | 1340 | [[package]] 1341 | name = "tokio-stream" 1342 | version = "0.1.17" 1343 | source = "registry+https://github.com/rust-lang/crates.io-index" 1344 | checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 1345 | dependencies = [ 1346 | "futures-core", 1347 | "pin-project-lite", 1348 | "tokio", 1349 | ] 1350 | 1351 | [[package]] 1352 | name = "tokio-util" 1353 | version = "0.7.15" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" 1356 | dependencies = [ 1357 | "bytes", 1358 | "futures-core", 1359 | "futures-sink", 1360 | "pin-project-lite", 1361 | "tokio", 1362 | ] 1363 | 1364 | [[package]] 1365 | name = "tower" 1366 | version = "0.5.2" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1369 | dependencies = [ 1370 | "futures-core", 1371 | "futures-util", 1372 | "pin-project-lite", 1373 | "sync_wrapper", 1374 | "tokio", 1375 | "tower-layer", 1376 | "tower-service", 1377 | ] 1378 | 1379 | [[package]] 1380 | name = "tower-http" 1381 | version = "0.6.6" 1382 | source = "registry+https://github.com/rust-lang/crates.io-index" 1383 | checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 1384 | dependencies = [ 1385 | "bitflags 2.9.1", 1386 | "bytes", 1387 | "futures-util", 1388 | "http", 1389 | "http-body", 1390 | "iri-string", 1391 | "pin-project-lite", 1392 | "tower", 1393 | "tower-layer", 1394 | "tower-service", 1395 | ] 1396 | 1397 | [[package]] 1398 | name = "tower-layer" 1399 | version = "0.3.3" 1400 | source = "registry+https://github.com/rust-lang/crates.io-index" 1401 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1402 | 1403 | [[package]] 1404 | name = "tower-service" 1405 | version = "0.3.3" 1406 | source = "registry+https://github.com/rust-lang/crates.io-index" 1407 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1408 | 1409 | [[package]] 1410 | name = "tracing" 1411 | version = "0.1.41" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1414 | dependencies = [ 1415 | "pin-project-lite", 1416 | "tracing-core", 1417 | ] 1418 | 1419 | [[package]] 1420 | name = "tracing-core" 1421 | version = "0.1.34" 1422 | source = "registry+https://github.com/rust-lang/crates.io-index" 1423 | checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 1424 | dependencies = [ 1425 | "once_cell", 1426 | ] 1427 | 1428 | [[package]] 1429 | name = "try-lock" 1430 | version = "0.2.5" 1431 | source = "registry+https://github.com/rust-lang/crates.io-index" 1432 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1433 | 1434 | [[package]] 1435 | name = "typed-builder" 1436 | version = "0.20.1" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7" 1439 | dependencies = [ 1440 | "typed-builder-macro", 1441 | ] 1442 | 1443 | [[package]] 1444 | name = "typed-builder-macro" 1445 | version = "0.20.1" 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" 1447 | checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28" 1448 | dependencies = [ 1449 | "proc-macro2", 1450 | "quote", 1451 | "syn", 1452 | ] 1453 | 1454 | [[package]] 1455 | name = "unicase" 1456 | version = "2.8.1" 1457 | source = "registry+https://github.com/rust-lang/crates.io-index" 1458 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 1459 | 1460 | [[package]] 1461 | name = "unicode-ident" 1462 | version = "1.0.18" 1463 | source = "registry+https://github.com/rust-lang/crates.io-index" 1464 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1465 | 1466 | [[package]] 1467 | name = "untrusted" 1468 | version = "0.9.0" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1471 | 1472 | [[package]] 1473 | name = "url" 1474 | version = "2.5.4" 1475 | source = "registry+https://github.com/rust-lang/crates.io-index" 1476 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1477 | dependencies = [ 1478 | "form_urlencoded", 1479 | "idna", 1480 | "percent-encoding", 1481 | ] 1482 | 1483 | [[package]] 1484 | name = "utf8_iter" 1485 | version = "1.0.4" 1486 | source = "registry+https://github.com/rust-lang/crates.io-index" 1487 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1488 | 1489 | [[package]] 1490 | name = "vcpkg" 1491 | version = "0.2.15" 1492 | source = "registry+https://github.com/rust-lang/crates.io-index" 1493 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1494 | 1495 | [[package]] 1496 | name = "want" 1497 | version = "0.3.1" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1500 | dependencies = [ 1501 | "try-lock", 1502 | ] 1503 | 1504 | [[package]] 1505 | name = "wasi" 1506 | version = "0.11.1+wasi-snapshot-preview1" 1507 | source = "registry+https://github.com/rust-lang/crates.io-index" 1508 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1509 | 1510 | [[package]] 1511 | name = "wasi" 1512 | version = "0.14.2+wasi-0.2.4" 1513 | source = "registry+https://github.com/rust-lang/crates.io-index" 1514 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1515 | dependencies = [ 1516 | "wit-bindgen-rt", 1517 | ] 1518 | 1519 | [[package]] 1520 | name = "wasm-bindgen" 1521 | version = "0.2.100" 1522 | source = "registry+https://github.com/rust-lang/crates.io-index" 1523 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1524 | dependencies = [ 1525 | "cfg-if", 1526 | "once_cell", 1527 | "rustversion", 1528 | "wasm-bindgen-macro", 1529 | ] 1530 | 1531 | [[package]] 1532 | name = "wasm-bindgen-backend" 1533 | version = "0.2.100" 1534 | source = "registry+https://github.com/rust-lang/crates.io-index" 1535 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1536 | dependencies = [ 1537 | "bumpalo", 1538 | "log", 1539 | "proc-macro2", 1540 | "quote", 1541 | "syn", 1542 | "wasm-bindgen-shared", 1543 | ] 1544 | 1545 | [[package]] 1546 | name = "wasm-bindgen-futures" 1547 | version = "0.4.50" 1548 | source = "registry+https://github.com/rust-lang/crates.io-index" 1549 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1550 | dependencies = [ 1551 | "cfg-if", 1552 | "js-sys", 1553 | "once_cell", 1554 | "wasm-bindgen", 1555 | "web-sys", 1556 | ] 1557 | 1558 | [[package]] 1559 | name = "wasm-bindgen-macro" 1560 | version = "0.2.100" 1561 | source = "registry+https://github.com/rust-lang/crates.io-index" 1562 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1563 | dependencies = [ 1564 | "quote", 1565 | "wasm-bindgen-macro-support", 1566 | ] 1567 | 1568 | [[package]] 1569 | name = "wasm-bindgen-macro-support" 1570 | version = "0.2.100" 1571 | source = "registry+https://github.com/rust-lang/crates.io-index" 1572 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1573 | dependencies = [ 1574 | "proc-macro2", 1575 | "quote", 1576 | "syn", 1577 | "wasm-bindgen-backend", 1578 | "wasm-bindgen-shared", 1579 | ] 1580 | 1581 | [[package]] 1582 | name = "wasm-bindgen-shared" 1583 | version = "0.2.100" 1584 | source = "registry+https://github.com/rust-lang/crates.io-index" 1585 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1586 | dependencies = [ 1587 | "unicode-ident", 1588 | ] 1589 | 1590 | [[package]] 1591 | name = "wasm-streams" 1592 | version = "0.4.2" 1593 | source = "registry+https://github.com/rust-lang/crates.io-index" 1594 | checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" 1595 | dependencies = [ 1596 | "futures-util", 1597 | "js-sys", 1598 | "wasm-bindgen", 1599 | "wasm-bindgen-futures", 1600 | "web-sys", 1601 | ] 1602 | 1603 | [[package]] 1604 | name = "web-sys" 1605 | version = "0.3.77" 1606 | source = "registry+https://github.com/rust-lang/crates.io-index" 1607 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1608 | dependencies = [ 1609 | "js-sys", 1610 | "wasm-bindgen", 1611 | ] 1612 | 1613 | [[package]] 1614 | name = "weezl" 1615 | version = "0.1.10" 1616 | source = "registry+https://github.com/rust-lang/crates.io-index" 1617 | checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" 1618 | 1619 | [[package]] 1620 | name = "windows-link" 1621 | version = "0.1.3" 1622 | source = "registry+https://github.com/rust-lang/crates.io-index" 1623 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 1624 | 1625 | [[package]] 1626 | name = "windows-registry" 1627 | version = "0.5.3" 1628 | source = "registry+https://github.com/rust-lang/crates.io-index" 1629 | checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" 1630 | dependencies = [ 1631 | "windows-link", 1632 | "windows-result", 1633 | "windows-strings", 1634 | ] 1635 | 1636 | [[package]] 1637 | name = "windows-result" 1638 | version = "0.3.4" 1639 | source = "registry+https://github.com/rust-lang/crates.io-index" 1640 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 1641 | dependencies = [ 1642 | "windows-link", 1643 | ] 1644 | 1645 | [[package]] 1646 | name = "windows-strings" 1647 | version = "0.4.2" 1648 | source = "registry+https://github.com/rust-lang/crates.io-index" 1649 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 1650 | dependencies = [ 1651 | "windows-link", 1652 | ] 1653 | 1654 | [[package]] 1655 | name = "windows-sys" 1656 | version = "0.52.0" 1657 | source = "registry+https://github.com/rust-lang/crates.io-index" 1658 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1659 | dependencies = [ 1660 | "windows-targets 0.52.6", 1661 | ] 1662 | 1663 | [[package]] 1664 | name = "windows-sys" 1665 | version = "0.59.0" 1666 | source = "registry+https://github.com/rust-lang/crates.io-index" 1667 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1668 | dependencies = [ 1669 | "windows-targets 0.52.6", 1670 | ] 1671 | 1672 | [[package]] 1673 | name = "windows-sys" 1674 | version = "0.60.2" 1675 | source = "registry+https://github.com/rust-lang/crates.io-index" 1676 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1677 | dependencies = [ 1678 | "windows-targets 0.53.2", 1679 | ] 1680 | 1681 | [[package]] 1682 | name = "windows-targets" 1683 | version = "0.52.6" 1684 | source = "registry+https://github.com/rust-lang/crates.io-index" 1685 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1686 | dependencies = [ 1687 | "windows_aarch64_gnullvm 0.52.6", 1688 | "windows_aarch64_msvc 0.52.6", 1689 | "windows_i686_gnu 0.52.6", 1690 | "windows_i686_gnullvm 0.52.6", 1691 | "windows_i686_msvc 0.52.6", 1692 | "windows_x86_64_gnu 0.52.6", 1693 | "windows_x86_64_gnullvm 0.52.6", 1694 | "windows_x86_64_msvc 0.52.6", 1695 | ] 1696 | 1697 | [[package]] 1698 | name = "windows-targets" 1699 | version = "0.53.2" 1700 | source = "registry+https://github.com/rust-lang/crates.io-index" 1701 | checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" 1702 | dependencies = [ 1703 | "windows_aarch64_gnullvm 0.53.0", 1704 | "windows_aarch64_msvc 0.53.0", 1705 | "windows_i686_gnu 0.53.0", 1706 | "windows_i686_gnullvm 0.53.0", 1707 | "windows_i686_msvc 0.53.0", 1708 | "windows_x86_64_gnu 0.53.0", 1709 | "windows_x86_64_gnullvm 0.53.0", 1710 | "windows_x86_64_msvc 0.53.0", 1711 | ] 1712 | 1713 | [[package]] 1714 | name = "windows_aarch64_gnullvm" 1715 | version = "0.52.6" 1716 | source = "registry+https://github.com/rust-lang/crates.io-index" 1717 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1718 | 1719 | [[package]] 1720 | name = "windows_aarch64_gnullvm" 1721 | version = "0.53.0" 1722 | source = "registry+https://github.com/rust-lang/crates.io-index" 1723 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 1724 | 1725 | [[package]] 1726 | name = "windows_aarch64_msvc" 1727 | version = "0.52.6" 1728 | source = "registry+https://github.com/rust-lang/crates.io-index" 1729 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1730 | 1731 | [[package]] 1732 | name = "windows_aarch64_msvc" 1733 | version = "0.53.0" 1734 | source = "registry+https://github.com/rust-lang/crates.io-index" 1735 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 1736 | 1737 | [[package]] 1738 | name = "windows_i686_gnu" 1739 | version = "0.52.6" 1740 | source = "registry+https://github.com/rust-lang/crates.io-index" 1741 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1742 | 1743 | [[package]] 1744 | name = "windows_i686_gnu" 1745 | version = "0.53.0" 1746 | source = "registry+https://github.com/rust-lang/crates.io-index" 1747 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 1748 | 1749 | [[package]] 1750 | name = "windows_i686_gnullvm" 1751 | version = "0.52.6" 1752 | source = "registry+https://github.com/rust-lang/crates.io-index" 1753 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1754 | 1755 | [[package]] 1756 | name = "windows_i686_gnullvm" 1757 | version = "0.53.0" 1758 | source = "registry+https://github.com/rust-lang/crates.io-index" 1759 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 1760 | 1761 | [[package]] 1762 | name = "windows_i686_msvc" 1763 | version = "0.52.6" 1764 | source = "registry+https://github.com/rust-lang/crates.io-index" 1765 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1766 | 1767 | [[package]] 1768 | name = "windows_i686_msvc" 1769 | version = "0.53.0" 1770 | source = "registry+https://github.com/rust-lang/crates.io-index" 1771 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 1772 | 1773 | [[package]] 1774 | name = "windows_x86_64_gnu" 1775 | version = "0.52.6" 1776 | source = "registry+https://github.com/rust-lang/crates.io-index" 1777 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1778 | 1779 | [[package]] 1780 | name = "windows_x86_64_gnu" 1781 | version = "0.53.0" 1782 | source = "registry+https://github.com/rust-lang/crates.io-index" 1783 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 1784 | 1785 | [[package]] 1786 | name = "windows_x86_64_gnullvm" 1787 | version = "0.52.6" 1788 | source = "registry+https://github.com/rust-lang/crates.io-index" 1789 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1790 | 1791 | [[package]] 1792 | name = "windows_x86_64_gnullvm" 1793 | version = "0.53.0" 1794 | source = "registry+https://github.com/rust-lang/crates.io-index" 1795 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 1796 | 1797 | [[package]] 1798 | name = "windows_x86_64_msvc" 1799 | version = "0.52.6" 1800 | source = "registry+https://github.com/rust-lang/crates.io-index" 1801 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1802 | 1803 | [[package]] 1804 | name = "windows_x86_64_msvc" 1805 | version = "0.53.0" 1806 | source = "registry+https://github.com/rust-lang/crates.io-index" 1807 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 1808 | 1809 | [[package]] 1810 | name = "wit-bindgen-rt" 1811 | version = "0.39.0" 1812 | source = "registry+https://github.com/rust-lang/crates.io-index" 1813 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1814 | dependencies = [ 1815 | "bitflags 2.9.1", 1816 | ] 1817 | 1818 | [[package]] 1819 | name = "writeable" 1820 | version = "0.6.1" 1821 | source = "registry+https://github.com/rust-lang/crates.io-index" 1822 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 1823 | 1824 | [[package]] 1825 | name = "xml-rs" 1826 | version = "0.8.27" 1827 | source = "registry+https://github.com/rust-lang/crates.io-index" 1828 | checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" 1829 | 1830 | [[package]] 1831 | name = "yoke" 1832 | version = "0.8.0" 1833 | source = "registry+https://github.com/rust-lang/crates.io-index" 1834 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 1835 | dependencies = [ 1836 | "serde", 1837 | "stable_deref_trait", 1838 | "yoke-derive", 1839 | "zerofrom", 1840 | ] 1841 | 1842 | [[package]] 1843 | name = "yoke-derive" 1844 | version = "0.8.0" 1845 | source = "registry+https://github.com/rust-lang/crates.io-index" 1846 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 1847 | dependencies = [ 1848 | "proc-macro2", 1849 | "quote", 1850 | "syn", 1851 | "synstructure", 1852 | ] 1853 | 1854 | [[package]] 1855 | name = "zerofrom" 1856 | version = "0.1.6" 1857 | source = "registry+https://github.com/rust-lang/crates.io-index" 1858 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1859 | dependencies = [ 1860 | "zerofrom-derive", 1861 | ] 1862 | 1863 | [[package]] 1864 | name = "zerofrom-derive" 1865 | version = "0.1.6" 1866 | source = "registry+https://github.com/rust-lang/crates.io-index" 1867 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1868 | dependencies = [ 1869 | "proc-macro2", 1870 | "quote", 1871 | "syn", 1872 | "synstructure", 1873 | ] 1874 | 1875 | [[package]] 1876 | name = "zeroize" 1877 | version = "1.8.1" 1878 | source = "registry+https://github.com/rust-lang/crates.io-index" 1879 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1880 | 1881 | [[package]] 1882 | name = "zerotrie" 1883 | version = "0.2.2" 1884 | source = "registry+https://github.com/rust-lang/crates.io-index" 1885 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 1886 | dependencies = [ 1887 | "displaydoc", 1888 | "yoke", 1889 | "zerofrom", 1890 | ] 1891 | 1892 | [[package]] 1893 | name = "zerovec" 1894 | version = "0.11.2" 1895 | source = "registry+https://github.com/rust-lang/crates.io-index" 1896 | checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 1897 | dependencies = [ 1898 | "yoke", 1899 | "zerofrom", 1900 | "zerovec-derive", 1901 | ] 1902 | 1903 | [[package]] 1904 | name = "zerovec-derive" 1905 | version = "0.11.1" 1906 | source = "registry+https://github.com/rust-lang/crates.io-index" 1907 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 1908 | dependencies = [ 1909 | "proc-macro2", 1910 | "quote", 1911 | "syn", 1912 | ] 1913 | 1914 | [[package]] 1915 | name = "zip" 1916 | version = "0.6.6" 1917 | source = "registry+https://github.com/rust-lang/crates.io-index" 1918 | checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" 1919 | dependencies = [ 1920 | "byteorder", 1921 | "crc32fast", 1922 | "crossbeam-utils", 1923 | "flate2", 1924 | ] 1925 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deepl" 3 | version = "0.6.6" 4 | edition = "2021" 5 | authors = ["Avimitin "] 6 | description = "A Rust implementation of the DeepL API" 7 | documentation = "https://docs.rs/deepl" 8 | keywords = ["translate", "deepl", "api"] 9 | license = "MIT" 10 | readme = "README.md" 11 | repository = "https://github.com/Avimitin/deepl-rs" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | thiserror = "2.0.12" 17 | reqwest = {version = "0.12.22", features = ["multipart", "json", "stream"]} 18 | serde = { version = "1.0.219", features = ["derive"] } 19 | serde_json = "1.0.140" 20 | tokio = { version = "1.46.1", features = ["rt", "macros", "fs", "rt-multi-thread", "io-util"] } 21 | tokio-stream = "0.1.17" 22 | paste = "1.0.15" 23 | typed-builder = "0.20" 24 | 25 | [dev-dependencies] 26 | docx-rs = "0.4.17" 27 | 28 | [lib] 29 | doctest = false 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Avimitin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeepL Api 2 | 3 | [github](https://github.com/Avimitin/deepl-rs) 4 | [crates.io](https://crates.io/crates/deepl) 5 | [docs.rs](https://docs.rs/deepl/latest/deepl/) 6 | 7 | Typed HTTP wrapper for interacting with DeepL API. File upload/download is also implemented. 8 | 9 | ## Usage 10 | 11 | ```toml 12 | [dependencies] 13 | deepl = "0.6" 14 | ``` 15 | 16 | ```rust 17 | use deepl::{DeepLApi, Lang}; 18 | 19 | let api = DeepLApi::with("YOUR AUTH KEY").new(); 20 | let translated = api.translate_text("Hello World", Lang::ZH).await.unwrap(); 21 | 22 | let sentences = translated.translations; 23 | assert_eq!(sentences[0].text, "你好,世界"); 24 | ``` 25 | 26 | Read [examples](./examples) for more usage. 27 | 28 | ## Collaboration 29 | 30 | If you find any bugs in this project or feel confused about any part of the code, 31 | feel free to open new issue. 32 | 33 | If you want to submit some code modification but don't know how to setup the 34 | code environment, you can follow the 35 | [Nix Installation](https://nixos.org/manual/nix/stable/installation/installing-binary.html#installing-a-binary-distribution) 36 | and [enable flakes support](https://nixos.wiki/wiki/Flakes#Enable_flakes). 37 | Then simply run `nix develop` in the project root, all the build dependencies will setup 38 | for you. 39 | 40 | ## License 41 | 42 | [MIT](./LICENSE) 43 | -------------------------------------------------------------------------------- /examples/basic_translation.rs: -------------------------------------------------------------------------------- 1 | use deepl::TagHandling; 2 | use deepl::{DeepLApi, Lang}; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | let key = std::env::var("DEEPL_API_KEY").unwrap(); 7 | let deepl = DeepLApi::with(&key).new(); 8 | 9 | let translated = deepl.translate_text("Hello World", Lang::DE).await.unwrap(); 10 | println!("Translated text: "); 11 | println!("{translated}"); 12 | 13 | let api = DeepLApi::with("YOUR AUTH KEY").new(); 14 | let str = "Hello World This will stay exactly the way it was"; 15 | let response = api 16 | .translate_text(str, Lang::DE) 17 | .source_lang(Lang::EN) 18 | .ignore_tags(vec!["keep".to_owned()]) 19 | .tag_handling(TagHandling::Xml) 20 | .await 21 | .unwrap(); 22 | 23 | let translated_results = response.translations; 24 | let should = "Hallo Welt This will stay exactly the way it was"; 25 | assert_eq!(translated_results[0].text, should); 26 | } 27 | -------------------------------------------------------------------------------- /examples/upload_document.rs: -------------------------------------------------------------------------------- 1 | use deepl::{DeepLApi, Lang}; 2 | use std::path::PathBuf; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | let key = std::env::var("DEEPL_API_KEY").unwrap(); 7 | let api = DeepLApi::with(&key).new(); 8 | 9 | let raw_text = "Doubt thou the stars are fire. \ 10 | Doubt that the sun doth move. \ 11 | Doubt truth to be a liar. \ 12 | But never doubt my love."; 13 | 14 | tokio::fs::write("./test.txt", &raw_text).await.unwrap(); 15 | 16 | let test_file = PathBuf::from("./test.txt"); 17 | let response = api.upload_document(&test_file, Lang::ZH).await.unwrap(); 18 | let mut status = api.check_document_status(&response).await.unwrap(); 19 | 20 | // wait for translation 21 | loop { 22 | if status.status.is_done() { 23 | break; 24 | } 25 | tokio::time::sleep(std::time::Duration::from_secs(3)).await; 26 | status = api.check_document_status(&response).await.unwrap(); 27 | } 28 | 29 | let path = api 30 | .download_document(&response, "test_translated.txt") 31 | .await 32 | .unwrap(); 33 | 34 | let content = tokio::fs::read_to_string(path).await.unwrap(); 35 | let expect = "怀疑你的星星是火。怀疑太阳在动。怀疑真理是个骗子。但永远不要怀疑我的爱。"; 36 | assert_eq!(content, expect); 37 | } 38 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1705309234, 9 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "flake-utils_2": { 22 | "inputs": { 23 | "systems": "systems_2" 24 | }, 25 | "locked": { 26 | "lastModified": 1705309234, 27 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 28 | "owner": "numtide", 29 | "repo": "flake-utils", 30 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "numtide", 35 | "repo": "flake-utils", 36 | "type": "github" 37 | } 38 | }, 39 | "nixpkgs": { 40 | "locked": { 41 | "lastModified": 1707689078, 42 | "narHash": "sha256-UUGmRa84ZJHpGZ1WZEBEUOzaPOWG8LZ0yPg1pdDF/yM=", 43 | "owner": "NixOS", 44 | "repo": "nixpkgs", 45 | "rev": "f9d39fb9aff0efee4a3d5f4a6d7c17701d38a1d8", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "owner": "NixOS", 50 | "ref": "nixos-unstable", 51 | "repo": "nixpkgs", 52 | "type": "github" 53 | } 54 | }, 55 | "nixpkgs_2": { 56 | "locked": { 57 | "lastModified": 1706487304, 58 | "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=", 59 | "owner": "NixOS", 60 | "repo": "nixpkgs", 61 | "rev": "90f456026d284c22b3e3497be980b2e47d0b28ac", 62 | "type": "github" 63 | }, 64 | "original": { 65 | "owner": "NixOS", 66 | "ref": "nixpkgs-unstable", 67 | "repo": "nixpkgs", 68 | "type": "github" 69 | } 70 | }, 71 | "root": { 72 | "inputs": { 73 | "flake-utils": "flake-utils", 74 | "nixpkgs": "nixpkgs", 75 | "rust-overlay": "rust-overlay" 76 | } 77 | }, 78 | "rust-overlay": { 79 | "inputs": { 80 | "flake-utils": "flake-utils_2", 81 | "nixpkgs": "nixpkgs_2" 82 | }, 83 | "locked": { 84 | "lastModified": 1707790272, 85 | "narHash": "sha256-KQXPNl3BLdRbz7xx+mwIq/017fxLRk6JhXHxVWCKsTU=", 86 | "owner": "oxalica", 87 | "repo": "rust-overlay", 88 | "rev": "8dfbe2dffc28c1a18a29ffa34d5d0b269622b158", 89 | "type": "github" 90 | }, 91 | "original": { 92 | "owner": "oxalica", 93 | "repo": "rust-overlay", 94 | "type": "github" 95 | } 96 | }, 97 | "systems": { 98 | "locked": { 99 | "lastModified": 1681028828, 100 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 101 | "owner": "nix-systems", 102 | "repo": "default", 103 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 104 | "type": "github" 105 | }, 106 | "original": { 107 | "owner": "nix-systems", 108 | "repo": "default", 109 | "type": "github" 110 | } 111 | }, 112 | "systems_2": { 113 | "locked": { 114 | "lastModified": 1681028828, 115 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 116 | "owner": "nix-systems", 117 | "repo": "default", 118 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 119 | "type": "github" 120 | }, 121 | "original": { 122 | "owner": "nix-systems", 123 | "repo": "default", 124 | "type": "github" 125 | } 126 | } 127 | }, 128 | "root": "root", 129 | "version": 7 130 | } 131 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Flakes for Rust development"; 3 | 4 | inputs = { 5 | # The nixpkgs 6 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 7 | 8 | # Utility functions 9 | flake-utils.url = "github:numtide/flake-utils"; 10 | 11 | # An nixpkgs overlay for overriding the nixpkgs to get declarative Rust toolchain specification. 12 | rust-overlay.url = "github:oxalica/rust-overlay"; 13 | }; 14 | 15 | outputs = { self, nixpkgs, flake-utils, rust-overlay }: 16 | flake-utils.lib.eachDefaultSystem (system: 17 | let 18 | overlays = [ (import rust-overlay) ]; 19 | pkgs = import nixpkgs { inherit system overlays; }; 20 | 21 | rs-toolchain = pkgs.rust-bin.stable.latest.default.override { 22 | extensions = [ "rust-src" ]; 23 | }; 24 | in 25 | { 26 | devShells.default = pkgs.mkShell { 27 | buildInputs = [ 28 | # Including latest cargo,clippy,cargo-fmt 29 | rs-toolchain 30 | # rust-analyzer comes from nixpkgs toolchain, I want the unwrapped version 31 | pkgs.rust-analyzer-unwrapped 32 | pkgs.cargo-expand 33 | pkgs.openssl 34 | ]; 35 | 36 | # To make rust-analyzer work correctly (The path prefix issue) 37 | RUST_SRC_PATH = "${rs-toolchain}/lib/rustlib/src/rust/library"; 38 | }; 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/endpoint/document.rs: -------------------------------------------------------------------------------- 1 | use super::{Pollable, Result}; 2 | use crate::{impl_requester, Formality, Lang}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{ 5 | future::IntoFuture, 6 | path::{Path, PathBuf}, 7 | }; 8 | use tokio::io::AsyncWriteExt; 9 | use tokio_stream::StreamExt; 10 | 11 | /// Response from api/v2/document 12 | #[derive(Serialize, Deserialize)] 13 | pub struct UploadDocumentResp { 14 | /// A unique ID assigned to the uploaded document and the translation process. 15 | /// Must be used when referring to this particular document in subsequent API requests. 16 | pub document_id: String, 17 | /// A unique key that is used to encrypt the uploaded document as well as the resulting 18 | /// translation on the server side. Must be provided with every subsequent API request 19 | /// regarding this particular document. 20 | pub document_key: String, 21 | } 22 | 23 | /// Response from api/v2/document/$ID 24 | #[derive(Deserialize, Debug)] 25 | pub struct DocumentStatusResp { 26 | /// A unique ID assigned to the uploaded document and the requested translation process. 27 | /// The same ID that was used when requesting the translation status. 28 | pub document_id: String, 29 | /// A short description of the state the document translation process is currently in. 30 | /// See [`DocumentTranslateStatus`] for more. 31 | pub status: DocumentTranslateStatus, 32 | /// Estimated number of seconds until the translation is done. 33 | /// This parameter is only included while status is "translating". 34 | pub seconds_remaining: Option, 35 | /// The number of characters billed to your account. 36 | pub billed_characters: Option, 37 | /// A short description of the error, if available. Note that the content is subject to change. 38 | /// This parameter may be included if an error occurred during translation. 39 | pub error_message: Option, 40 | } 41 | 42 | /// Possible value of the document translate status 43 | #[derive(Debug, Deserialize, PartialEq, Eq)] 44 | #[serde(rename_all = "lowercase")] 45 | pub enum DocumentTranslateStatus { 46 | /// The translation job is waiting in line to be processed 47 | Queued, 48 | /// The translation is currently ongoing 49 | Translating, 50 | /// The translation is done and the translated document is ready for download 51 | Done, 52 | /// An irrecoverable error occurred while translating the document 53 | Error, 54 | } 55 | 56 | impl DocumentTranslateStatus { 57 | pub fn is_done(&self) -> bool { 58 | self == &Self::Done 59 | } 60 | } 61 | 62 | impl_requester! { 63 | UploadDocumentRequester { 64 | @required{ 65 | file_path: PathBuf, 66 | target_lang: Lang, 67 | }; 68 | @optional{ 69 | source_lang: Lang, 70 | filename: String, 71 | formality: Formality, 72 | glossary_id: String, 73 | }; 74 | } -> Result; 75 | } 76 | 77 | impl<'a> UploadDocumentRequester<'a> { 78 | fn to_multipart_form(&self) -> reqwest::multipart::Form { 79 | let Self { 80 | source_lang, 81 | target_lang, 82 | formality, 83 | glossary_id, 84 | .. 85 | } = self; 86 | 87 | let mut form = reqwest::multipart::Form::new(); 88 | 89 | // SET source_lang 90 | if let Some(lang) = source_lang { 91 | form = form.text("source_lang", lang.to_string()); 92 | } 93 | 94 | // SET target_lang 95 | form = form.text("target_lang", target_lang.to_string()); 96 | 97 | // SET formality 98 | if let Some(formal) = formality { 99 | form = form.text("formality", formal.to_string()); 100 | } 101 | 102 | // SET glossary 103 | if let Some(id) = glossary_id { 104 | form = form.text("glossary_id", id.to_string()); 105 | } 106 | 107 | form 108 | } 109 | 110 | fn send(&self) -> Pollable<'a, Result> { 111 | let mut form = self.to_multipart_form(); 112 | let client = self.client.clone(); 113 | let filename = self.filename.clone(); 114 | let file_path = self.file_path.clone(); 115 | 116 | let fut = async move { 117 | // SET file && filename asynchronously 118 | let file = tokio::fs::read(&file_path).await.map_err(|err| { 119 | Error::ReadFileError(file_path.to_str().unwrap().to_string(), err) 120 | })?; 121 | 122 | let mut part = reqwest::multipart::Part::bytes(file); 123 | if let Some(filename) = filename { 124 | part = part.file_name(filename.to_string()); 125 | form = form.text("filename", filename); 126 | } else { 127 | part = part.file_name(file_path.file_name().expect( 128 | "No extension found for this file, and no filename given, cannot make request", 129 | ).to_str().expect("not a valid UTF-8 filepath!").to_string()); 130 | } 131 | 132 | form = form.part("file", part); 133 | 134 | let res = client 135 | .post(client.get_endpoint("document")) 136 | .multipart(form) 137 | .send() 138 | .await 139 | .map_err(|err| Error::RequestFail(format!("fail to upload file: {err}")))?; 140 | 141 | if !res.status().is_success() { 142 | return super::extract_deepl_error(res).await; 143 | } 144 | 145 | let res: UploadDocumentResp = res.json().await.map_err(|err| { 146 | Error::InvalidResponse(format!("fail to decode response body: {err}")) 147 | })?; 148 | Ok(res) 149 | }; 150 | 151 | Box::pin(fut) 152 | } 153 | } 154 | 155 | impl<'a> IntoFuture for UploadDocumentRequester<'a> { 156 | type Output = Result; 157 | type IntoFuture = Pollable<'a, Self::Output>; 158 | 159 | fn into_future(self) -> Self::IntoFuture { 160 | self.send() 161 | } 162 | } 163 | 164 | impl<'a> IntoFuture for &mut UploadDocumentRequester<'a> { 165 | type Output = Result; 166 | type IntoFuture = Pollable<'a, Self::Output>; 167 | 168 | fn into_future(self) -> Self::IntoFuture { 169 | self.send() 170 | } 171 | } 172 | 173 | impl DeepLApi { 174 | /// Upload document to DeepL API server, return [`UploadDocumentResp`] for 175 | /// querying the translation status and to download the translated document once 176 | /// translation is complete. 177 | /// 178 | /// # Example 179 | /// 180 | /// ```rust 181 | /// use deepl::DeepLApi; 182 | /// 183 | /// let key = std::env::var("DEEPL_API_KEY").unwrap(); 184 | /// let deepl = DeepLApi::with(&key).new(); 185 | /// 186 | /// // Upload the file to DeepL 187 | /// let filepath = std::path::PathBuf::from("./hamlet.txt"); 188 | /// let response = deepl.upload_document(&filepath, Lang::ZH) 189 | /// .source_lang(Lang::EN) 190 | /// .filename("Hamlet.txt".to_string()) 191 | /// .formality(Formality::Default) 192 | /// .glossary_id("def3a26b-3e84-45b3-84ae-0c0aaf3525f7".to_string()) 193 | /// .await 194 | /// .unwrap(); 195 | /// ``` 196 | /// 197 | /// Read the example `upload_document` in repository for detailed usage 198 | pub fn upload_document( 199 | &self, 200 | fp: impl Into, 201 | target_lang: Lang, 202 | ) -> UploadDocumentRequester { 203 | UploadDocumentRequester::new(self, fp.into(), target_lang) 204 | } 205 | 206 | async fn open_file_to_write(p: &Path) -> Result { 207 | let open_result = tokio::fs::OpenOptions::new() 208 | .append(true) 209 | .create_new(true) 210 | .open(p) 211 | .await; 212 | 213 | if let Ok(file) = open_result { 214 | return Ok(file); 215 | } 216 | 217 | let err = open_result.unwrap_err(); 218 | if err.kind() != std::io::ErrorKind::AlreadyExists { 219 | return Err(Error::WriteFileError(format!( 220 | "Fail to open file {p:?}: {err}" 221 | ))); 222 | } 223 | 224 | tokio::fs::remove_file(p).await.map_err(|err| { 225 | Error::WriteFileError(format!( 226 | "There was already a file there and it is not deletable: {err}" 227 | )) 228 | })?; 229 | dbg!("Detect exist, removed"); 230 | 231 | let open_result = tokio::fs::OpenOptions::new() 232 | .append(true) 233 | .create_new(true) 234 | .open(p) 235 | .await; 236 | 237 | if let Err(err) = open_result { 238 | return Err(Error::WriteFileError(format!( 239 | "Fail to open file for download document, even after retry: {err}" 240 | ))); 241 | } 242 | 243 | Ok(open_result.unwrap()) 244 | } 245 | 246 | /// Check the status of document, returning [`DocumentStatusResp`] if success. 247 | pub async fn check_document_status( 248 | &self, 249 | ident: &UploadDocumentResp, 250 | ) -> Result { 251 | let form = [("document_key", ident.document_key.as_str())]; 252 | let url = self.get_endpoint(&format!("document/{}", ident.document_id)); 253 | let res = self 254 | .post(url) 255 | .form(&form) 256 | .send() 257 | .await 258 | .map_err(|err| Error::RequestFail(err.to_string()))?; 259 | 260 | if !res.status().is_success() { 261 | return super::extract_deepl_error(res).await; 262 | } 263 | 264 | let status: DocumentStatusResp = res 265 | .json() 266 | .await 267 | .map_err(|err| Error::InvalidResponse(format!("response is not JSON: {err}")))?; 268 | 269 | Ok(status) 270 | } 271 | 272 | /// Download the possibly translated document. Downloaded document will store to the given 273 | /// `output` path. 274 | /// 275 | /// Return downloaded file's path if success 276 | pub async fn download_document>( 277 | &self, 278 | ident: &UploadDocumentResp, 279 | output: O, 280 | ) -> Result { 281 | let url = self.get_endpoint(&format!("document/{}/result", ident.document_id)); 282 | let form = [("document_key", ident.document_key.as_str())]; 283 | let res = self 284 | .post(url) 285 | .form(&form) 286 | .send() 287 | .await 288 | .map_err(|err| Error::RequestFail(err.to_string()))?; 289 | 290 | if res.status() == reqwest::StatusCode::NOT_FOUND { 291 | return Err(Error::NonExistDocument); 292 | } 293 | 294 | if res.status() == reqwest::StatusCode::SERVICE_UNAVAILABLE { 295 | return Err(Error::TranslationNotDone); 296 | } 297 | 298 | if !res.status().is_success() { 299 | return super::extract_deepl_error(res).await; 300 | } 301 | 302 | let mut file = Self::open_file_to_write(output.as_ref()).await?; 303 | 304 | let mut stream = res.bytes_stream(); 305 | 306 | #[inline] 307 | fn mapper(s: &'static str) -> Box Error> { 308 | Box::new(move |err: E| Error::WriteFileError(format!("{s}: {err}"))) 309 | } 310 | 311 | while let Some(chunk) = stream.next().await { 312 | let chunk = chunk.map_err(mapper("fail to download part of the document"))?; 313 | file.write_all(&chunk) 314 | .await 315 | .map_err(mapper("fail to write downloaded part into file"))?; 316 | file.sync_all() 317 | .await 318 | .map_err(mapper("fail to sync file content"))?; 319 | } 320 | 321 | Ok(output.as_ref().to_path_buf()) 322 | } 323 | } 324 | 325 | #[tokio::test] 326 | async fn test_upload_document() { 327 | let key = std::env::var("DEEPL_API_KEY").unwrap(); 328 | let api = DeepLApi::with(&key).new(); 329 | 330 | let raw_text = "Hello World"; 331 | 332 | tokio::fs::write("./test.txt", &raw_text).await.unwrap(); 333 | 334 | let test_file = PathBuf::from("./test.txt"); 335 | let response = api.upload_document(&test_file, Lang::DE).await.unwrap(); 336 | let mut status = api.check_document_status(&response).await.unwrap(); 337 | 338 | // wait for translation 339 | loop { 340 | if status.status.is_done() { 341 | break; 342 | } 343 | if let Some(msg) = status.error_message { 344 | println!("{}", msg); 345 | break; 346 | } 347 | tokio::time::sleep(std::time::Duration::from_secs(3)).await; 348 | status = api.check_document_status(&response).await.unwrap(); 349 | dbg!(&status); 350 | } 351 | 352 | let path = api 353 | .download_document(&response, "test_translated.txt") 354 | .await 355 | .unwrap(); 356 | 357 | let content = tokio::fs::read_to_string(path).await.unwrap(); 358 | let expect = "Hallo Welt"; 359 | assert_eq!(content, expect); 360 | } 361 | 362 | #[tokio::test] 363 | async fn test_upload_docx() { 364 | use docx_rs::{read_docx, DocumentChild, Docx, Paragraph, ParagraphChild, Run, RunChild}; 365 | 366 | let key = std::env::var("DEEPL_API_KEY").unwrap(); 367 | let api = DeepLApi::with(&key).new(); 368 | 369 | let test_file = PathBuf::from("./example.docx"); 370 | let file = std::fs::File::create(&test_file).expect("fail to create test asserts"); 371 | Docx::new() 372 | .add_paragraph( 373 | Paragraph::new() 374 | .add_run(Run::new().add_text("To be, or not to be, that is the question")), 375 | ) 376 | .build() 377 | .pack(file) 378 | .expect("fail to write test asserts"); 379 | 380 | let response = api.upload_document(&test_file, Lang::DE).await.unwrap(); 381 | let mut status = api.check_document_status(&response).await.unwrap(); 382 | 383 | // wait for translation 384 | loop { 385 | if status.status.is_done() { 386 | break; 387 | } 388 | if let Some(msg) = status.error_message { 389 | println!("{}", msg); 390 | break; 391 | } 392 | tokio::time::sleep(std::time::Duration::from_secs(3)).await; 393 | status = api.check_document_status(&response).await.unwrap(); 394 | dbg!(&status); 395 | } 396 | 397 | let path = api 398 | .download_document(&response, "translated.docx") 399 | .await 400 | .unwrap(); 401 | let get = tokio::fs::read(&path).await.unwrap(); 402 | let doc = read_docx(&get).expect("can not open downloaded document"); 403 | // collect all the text in this docx file 404 | let text = doc 405 | .document 406 | .children 407 | .iter() 408 | .filter_map(|child| { 409 | if let DocumentChild::Paragraph(paragraph) = child { 410 | let text = paragraph 411 | .children 412 | .iter() 413 | .filter_map(|pchild| { 414 | if let ParagraphChild::Run(run) = pchild { 415 | let text = run 416 | .children 417 | .iter() 418 | .filter_map(|rchild| { 419 | if let RunChild::Text(text) = rchild { 420 | Some(text.text.to_string()) 421 | } else { 422 | None 423 | } 424 | }) 425 | .collect::(); 426 | 427 | Some(text) 428 | } else { 429 | None 430 | } 431 | }) 432 | .collect::(); 433 | Some(text) 434 | } else { 435 | None 436 | } 437 | }) 438 | .collect::(); 439 | 440 | assert_eq!(text, "Sein oder nicht sein, das ist hier die Frage"); 441 | } 442 | -------------------------------------------------------------------------------- /src/endpoint/glossary.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | endpoint::{Error, Result, REPO_URL}, 3 | DeepLApi, Lang, 4 | }; 5 | use core::future::IntoFuture; 6 | use std::borrow::Borrow; 7 | use std::collections::HashMap; 8 | use typed_builder::TypedBuilder; 9 | 10 | use super::Pollable; 11 | 12 | #[derive(Debug, TypedBuilder)] 13 | #[builder(build_method(name = send))] 14 | pub struct CreateGlossary<'a> { 15 | client: &'a DeepLApi, 16 | 17 | name: String, 18 | 19 | source_lang: Lang, 20 | target_lang: Lang, 21 | 22 | #[builder(setter(prefix = "__"))] 23 | entries: Vec<(String, String)>, 24 | 25 | #[builder(default = EntriesFormat::TSV)] 26 | format: EntriesFormat, 27 | } 28 | 29 | #[allow(non_camel_case_types)] 30 | impl<'a, _c, _n, _s, _t, _f> CreateGlossaryBuilder<'a, (_c, _n, _s, _t, (), _f)> { 31 | /// The entries of the glossary. 32 | /// 33 | /// Entries parameter should be able to yield type (String, String). 34 | /// 35 | /// # Example 36 | /// ```rust 37 | /// let my_entries = vec![("Hello", "Guten Tag"), ("Bye", "Auf Wiedersehen")]; 38 | /// // Pass in a HashMap is also okay. 39 | /// // let my_entries = HashMap::from([("Hello", "Guten Tag"), ("Bye", "Auf Wiedersehen")]); 40 | /// let resp = deepl 41 | /// .create_glossary("My Glossary") 42 | /// .source_lang(Lang::EN) 43 | /// .target_lang(Lang::DE) 44 | /// .entries(&my_entries) 45 | /// .format(EntriesFormat::CSV) // This field is optional, we will use TSV as default. 46 | /// .send() 47 | /// .await 48 | /// .unwrap(); 49 | /// assert_eq!(resp.name, "My Glossary"); 50 | /// ``` 51 | pub fn entries( 52 | self, 53 | iter: I, 54 | ) -> CreateGlossaryBuilder<'a, (_c, _n, _s, _t, (Vec<(String, String)>,), _f)> 55 | where 56 | S: ToString, 57 | T: ToString, 58 | B: Borrow<(S, T)>, 59 | I: IntoIterator, 60 | { 61 | let entries = iter 62 | .into_iter() 63 | .map(|t| (t.borrow().0.to_string(), t.borrow().1.to_string())) 64 | .collect(); 65 | let (client, name, source_lang, target_lang, (), format) = self.fields; 66 | CreateGlossaryBuilder { 67 | fields: (client, name, source_lang, target_lang, (entries,), format), 68 | phantom: self.phantom, 69 | } 70 | } 71 | } 72 | 73 | type CreateGlossaryBuilderStart<'a> = 74 | CreateGlossaryBuilder<'a, ((&'a DeepLApi,), (String,), (), (), (), ())>; 75 | 76 | impl<'a> IntoFuture for CreateGlossary<'a> { 77 | type Output = Result; 78 | type IntoFuture = Pollable<'a, Self::Output>; 79 | 80 | fn into_future(self) -> Self::IntoFuture { 81 | let client = self.client.clone(); 82 | let fields = CreateGlossaryRequestParam::from(self); 83 | let fut = async move { 84 | let resp = client 85 | .post(client.get_endpoint("glossaries")) 86 | .json(&fields) 87 | .send() 88 | .await 89 | .map_err(|err| Error::RequestFail(err.to_string()))? 90 | .json::() 91 | .await 92 | .unwrap_or_else(|_| { 93 | panic!( 94 | "Unmatched response to CreateGlossaryResp, please open issue on {REPO_URL}." 95 | ) 96 | }); 97 | 98 | match resp { 99 | GlossaryPossibleResps::Fail { message } => Err(Error::RequestFail(format!( 100 | "Fail to create request to glossary API: {message}" 101 | ))), 102 | GlossaryPossibleResps::Success { 103 | glossary_id, 104 | name, 105 | ready, 106 | source_lang, 107 | target_lang, 108 | creation_time, 109 | entry_count, 110 | } => Ok(GlossaryResp { 111 | glossary_id, 112 | name, 113 | ready, 114 | source_lang, 115 | target_lang, 116 | creation_time, 117 | entry_count, 118 | }), 119 | } 120 | }; 121 | 122 | Box::pin(fut) 123 | } 124 | } 125 | 126 | #[derive(serde::Deserialize)] 127 | #[serde(untagged)] 128 | enum GlossaryPossibleResps { 129 | Success { 130 | glossary_id: String, 131 | name: String, 132 | ready: bool, 133 | source_lang: Lang, 134 | target_lang: Lang, 135 | creation_time: String, 136 | entry_count: u64, 137 | }, 138 | Fail { 139 | message: String, 140 | }, 141 | } 142 | 143 | #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] 144 | pub struct GlossaryResp { 145 | /// A unique ID assigned to a glossary. 146 | pub glossary_id: String, 147 | /// Name associated with the glossary. 148 | pub name: String, 149 | /// Indicates if the newly created glossary can already be used in translate requests. 150 | /// If the created glossary is not yet ready, you have to wait and check the ready status 151 | /// of the glossary before using it in a translate request. 152 | pub ready: bool, 153 | /// The language in which the source texts in the glossary are specified. 154 | pub source_lang: Lang, 155 | /// The language in which the target texts in the glossary are specified. 156 | pub target_lang: Lang, 157 | /// The creation time of the glossary in the ISO 8601-1:2019 format (e.g.: 2021-08-03T14:16:18.329Z). 158 | pub creation_time: String, 159 | /// The number of entries in the glossary. 160 | pub entry_count: u64, 161 | } 162 | 163 | #[derive(Debug, serde::Serialize, serde::Deserialize)] 164 | struct CreateGlossaryRequestParam { 165 | name: String, 166 | source_lang: String, 167 | target_lang: String, 168 | entries: String, 169 | entries_format: String, 170 | } 171 | 172 | impl<'a> From> for CreateGlossaryRequestParam { 173 | fn from(value: CreateGlossary<'a>) -> Self { 174 | CreateGlossaryRequestParam { 175 | name: value.name, 176 | source_lang: value.source_lang.to_string().to_lowercase(), 177 | target_lang: value.target_lang.to_string().to_lowercase(), 178 | entries: match value.format { 179 | EntriesFormat::TSV => value 180 | .entries 181 | .iter() 182 | .map(|(x, y)| format!("{x}\t{y}")) 183 | .collect::>() 184 | .join("\n"), 185 | EntriesFormat::CSV => value 186 | .entries 187 | .iter() 188 | .map(|(x, y)| format!("{x},{y}")) 189 | .collect::>() 190 | .join("\n"), 191 | }, 192 | entries_format: value.format.to_string(), 193 | } 194 | } 195 | } 196 | 197 | #[derive(Debug)] 198 | pub enum EntriesFormat { 199 | TSV, 200 | CSV, 201 | } 202 | 203 | impl ToString for EntriesFormat { 204 | fn to_string(&self) -> String { 205 | match self { 206 | EntriesFormat::TSV => "tsv".to_string(), 207 | EntriesFormat::CSV => "csv".to_string(), 208 | } 209 | } 210 | } 211 | 212 | #[derive(Debug, serde::Deserialize, serde::Serialize)] 213 | pub struct GlossaryLanguagePair { 214 | pub source_lang: Lang, 215 | pub target_lang: Lang, 216 | } 217 | 218 | impl DeepLApi { 219 | /// API for endpoint: https://www.deepl.com/de/docs-api/glossaries/create-glossary. 220 | /// The function for creating a glossary returns a JSON object containing the 221 | /// ID of the newly created glossary and a boolean flag that indicates if the 222 | /// created glossary can already be used in translate requests. 223 | /// 224 | /// # Example 225 | /// 226 | /// ```rust 227 | /// use crate::{glossary::EntriesFormat, DeepLApi, Lang}; 228 | /// 229 | /// let key = std::env::var("DEEPL_API_KEY").unwrap(); 230 | /// let deepl = DeepLApi::with(&key).new(); 231 | /// 232 | /// let _: CreateGlossaryResp = deepl 233 | /// .create_glossary("My Glossary") 234 | /// .source("Hello", Lang::EN) 235 | /// .target("Guten Tag", Lang::DE) 236 | /// .format(EntriesFormat::CSV) // This field is optional, we will use TSV as default. 237 | /// .send() 238 | /// .await 239 | /// .unwrap(); 240 | /// ``` 241 | pub fn create_glossary(&self, name: impl ToString) -> CreateGlossaryBuilderStart { 242 | CreateGlossary::builder() 243 | .client(self) 244 | .name(name.to_string()) 245 | } 246 | 247 | /// List all glossaries and their meta-information, but not the glossary entries. 248 | pub async fn list_all_glossaries(&self) -> Result> { 249 | Ok( 250 | self.get(self.get_endpoint("glossaries")) 251 | .send() 252 | .await 253 | .map_err(|e| Error::RequestFail(e.to_string()))? 254 | .json::>>() 255 | .await 256 | .map_err(|err| Error::RequestFail(format!("Unexpected error when requesting list_all_glossaries, please open issue on {REPO_URL}: {err}")))? 257 | .remove("glossaries") 258 | .ok_or(Error::RequestFail(format!("Unable to find key glossaries in response, please open issue on {REPO_URL}")))? 259 | ) 260 | } 261 | 262 | /// Retrieve meta information for a single glossary, omitting the glossary entries. 263 | /// Require a unique ID assigned to the glossary. 264 | pub async fn retrieve_glossary_details(&self, id: impl ToString) -> Result { 265 | match self 266 | .get(self.get_endpoint(&format!("glossaries/{}", id.to_string()))) 267 | .send() 268 | .await 269 | .map_err(|e| Error::RequestFail(e.to_string()))? 270 | .json::() 271 | .await 272 | .expect("") 273 | { 274 | GlossaryPossibleResps::Fail { message } => Err(Error::RequestFail(format!( 275 | "fail to send request to glossary API: {message}" 276 | ))), 277 | GlossaryPossibleResps::Success { 278 | glossary_id, 279 | name, 280 | ready, 281 | source_lang, 282 | target_lang, 283 | creation_time, 284 | entry_count, 285 | } => Ok(GlossaryResp { 286 | glossary_id, 287 | name, 288 | ready, 289 | source_lang, 290 | target_lang, 291 | creation_time, 292 | entry_count, 293 | }), 294 | } 295 | } 296 | 297 | /// Deletes the specified glossary. 298 | pub async fn delete_glossary(&self, id: impl ToString) -> Result<()> { 299 | self.del(self.get_endpoint(&format!("glossaries/{}", id.to_string()))) 300 | .send() 301 | .await 302 | .map_err(|e| Error::RequestFail(e.to_string())) 303 | .map(|_| ()) 304 | } 305 | 306 | /// List the entries of a single glossary in the format specified by the Accept header. 307 | /// Currently, support TSV(tab separated value) only. 308 | pub async fn retrieve_glossary_entries( 309 | &self, 310 | id: impl ToString, 311 | ) -> Result> { 312 | Ok(self 313 | .get(self.get_endpoint(&format!("glossaries/{}/entries", id.to_string()))) 314 | .header("Accept", "text/tab-separated-values") 315 | .send() 316 | .await 317 | .map_err(|e| Error::RequestFail(e.to_string()))? 318 | .text() 319 | .await 320 | .map(|resp| { 321 | resp.split("\n") 322 | .map(|line| { 323 | let mut pair = line.split("\t"); 324 | ( 325 | pair.next().unwrap().to_string(), 326 | pair.next().unwrap().to_string(), 327 | ) 328 | }) 329 | .collect() 330 | }) 331 | .map_err(|err| { 332 | Error::RequestFail(format!("fail to retrieve glossary entries: {err}")) 333 | }))? 334 | } 335 | 336 | /// Retrieve the list of language pairs supported by the glossary feature. 337 | pub async fn list_glossary_language_pairs(&self) -> Result> { 338 | let pair = self 339 | .get(self.get_endpoint("glossary-language-pairs")) 340 | .send() 341 | .await 342 | .map_err(|e| Error::RequestFail(e.to_string()))? 343 | .json::>>() 344 | .await 345 | .map_err(|err| { 346 | Error::RequestFail(format!("fail to list glossary language pairs: {err}")) 347 | })? 348 | .remove("supported_languages") 349 | .ok_or(Error::RequestFail(format!( 350 | "Fail to get supported languages from glossary language pairs" 351 | )))?; 352 | 353 | Ok(pair) 354 | } 355 | } 356 | 357 | #[tokio::test] 358 | async fn test_glossary_api() { 359 | use crate::{glossary::EntriesFormat, DeepLApi, Lang}; 360 | 361 | let key = std::env::var("DEEPL_API_KEY").unwrap(); 362 | let deepl = DeepLApi::with(&key).new(); 363 | 364 | assert_ne!(deepl.list_glossary_language_pairs().await.unwrap().len(), 0); 365 | 366 | let my_entries = vec![("Hello", "Guten Tag"), ("Bye", "Auf Wiedersehen")]; 367 | // let my_entries = HashMap::from([("Hello", "Guten Tag"), ("Bye", "Auf Wiedersehen")]); 368 | let resp = deepl 369 | .create_glossary("My Glossary") 370 | .source_lang(Lang::EN) 371 | .target_lang(Lang::DE) 372 | .entries(&my_entries) 373 | .format(EntriesFormat::CSV) // This field is optional, we will use TSV as default. 374 | .send() 375 | .await 376 | .unwrap(); 377 | assert_eq!(resp.name, "My Glossary"); 378 | 379 | let all = deepl.list_all_glossaries().await.unwrap(); 380 | assert_ne!(all.len(), 0); 381 | 382 | let detail = deepl 383 | .retrieve_glossary_details(&resp.glossary_id) 384 | .await 385 | .unwrap(); 386 | 387 | assert_eq!(detail, resp); 388 | 389 | let entries = deepl 390 | .retrieve_glossary_entries(&resp.glossary_id) 391 | .await 392 | .unwrap(); 393 | assert_eq!(entries.len(), 2); 394 | let entries: HashMap = HashMap::from_iter(entries); 395 | assert_eq!(entries["Hello"], "Guten Tag"); 396 | assert_eq!(entries["Bye"], "Auf Wiedersehen"); 397 | 398 | deepl.delete_glossary(resp.glossary_id).await.unwrap(); 399 | } 400 | -------------------------------------------------------------------------------- /src/endpoint/languages.rs: -------------------------------------------------------------------------------- 1 | use super::{Error, Result}; 2 | use crate::DeepLApi; 3 | use serde::Deserialize; 4 | 5 | /// Information about a supported language 6 | #[derive(Deserialize)] 7 | pub struct LangInfo { 8 | /// Language code 9 | pub language: String, 10 | /// Language name 11 | pub name: String, 12 | /// Denotes a target language supports formality 13 | pub supports_formality: Option, 14 | } 15 | 16 | /// Language type used to request supported languages 17 | #[derive(Debug)] 18 | pub enum LangType { 19 | /// Source language 20 | Source, 21 | /// Target language 22 | Target, 23 | } 24 | 25 | impl AsRef for LangType { 26 | fn as_ref(&self) -> &str { 27 | match self { 28 | Self::Source => "source", 29 | Self::Target => "target", 30 | } 31 | } 32 | } 33 | 34 | impl DeepLApi { 35 | /// 36 | /// Retrieve supported languages for a given [`LangType`] 37 | /// 38 | /// # Example 39 | /// 40 | /// ```rust 41 | /// let target_langs = deepl.languages(LangType::Target).await.unwrap(); 42 | /// assert!(!target_langs.is_empty()); 43 | /// 44 | /// let lang = target_langs.first().unwrap(); 45 | /// println!("{}", lang.language); // BG 46 | /// println!("{}", lang.name); // Bulgarian 47 | /// ``` 48 | pub async fn languages(&self, lang_type: LangType) -> Result> { 49 | let q = vec![("type", lang_type.as_ref())]; 50 | 51 | let resp = self 52 | .get(self.get_endpoint("languages")) 53 | .query(&q) 54 | .send() 55 | .await 56 | .map_err(|err| Error::RequestFail(err.to_string()))?; 57 | 58 | if !resp.status().is_success() { 59 | return super::extract_deepl_error(resp).await; 60 | } 61 | 62 | resp.json().await.map_err(|err| { 63 | Error::InvalidResponse(format!("convert json bytes to Rust type: {err}")) 64 | }) 65 | } 66 | } 67 | 68 | #[tokio::test] 69 | async fn test_get_languages() { 70 | let deepl = DeepLApi::with(&std::env::var("DEEPL_API_KEY").unwrap()).new(); 71 | 72 | let langs = deepl.languages(LangType::Target).await.unwrap(); 73 | assert!(!langs.is_empty()); 74 | } 75 | 76 | #[tokio::test] 77 | async fn test_generate_langs() { 78 | use crate::Lang; 79 | let deepl = DeepLApi::with(&std::env::var("DEEPL_API_KEY").unwrap()).new(); 80 | 81 | // fetch source langs 82 | let source_langs = deepl.languages(LangType::Source).await.unwrap(); 83 | let codes: Vec<&str> = source_langs.iter().map(|l| l.language.as_str()).collect(); 84 | 85 | // fetch target langs, filtering same lang code 86 | let mut target_langs = deepl.languages(LangType::Target).await.unwrap(); 87 | target_langs.retain(|l| !codes.contains(&l.language.as_str())); 88 | 89 | // iterate `LangInfo`s and try to create a `Lang`. 90 | // prints the missing lang to stdout in case of error 91 | let _: Vec = source_langs 92 | .into_iter() 93 | .chain(target_langs) 94 | .map(|l| { 95 | let code = &l.language; 96 | let name = &l.name; 97 | Lang::try_from(code) 98 | .map_err(|_| println!("Failed to convert lang: {code} {name}")) 99 | .unwrap() 100 | }) 101 | .collect(); 102 | } 103 | -------------------------------------------------------------------------------- /src/endpoint/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::{future::Future, pin::Pin}; 3 | use thiserror::Error; 4 | 5 | pub mod document; 6 | pub mod glossary; 7 | pub mod languages; 8 | pub mod translate; 9 | pub mod usage; 10 | 11 | /// Representing error during interaction with DeepL 12 | #[derive(Debug, Error)] 13 | pub enum Error { 14 | #[error("invalid response: {0}")] 15 | InvalidResponse(String), 16 | 17 | #[error("request fail: {0}")] 18 | RequestFail(String), 19 | 20 | #[error("fail to read file {0}: {1}")] 21 | ReadFileError(String, tokio::io::Error), 22 | 23 | #[error( 24 | "trying to download a document using a non-existing document ID or the wrong document key" 25 | )] 26 | NonExistDocument, 27 | 28 | #[error("tries to download a translated document that is currently being processed and is not yet ready for download")] 29 | TranslationNotDone, 30 | 31 | #[error("fail to write file: {0}")] 32 | WriteFileError(String), 33 | } 34 | 35 | const REPO_URL: &'static str = "https://github.com/Avimitin/deepl-rs"; 36 | 37 | /// Alias Result to Result 38 | type Result = std::result::Result; 39 | 40 | /// Pollable alias to a Pin>>. A convenient type for impl 41 | /// [`IntoFuture`](std::future::IntoFuture) trait 42 | type Pollable<'poll, T> = Pin + Send + Sync + 'poll>>; 43 | 44 | /// A self implemented Type Builder 45 | #[macro_export] 46 | macro_rules! impl_requester { 47 | ( 48 | $name:ident { 49 | @required{ 50 | $($must_field:ident: $must_type:ty,)+ 51 | }; 52 | @optional{ 53 | $($opt_field:ident: $opt_type:ty,)* 54 | }; 55 | } -> $fut_ret:ty; 56 | ) => { 57 | use paste::paste; 58 | use $crate::{DeepLApi, Error}; 59 | 60 | paste! { 61 | #[doc = "Builder type for `" $name "`"] 62 | #[derive(Debug, serde::Serialize)] 63 | pub struct $name<'a> { 64 | #[serde(skip)] 65 | client: &'a DeepLApi, 66 | 67 | $($must_field: $must_type,)+ 68 | $($opt_field: Option<$opt_type>,)* 69 | } 70 | 71 | impl<'a> $name<'a> { 72 | pub fn new(client: &'a DeepLApi, $($must_field: $must_type,)+) -> Self { 73 | Self { 74 | client, 75 | $($must_field,)+ 76 | $($opt_field: None,)* 77 | } 78 | } 79 | 80 | $( 81 | #[doc = "Setter for `" $opt_field "`"] 82 | pub fn $opt_field(&mut self, $opt_field: $opt_type) -> &mut Self { 83 | self.$opt_field = Some($opt_field); 84 | self 85 | } 86 | )* 87 | } 88 | } 89 | }; 90 | } 91 | 92 | /// Formality preference for translation 93 | #[derive(Debug, Serialize)] 94 | #[serde(rename_all = "snake_case")] 95 | pub enum Formality { 96 | Default, 97 | More, 98 | Less, 99 | PreferMore, 100 | PreferLess, 101 | } 102 | 103 | impl AsRef for Formality { 104 | fn as_ref(&self) -> &str { 105 | match self { 106 | Self::Default => "default", 107 | Self::More => "more", 108 | Self::Less => "less", 109 | Self::PreferMore => "prefer_more", 110 | Self::PreferLess => "prefer_less", 111 | } 112 | } 113 | } 114 | 115 | impl std::fmt::Display for Formality { 116 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 117 | write!(f, "{}", self.as_ref()) 118 | } 119 | } 120 | 121 | // detail message of the API error 122 | #[derive(Deserialize)] 123 | struct DeepLErrorResp { 124 | message: String, 125 | } 126 | 127 | /// Turn DeepL API error message into [`Error`] 128 | async fn extract_deepl_error(res: reqwest::Response) -> Result { 129 | let resp = res 130 | .json::() 131 | .await 132 | .map_err(|err| Error::InvalidResponse(format!("invalid error response: {err}")))?; 133 | Err(Error::RequestFail(resp.message)) 134 | } 135 | -------------------------------------------------------------------------------- /src/endpoint/translate.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::{ 4 | endpoint::{Formality, Pollable, Result}, 5 | impl_requester, Lang, 6 | }; 7 | 8 | use serde::{Deserialize, Serialize}; 9 | use serde_json::json; 10 | 11 | /// Response from basic translation API 12 | #[derive(Deserialize)] 13 | pub struct TranslateTextResp { 14 | pub translations: Vec, 15 | } 16 | 17 | impl std::fmt::Display for TranslateTextResp { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | write!( 20 | f, 21 | "{}", 22 | self.translations 23 | .iter() 24 | .map(|sent| sent.text.to_string()) 25 | .collect::() 26 | ) 27 | } 28 | } 29 | 30 | /// Translated result for a sentence 31 | #[derive(Deserialize)] 32 | pub struct Sentence { 33 | pub detected_source_language: Lang, 34 | pub text: String, 35 | } 36 | 37 | /// 38 | /// Sets whether the translation engine should respect the original formatting, 39 | /// even if it would usually correct some aspects. 40 | /// The formatting aspects affected by this setting include: 41 | /// - Punctuation at the beginning and end of the sentence 42 | /// - Upper/lower case at the beginning of the sentence 43 | /// 44 | #[derive(Debug, Serialize)] 45 | pub enum PreserveFormatting { 46 | #[serde(rename = "1")] 47 | Preserve, 48 | #[serde(rename = "0")] 49 | DontPreserve, 50 | } 51 | 52 | /// 53 | /// Sets whether the translation engine should first split the input into sentences 54 | /// 55 | /// For applications that send one sentence per text parameter, it is advisable to set this to `None`, 56 | /// in order to prevent the engine from splitting the sentence unintentionally. 57 | /// Please note that newlines will split sentences. You should therefore clean files to avoid breaking sentences or set this to `PunctuationOnly`. 58 | /// 59 | #[derive(Debug, Serialize)] 60 | pub enum SplitSentences { 61 | /// Perform no splitting at all, whole input is treated as one sentence 62 | #[serde(rename = "0")] 63 | None, 64 | /// Split on punctuation and on newlines (default) 65 | #[serde(rename = "1")] 66 | PunctuationAndNewlines, 67 | /// Split on punctuation only, ignoring newlines 68 | #[serde(rename = "nonewlines")] 69 | PunctuationOnly, 70 | } 71 | 72 | /// 73 | /// Sets which kind of tags should be handled. Options currently available 74 | /// 75 | #[derive(Debug, Serialize)] 76 | #[serde(rename_all = "lowercase")] 77 | pub enum TagHandling { 78 | /// Enable XML tag handling 79 | /// see: 80 | Xml, 81 | /// Enable HTML tag handling 82 | /// see: 83 | Html, 84 | } 85 | 86 | /// Sets the language model to use: allows to choose an improved "next-gen" model 87 | #[derive(Debug, Serialize)] 88 | #[serde(rename_all = "snake_case")] 89 | pub enum ModelType { 90 | /// Use classic lower-latency default model 91 | LatencyOptimized, 92 | /// Use improved higher-latency model 93 | QualityOptimized, 94 | /// Use improved higher-latency model, but fallback to default model if not available for the selected language 95 | PreferQualityOptimized 96 | } 97 | 98 | impl_requester! { 99 | TranslateRequester { 100 | @required{ 101 | text: Vec, 102 | target_lang: Lang, 103 | }; 104 | @optional{ 105 | context: String, 106 | source_lang: Lang, 107 | split_sentences: SplitSentences, 108 | preserve_formatting: PreserveFormatting, 109 | formality: Formality, 110 | glossary_id: String, 111 | tag_handling: TagHandling, 112 | model_type: ModelType, 113 | non_splitting_tags: Vec, 114 | splitting_tags: Vec, 115 | ignore_tags: Vec, 116 | }; 117 | } -> Result; 118 | } 119 | 120 | impl<'a> IntoFuture for TranslateRequester<'a> { 121 | type Output = Result; 122 | type IntoFuture = Pollable<'a, Self::Output>; 123 | 124 | fn into_future(self) -> Self::IntoFuture { 125 | self.send() 126 | } 127 | } 128 | 129 | impl<'a> IntoFuture for &mut TranslateRequester<'a> { 130 | type Output = Result; 131 | type IntoFuture = Pollable<'a, Self::Output>; 132 | 133 | fn into_future(self) -> Self::IntoFuture { 134 | self.send() 135 | } 136 | } 137 | 138 | impl<'a> TranslateRequester<'a> { 139 | fn send(&self) -> Pollable<'a, Result> { 140 | let client = self.client.clone(); 141 | let obj = json!(self); 142 | 143 | let fut = async move { 144 | let response = client 145 | .post(client.inner.endpoint.join("translate").unwrap()) 146 | .json(&obj) 147 | .send() 148 | .await 149 | .map_err(|err| Error::RequestFail(err.to_string()))?; 150 | 151 | if !response.status().is_success() { 152 | return super::extract_deepl_error(response).await; 153 | } 154 | 155 | let response: TranslateTextResp = response.json().await.map_err(|err| { 156 | Error::InvalidResponse(format!("convert json bytes to Rust type: {err}")) 157 | })?; 158 | 159 | Ok(response) 160 | }; 161 | 162 | Box::pin(fut) 163 | } 164 | } 165 | 166 | impl DeepLApi { 167 | /// Translate the given text with specific target language. 168 | /// 169 | /// # Error 170 | /// 171 | /// Return [`Error`] if the http request fail 172 | /// 173 | /// # Example 174 | /// 175 | /// * Simple translation 176 | /// 177 | /// ```rust 178 | /// use deepl::{DeepLApi, Lang}; 179 | /// 180 | /// let key = std::env::var("DEEPL_API_KEY").unwrap(); 181 | /// let deepl = DeepLApi::with(&key).new(); 182 | /// 183 | /// let response = deepl.translate_text("Hello World", Lang::ZH).await.unwrap(); 184 | /// assert!(!response.translations.is_empty()); 185 | /// ``` 186 | /// 187 | /// * Translation with XML tag enabled 188 | /// 189 | /// ```rust 190 | /// use deepl::{DeepLApi, Lang}; 191 | /// 192 | /// let key = std::env::var("DEEPL_API_KEY").unwrap(); 193 | /// let deepl = DeepLApi::with(&key).new(); 194 | /// 195 | /// let str = "Hello World This will stay exactly the way it was"; 196 | /// let response = deepl 197 | /// .translate_text(str, Lang::DE) 198 | /// .source_lang(Lang::EN) 199 | /// .ignore_tags(vec!["keep".to_owned()]) 200 | /// .tag_handling(TagHandling::Xml) 201 | /// .await 202 | /// .unwrap(); 203 | /// 204 | /// let translated_results = response.translations; 205 | /// let should = "Hallo Welt This will stay exactly the way it was"; 206 | /// assert_eq!(translated_results[0].text, should); 207 | /// ``` 208 | pub fn translate_text(&self, text: impl ToString, target_lang: Lang) -> TranslateRequester { 209 | TranslateRequester::new(self, vec![text.to_string()], target_lang) 210 | } 211 | } 212 | 213 | #[tokio::test] 214 | async fn test_translate_text() { 215 | let key = std::env::var("DEEPL_API_KEY").unwrap(); 216 | let api = DeepLApi::with(&key).new(); 217 | let response = api.translate_text("Hello World", Lang::ZH).await.unwrap(); 218 | 219 | assert!(!response.translations.is_empty()); 220 | 221 | let translated_results = response.translations; 222 | assert_eq!(translated_results[0].text, "你好,世界"); 223 | assert_eq!(translated_results[0].detected_source_language, Lang::EN); 224 | } 225 | 226 | #[tokio::test] 227 | async fn test_advanced_translate() { 228 | let key = std::env::var("DEEPL_API_KEY").unwrap(); 229 | let api = DeepLApi::with(&key).new(); 230 | 231 | let response = api.translate_text( 232 | "Hello World This will stay exactly the way it was", 233 | Lang::DE 234 | ) 235 | .source_lang(Lang::EN) 236 | .ignore_tags(vec!["keep".to_string()]) 237 | .tag_handling(TagHandling::Xml) 238 | .await 239 | .unwrap(); 240 | 241 | assert!(!response.translations.is_empty()); 242 | 243 | let translated_results = response.translations; 244 | assert_eq!( 245 | translated_results[0].text, 246 | "Hallo Welt This will stay exactly the way it was" 247 | ); 248 | assert_eq!(translated_results[0].detected_source_language, Lang::EN); 249 | } 250 | 251 | #[tokio::test] 252 | async fn test_models() { 253 | let api = DeepLApi::with(&std::env::var("DEEPL_API_KEY").unwrap()).new(); 254 | 255 | // whatever model is used, the translation should happen, and it can differ slightly 256 | for model_type in [ModelType::LatencyOptimized, ModelType::QualityOptimized, ModelType::QualityOptimized] { 257 | let response = api 258 | .translate_text("No te muevas, pringao", Lang::EN) 259 | .source_lang(Lang::ES) 260 | .model_type(model_type) 261 | .await 262 | .unwrap(); 263 | 264 | assert!(response 265 | .translations.get(0).expect("should be a translation") 266 | .text.contains("Don't move")); // the last word can differ depending on the model chosen 267 | } 268 | } 269 | 270 | #[tokio::test] 271 | async fn test_advanced_translator_html() { 272 | let key = std::env::var("DEEPL_API_KEY").unwrap(); 273 | let api = DeepLApi::with(&key).new(); 274 | 275 | let response = api 276 | .translate_text( 277 | "Hello World This will stay exactly the way it was", 278 | Lang::DE, 279 | ) 280 | .tag_handling(TagHandling::Html) 281 | .await 282 | .unwrap(); 283 | 284 | assert!(!response.translations.is_empty()); 285 | 286 | let translated_results = response.translations; 287 | assert_eq!( 288 | translated_results[0].text, 289 | "Hallo Welt This will stay exactly the way it was" 290 | ); 291 | assert_eq!(translated_results[0].detected_source_language, Lang::EN); 292 | } 293 | 294 | #[tokio::test] 295 | async fn test_formality() { 296 | let api = DeepLApi::with(&std::env::var("DEEPL_API_KEY").unwrap()).new(); 297 | 298 | // can specify a formality 299 | let text = "How are you?"; 300 | let src = Lang::EN; 301 | let trg = Lang::ES; 302 | let more = Formality::More; 303 | 304 | let response = api 305 | .translate_text(text, trg) 306 | .source_lang(src) 307 | .formality(more) 308 | .await 309 | .unwrap(); 310 | assert!(!response.translations.is_empty()); 311 | 312 | // response ok, despite target lang not supporting formality 313 | let text = "¿Cómo estás?"; 314 | let src = Lang::ES; 315 | let trg = Lang::EN_US; 316 | let less = Formality::PreferLess; 317 | 318 | let response = api 319 | .translate_text(text, trg) 320 | .source_lang(src) 321 | .formality(less) 322 | .await 323 | .unwrap(); 324 | assert!(!response.translations.is_empty()); 325 | } 326 | -------------------------------------------------------------------------------- /src/endpoint/usage.rs: -------------------------------------------------------------------------------- 1 | use super::Result; 2 | use crate::{DeepLApi, Error}; 3 | use serde::Deserialize; 4 | 5 | /// Response from the usage API 6 | #[derive(Deserialize)] 7 | pub struct UsageResponse { 8 | pub character_count: u64, 9 | pub character_limit: u64, 10 | } 11 | 12 | impl DeepLApi { 13 | /// Get the current DeepL API usage 14 | /// 15 | /// # Example 16 | /// 17 | /// ```rust 18 | /// use deepl::DeepLApi; 19 | /// 20 | /// let key = std::env::var("DEEPL_API_KEY").unwrap(); 21 | /// let deepl = DeepLApi::with(&key).new(); 22 | /// let response = deepl.get_usage().await.unwrap(); 23 | /// 24 | /// assert_ne!(response.character_count, 0); 25 | /// ``` 26 | pub async fn get_usage(&self) -> Result { 27 | let response = self 28 | .post(self.get_endpoint("usage")) 29 | .send() 30 | .await 31 | .map_err(|err| Error::RequestFail(err.to_string()))?; 32 | 33 | if !response.status().is_success() { 34 | return super::extract_deepl_error(response).await; 35 | } 36 | 37 | let response: UsageResponse = response.json().await.map_err(|err| { 38 | Error::InvalidResponse(format!("convert json bytes to Rust type: {err}")) 39 | })?; 40 | 41 | Ok(response) 42 | } 43 | } 44 | 45 | #[tokio::test] 46 | async fn test_usage() { 47 | let key = std::env::var("DEEPL_API_KEY").unwrap(); 48 | let api = DeepLApi::with(&key).new(); 49 | let response = api.get_usage().await.unwrap(); 50 | 51 | assert_ne!(response.character_limit, 0); 52 | } 53 | -------------------------------------------------------------------------------- /src/lang.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, str::FromStr}; 2 | 3 | use paste::paste; 4 | use serde::{Deserialize, Deserializer, Serialize}; 5 | use thiserror::Error; 6 | 7 | #[derive(Debug, Error)] 8 | pub enum LangConvertError { 9 | #[error("invalid language code {0}")] 10 | InvalidLang(String), 11 | } 12 | 13 | type Result = core::result::Result; 14 | 15 | macro_rules! generate_langs { 16 | ( 17 | $( 18 | ($code:literal, $desc:literal); 19 | )+ 20 | ) => { 21 | paste! { 22 | /// Languages 23 | #[allow(non_camel_case_types)] 24 | #[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize)] 25 | pub enum Lang { 26 | $( 27 | #[doc = $desc] 28 | #[serde(rename = $code)] 29 | [<$code>], 30 | )+ 31 | } 32 | 33 | impl Lang { 34 | /// Return full language name for the code 35 | pub fn description(&self) -> String { 36 | match self { 37 | $( 38 | Self::[<$code>] => $desc.to_string(), 39 | )+ 40 | } 41 | } 42 | } 43 | 44 | impl TryFrom<&str> for Lang { 45 | type Error = LangConvertError; 46 | 47 | /// Convert literal to enum `Lang` 48 | /// 49 | /// # Error 50 | /// 51 | /// Return `Error::InvalidLang` when given language code is not in the support list. 52 | fn try_from(value: &str) -> Result { 53 | let lang = match value { 54 | $( 55 | $code => Self::[<$code>], 56 | )+ 57 | _ => return Err(LangConvertError::InvalidLang(value.to_string())), 58 | }; 59 | 60 | Ok(lang) 61 | } 62 | } 63 | 64 | impl TryFrom<&String> for Lang { 65 | type Error = LangConvertError; 66 | 67 | /// Convert ref String to enum `Lang` 68 | /// 69 | /// # Error 70 | /// 71 | /// Return `Error::InvalidLang` when given language code is not in the support list. 72 | fn try_from(value: &String) -> Result { 73 | let lang = match value.as_ref() { 74 | $( 75 | $code => Self::[<$code>], 76 | )+ 77 | _ => return Err(LangConvertError::InvalidLang(value.to_string())), 78 | }; 79 | 80 | Ok(lang) 81 | } 82 | } 83 | 84 | impl AsRef for Lang { 85 | fn as_ref(&self) -> &'static str { 86 | match self { 87 | $( 88 | Self::[<$code>] => $code, 89 | )+ 90 | } 91 | } 92 | } 93 | } 94 | }; 95 | } 96 | 97 | generate_langs! { 98 | ("AR", "Arabic"); 99 | ("BG", "Bulgarian"); 100 | ("CS", "Czech"); 101 | ("DA", "Danish"); 102 | ("DE", "German"); 103 | ("EL", "Greek"); 104 | ("EN", "English (Unspecified variant)"); 105 | ("EN-GB", "English (American)"); 106 | ("EN-US", "English (British)"); 107 | ("ES", "Spanish"); 108 | ("ET", "Estonian"); 109 | ("FI", "Finnish"); 110 | ("FR", "French"); 111 | ("HU", "Hungarian"); 112 | ("ID", "Indonesian"); 113 | ("IT", "Italian"); 114 | ("JA", "Japanese"); 115 | ("KO", "Korean"); 116 | ("LT", "Lithuanian"); 117 | ("LV", "Latvian"); 118 | ("NB", "Norwegian"); 119 | ("NL", "Dutch"); 120 | ("PL", "Polish"); 121 | ("PT", "Portuguese (all Portuguese varieties mixed)"); 122 | ("PT-BR", "Portuguese (Brazilian)"); 123 | ("PT-PT", "Portuguese (All Portuguese varieties excluding Brazilian)"); 124 | ("RO", "Romanian"); 125 | ("RU", "Russian"); 126 | ("SK", "Slovak"); 127 | ("SL", "Slovenian"); 128 | ("SV", "Swedish"); 129 | ("TR", "Turkish"); 130 | ("UK", "Ukrainian"); 131 | ("ZH", "Chinese"); 132 | ("ZH-HANS", "Chinese (simplified)"); 133 | ("ZH-HANT", "Chinese (tranditional)"); 134 | } 135 | 136 | impl<'de> Deserialize<'de> for Lang { 137 | fn deserialize(deserializer: D) -> Result 138 | where 139 | D: Deserializer<'de>, 140 | { 141 | let lang = String::deserialize(deserializer)?.to_uppercase(); 142 | 143 | let lang = Lang::try_from(&lang).map_err(|_| { 144 | serde::de::Error::custom( 145 | format!("invalid language code {lang}. This is an internal issue with the lib, please open issue") 146 | ) 147 | })?; 148 | 149 | Ok(lang) 150 | } 151 | } 152 | 153 | impl FromStr for Lang { 154 | type Err = LangConvertError; 155 | 156 | fn from_str(s: &str) -> Result { 157 | Lang::try_from(s) 158 | } 159 | } 160 | 161 | impl Display for Lang { 162 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 163 | write!(f, "{}", self.as_ref()) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # deepl-rs 2 | //! 3 | //! Deepl-rs is a simple library for making requests to the DeepL API endpoint easier. 4 | //! And it also provides types wrapping to guarantee runtime safety. 5 | //! 6 | //! This is still a **WORK IN PROGRESS** library, please open an issue on GitHub to request 7 | //! features. Be aware breaking changes will be released frequently. 8 | //! 9 | //! # Usage 10 | //! 11 | //! ```rust 12 | //! use deepl::DeepLApi; 13 | //! 14 | //! let key = std::env::var("DEEPL_API_KEY").unwrap(); 15 | //! let api = DeepLApi::with(&key).new(); 16 | //! let response = api.translate_text("Hello World", Lang::ZH).await.unwrap(); 17 | //! 18 | //! assert!(!response.translations.is_empty()); 19 | //! ``` 20 | //! 21 | //! See [`DeepLApi`] for detailed usage. 22 | //! 23 | //! # License 24 | //! 25 | //! This project is licensed under MIT license. 26 | //! 27 | 28 | mod endpoint; 29 | mod lang; 30 | 31 | use std::sync::Arc; 32 | 33 | //- Type Re-exporting 34 | pub use endpoint::{ 35 | document::{DocumentStatusResp, DocumentTranslateStatus, UploadDocumentResp}, 36 | glossary, 37 | languages::{LangInfo, LangType}, 38 | translate::{TagHandling, TranslateTextResp, ModelType}, 39 | usage::UsageResponse, 40 | Error, Formality, 41 | }; 42 | pub use lang::{Lang, LangConvertError}; 43 | pub use reqwest; 44 | //- 45 | 46 | /// A struct that contains necessary data for runtime. Data is stored in 47 | /// [`Arc`], so it is cheap to clone in your App's code. 48 | /// 49 | /// # Example 50 | /// 51 | /// ``` 52 | /// // simple API creation 53 | /// let deepl = DeepLApi::with("Your DeepL Key").new(); 54 | /// 55 | /// // **OR** customize it 56 | /// let duration = std::time::Duration::from_secs(30); 57 | /// let client = reqwest::Client::builder() 58 | /// .timeout(duration) 59 | /// .build() 60 | /// .unwrap(); 61 | /// 62 | /// // use the pro version API, and a custom client with 63 | /// // 30 secs timeout 64 | /// let deepl = DeepLApi::with("Your DeepL Key") 65 | /// .is_pro(true) 66 | /// .client(client) 67 | /// .new(); 68 | /// ``` 69 | #[derive(Debug, Clone)] 70 | pub struct DeepLApi { 71 | inner: Arc, 72 | } 73 | 74 | /// The inner instance which actually holds data 75 | #[derive(Debug)] 76 | struct DeepLApiInner { 77 | client: reqwest::Client, 78 | key: String, 79 | endpoint: reqwest::Url, 80 | } 81 | 82 | impl DeepLApi { 83 | /// Create a new api instance with auth key. 84 | pub fn with(key: &str) -> DeepLApiBuilder { 85 | DeepLApiBuilder::init(key.to_string()) 86 | } 87 | 88 | fn del(&self, url: reqwest::Url) -> reqwest::RequestBuilder { 89 | self.inner 90 | .client 91 | .delete(url) 92 | .header("Authorization", &self.inner.key) 93 | } 94 | 95 | fn post(&self, url: reqwest::Url) -> reqwest::RequestBuilder { 96 | self.inner 97 | .client 98 | .post(url) 99 | .header("Authorization", &self.inner.key) 100 | } 101 | 102 | fn get(&self, url: reqwest::Url) -> reqwest::RequestBuilder { 103 | self.inner 104 | .client 105 | .get(url) 106 | .header("Authorization", &self.inner.key) 107 | } 108 | 109 | fn get_endpoint(&self, route: &str) -> reqwest::Url { 110 | self.inner.endpoint.join(route).unwrap() 111 | } 112 | } 113 | 114 | /// The builder struct. **DO NOT USE IT IN YOUR APPS** 115 | pub struct DeepLApiBuilder { 116 | is_pro: bool, 117 | client: Option, 118 | key: String, 119 | } 120 | 121 | impl DeepLApiBuilder { 122 | fn init(key: String) -> Self { 123 | Self { 124 | key, 125 | is_pro: false, 126 | client: None, 127 | } 128 | } 129 | 130 | /// Set the a user defined [`reqwest::Client`] 131 | pub fn client(&mut self, c: reqwest::Client) -> &mut Self { 132 | self.client = Some(c); 133 | self 134 | } 135 | 136 | /// Set if you want to use the pro version DeepL Api 137 | pub fn is_pro(&mut self, is_pro: bool) -> &mut Self { 138 | self.is_pro = is_pro; 139 | self 140 | } 141 | 142 | /// Create a new instance of the DeepLApi 143 | pub fn new(&self) -> DeepLApi { 144 | let client = self.client.clone().unwrap_or_else(reqwest::Client::new); 145 | let endpoint = if self.is_pro || !self.key.ends_with(":fx") { 146 | "https://api.deepl.com/v2/" 147 | } else { 148 | "https://api-free.deepl.com/v2/" 149 | }; 150 | 151 | let inner = DeepLApiInner { 152 | key: format!("DeepL-Auth-Key {}", self.key), 153 | client, 154 | endpoint: reqwest::Url::parse(endpoint).unwrap(), 155 | }; 156 | 157 | DeepLApi { 158 | inner: Arc::new(inner), 159 | } 160 | } 161 | } 162 | --------------------------------------------------------------------------------