├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── main.rs ├── src ├── http │ ├── mod.rs │ ├── request.rs │ └── response.rs ├── lib.rs ├── router.rs └── server.rs └── tests └── integration_test.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | check: 7 | name: Lint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v3 12 | - uses: actions-rs/toolchain@v1 13 | with: 14 | profile: minimal 15 | toolchain: nightly 16 | override: true 17 | components: rustfmt, clippy 18 | - name: Cache build 19 | uses: Swatinem/rust-cache@v2 20 | with: 21 | key: cache 22 | - name: Check formatting 23 | uses: actions-rs/cargo@v1 24 | with: 25 | command: fmt 26 | args: --all -- --check 27 | - name: Clippy 28 | uses: actions-rs/clippy-check@v1 29 | with: 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | args: --locked --all-features --all-targets 32 | 33 | test: 34 | name: Tests 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v2 38 | - uses: actions-rs/toolchain@v1 39 | with: 40 | profile: minimal 41 | toolchain: nightly 42 | override: true 43 | - name: Cache build 44 | uses: Swatinem/rust-cache@v2 45 | with: 46 | key: cache 47 | - uses: actions-rs/cargo@v1 48 | with: 49 | command: test 50 | codecov: 51 | name: Coverage 52 | runs-on: ubuntu-latest 53 | env: 54 | RUSTFLAGS: -Cinstrument-coverage 55 | RUSTDOCFLAGS: -C instrument-coverage -Z unstable-options --persist-doctests target/debug/doctestbins 56 | LLVM_PROFILE_FILE: profile-%m.profraw 57 | steps: 58 | - name: Checkout repository 59 | uses: actions/checkout@v3 60 | - name: Install rust 61 | uses: actions-rs/toolchain@v1 62 | with: 63 | profile: minimal 64 | toolchain: nightly 65 | override: true 66 | components: llvm-tools-preview 67 | - name: Cache build 68 | uses: Swatinem/rust-cache@v2 69 | with: 70 | key: cache 71 | - name: Install cargo-binutils 72 | run: cargo install cargo-binutils 73 | - name: Build tests with coverage 74 | run: | 75 | cargo test --locked --all-features --all-targets --no-fail-fast --no-run 76 | cargo test --locked --all-features --doc --no-fail-fast -- --help 77 | - name: Run tests with coverage 78 | run: | 79 | cargo test --locked --all-features --all-targets --no-fail-fast -- --nocapture 80 | cargo test --locked --all-features --doc --no-fail-fast 81 | - name: Merge execution traces 82 | run: cargo profdata -- merge -sparse $(find . -iname "profile-*.profraw") -o profile.profdata 83 | - name: Export to lcov format for codecov 84 | run: cargo cov -- export 85 | --format=lcov > profile.lcov 86 | --instr-profile=profile.profdata 87 | $( 88 | for file in 89 | $( 90 | cargo test --locked --all-features --all-targets 91 | --no-fail-fast --no-run --message-format=json 92 | | jq -r "select(.profile.test == true) | .filenames[]" 93 | | grep -v dSYM - 94 | ) 95 | target/debug/doctestbins/*/rust_out; 96 | do 97 | [[ -x $file ]] && printf "%s %s " -object $file ; 98 | done 99 | ) 100 | - name: Submit to codecov.io 101 | uses: codecov/codecov-action@v3.1.1 102 | with: 103 | flags: test 104 | fail_ci_if_error: true 105 | verbose: true 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.68" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" 10 | 11 | [[package]] 12 | name = "async-stream" 13 | version = "0.3.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" 16 | dependencies = [ 17 | "async-stream-impl", 18 | "futures-core", 19 | ] 20 | 21 | [[package]] 22 | name = "async-stream-impl" 23 | version = "0.3.3" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" 26 | dependencies = [ 27 | "proc-macro2", 28 | "quote", 29 | "syn", 30 | ] 31 | 32 | [[package]] 33 | name = "autocfg" 34 | version = "1.1.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 37 | 38 | [[package]] 39 | name = "base64" 40 | version = "0.13.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 43 | 44 | [[package]] 45 | name = "bitflags" 46 | version = "1.3.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 49 | 50 | [[package]] 51 | name = "bumpalo" 52 | version = "3.11.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" 55 | 56 | [[package]] 57 | name = "bytes" 58 | version = "1.3.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" 61 | 62 | [[package]] 63 | name = "cc" 64 | version = "1.0.78" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" 67 | 68 | [[package]] 69 | name = "cfg-if" 70 | version = "1.0.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 73 | 74 | [[package]] 75 | name = "core-foundation" 76 | version = "0.9.3" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 79 | dependencies = [ 80 | "core-foundation-sys", 81 | "libc", 82 | ] 83 | 84 | [[package]] 85 | name = "core-foundation-sys" 86 | version = "0.8.3" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 89 | 90 | [[package]] 91 | name = "dashmap" 92 | version = "5.4.0" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" 95 | dependencies = [ 96 | "cfg-if", 97 | "hashbrown", 98 | "lock_api", 99 | "once_cell", 100 | "parking_lot_core", 101 | ] 102 | 103 | [[package]] 104 | name = "encoding_rs" 105 | version = "0.8.31" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" 108 | dependencies = [ 109 | "cfg-if", 110 | ] 111 | 112 | [[package]] 113 | name = "fastrand" 114 | version = "1.8.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" 117 | dependencies = [ 118 | "instant", 119 | ] 120 | 121 | [[package]] 122 | name = "fnv" 123 | version = "1.0.7" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 126 | 127 | [[package]] 128 | name = "foreign-types" 129 | version = "0.3.2" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 132 | dependencies = [ 133 | "foreign-types-shared", 134 | ] 135 | 136 | [[package]] 137 | name = "foreign-types-shared" 138 | version = "0.1.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 141 | 142 | [[package]] 143 | name = "form_urlencoded" 144 | version = "1.1.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 147 | dependencies = [ 148 | "percent-encoding", 149 | ] 150 | 151 | [[package]] 152 | name = "futures" 153 | version = "0.3.25" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" 156 | dependencies = [ 157 | "futures-channel", 158 | "futures-core", 159 | "futures-executor", 160 | "futures-io", 161 | "futures-sink", 162 | "futures-task", 163 | "futures-util", 164 | ] 165 | 166 | [[package]] 167 | name = "futures-channel" 168 | version = "0.3.25" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" 171 | dependencies = [ 172 | "futures-core", 173 | "futures-sink", 174 | ] 175 | 176 | [[package]] 177 | name = "futures-core" 178 | version = "0.3.25" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" 181 | 182 | [[package]] 183 | name = "futures-executor" 184 | version = "0.3.25" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" 187 | dependencies = [ 188 | "futures-core", 189 | "futures-task", 190 | "futures-util", 191 | ] 192 | 193 | [[package]] 194 | name = "futures-io" 195 | version = "0.3.25" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" 198 | 199 | [[package]] 200 | name = "futures-sink" 201 | version = "0.3.25" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" 204 | 205 | [[package]] 206 | name = "futures-task" 207 | version = "0.3.25" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" 210 | 211 | [[package]] 212 | name = "futures-util" 213 | version = "0.3.25" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" 216 | dependencies = [ 217 | "futures-channel", 218 | "futures-core", 219 | "futures-io", 220 | "futures-sink", 221 | "futures-task", 222 | "memchr", 223 | "pin-project-lite", 224 | "pin-utils", 225 | "slab", 226 | ] 227 | 228 | [[package]] 229 | name = "h2" 230 | version = "0.3.15" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" 233 | dependencies = [ 234 | "bytes", 235 | "fnv", 236 | "futures-core", 237 | "futures-sink", 238 | "futures-util", 239 | "http", 240 | "indexmap", 241 | "slab", 242 | "tokio", 243 | "tokio-util", 244 | "tracing", 245 | ] 246 | 247 | [[package]] 248 | name = "hashbrown" 249 | version = "0.12.3" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 252 | 253 | [[package]] 254 | name = "hermit-abi" 255 | version = "0.2.6" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 258 | dependencies = [ 259 | "libc", 260 | ] 261 | 262 | [[package]] 263 | name = "http" 264 | version = "0.2.8" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" 267 | dependencies = [ 268 | "bytes", 269 | "fnv", 270 | "itoa", 271 | ] 272 | 273 | [[package]] 274 | name = "http-body" 275 | version = "0.4.5" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 278 | dependencies = [ 279 | "bytes", 280 | "http", 281 | "pin-project-lite", 282 | ] 283 | 284 | [[package]] 285 | name = "httparse" 286 | version = "1.8.0" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 289 | 290 | [[package]] 291 | name = "httpdate" 292 | version = "1.0.2" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 295 | 296 | [[package]] 297 | name = "hyper" 298 | version = "0.14.23" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" 301 | dependencies = [ 302 | "bytes", 303 | "futures-channel", 304 | "futures-core", 305 | "futures-util", 306 | "h2", 307 | "http", 308 | "http-body", 309 | "httparse", 310 | "httpdate", 311 | "itoa", 312 | "pin-project-lite", 313 | "socket2", 314 | "tokio", 315 | "tower-service", 316 | "tracing", 317 | "want", 318 | ] 319 | 320 | [[package]] 321 | name = "hyper-tls" 322 | version = "0.5.0" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 325 | dependencies = [ 326 | "bytes", 327 | "hyper", 328 | "native-tls", 329 | "tokio", 330 | "tokio-native-tls", 331 | ] 332 | 333 | [[package]] 334 | name = "idna" 335 | version = "0.3.0" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 338 | dependencies = [ 339 | "unicode-bidi", 340 | "unicode-normalization", 341 | ] 342 | 343 | [[package]] 344 | name = "indexmap" 345 | version = "1.9.2" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" 348 | dependencies = [ 349 | "autocfg", 350 | "hashbrown", 351 | ] 352 | 353 | [[package]] 354 | name = "instant" 355 | version = "0.1.12" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 358 | dependencies = [ 359 | "cfg-if", 360 | ] 361 | 362 | [[package]] 363 | name = "ipnet" 364 | version = "2.7.0" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e" 367 | 368 | [[package]] 369 | name = "itoa" 370 | version = "1.0.5" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 373 | 374 | [[package]] 375 | name = "js-sys" 376 | version = "0.3.60" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" 379 | dependencies = [ 380 | "wasm-bindgen", 381 | ] 382 | 383 | [[package]] 384 | name = "lazy_static" 385 | version = "1.4.0" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 388 | 389 | [[package]] 390 | name = "libc" 391 | version = "0.2.139" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 394 | 395 | [[package]] 396 | name = "lil_http" 397 | version = "0.1.1" 398 | dependencies = [ 399 | "anyhow", 400 | "reqwest", 401 | "serde_json", 402 | "serial_test", 403 | "tokio", 404 | "tokio-test", 405 | ] 406 | 407 | [[package]] 408 | name = "lock_api" 409 | version = "0.4.9" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 412 | dependencies = [ 413 | "autocfg", 414 | "scopeguard", 415 | ] 416 | 417 | [[package]] 418 | name = "log" 419 | version = "0.4.17" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 422 | dependencies = [ 423 | "cfg-if", 424 | ] 425 | 426 | [[package]] 427 | name = "memchr" 428 | version = "2.5.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 431 | 432 | [[package]] 433 | name = "mime" 434 | version = "0.3.16" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 437 | 438 | [[package]] 439 | name = "mio" 440 | version = "0.8.5" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" 443 | dependencies = [ 444 | "libc", 445 | "log", 446 | "wasi", 447 | "windows-sys 0.42.0", 448 | ] 449 | 450 | [[package]] 451 | name = "native-tls" 452 | version = "0.2.11" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 455 | dependencies = [ 456 | "lazy_static", 457 | "libc", 458 | "log", 459 | "openssl", 460 | "openssl-probe", 461 | "openssl-sys", 462 | "schannel", 463 | "security-framework", 464 | "security-framework-sys", 465 | "tempfile", 466 | ] 467 | 468 | [[package]] 469 | name = "num_cpus" 470 | version = "1.15.0" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 473 | dependencies = [ 474 | "hermit-abi", 475 | "libc", 476 | ] 477 | 478 | [[package]] 479 | name = "once_cell" 480 | version = "1.16.0" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" 483 | 484 | [[package]] 485 | name = "openssl" 486 | version = "0.10.45" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" 489 | dependencies = [ 490 | "bitflags", 491 | "cfg-if", 492 | "foreign-types", 493 | "libc", 494 | "once_cell", 495 | "openssl-macros", 496 | "openssl-sys", 497 | ] 498 | 499 | [[package]] 500 | name = "openssl-macros" 501 | version = "0.1.0" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" 504 | dependencies = [ 505 | "proc-macro2", 506 | "quote", 507 | "syn", 508 | ] 509 | 510 | [[package]] 511 | name = "openssl-probe" 512 | version = "0.1.5" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 515 | 516 | [[package]] 517 | name = "openssl-sys" 518 | version = "0.9.80" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" 521 | dependencies = [ 522 | "autocfg", 523 | "cc", 524 | "libc", 525 | "pkg-config", 526 | "vcpkg", 527 | ] 528 | 529 | [[package]] 530 | name = "parking_lot" 531 | version = "0.12.1" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 534 | dependencies = [ 535 | "lock_api", 536 | "parking_lot_core", 537 | ] 538 | 539 | [[package]] 540 | name = "parking_lot_core" 541 | version = "0.9.5" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" 544 | dependencies = [ 545 | "cfg-if", 546 | "libc", 547 | "redox_syscall", 548 | "smallvec", 549 | "windows-sys 0.42.0", 550 | ] 551 | 552 | [[package]] 553 | name = "percent-encoding" 554 | version = "2.2.0" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 557 | 558 | [[package]] 559 | name = "pin-project-lite" 560 | version = "0.2.9" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 563 | 564 | [[package]] 565 | name = "pin-utils" 566 | version = "0.1.0" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 569 | 570 | [[package]] 571 | name = "pkg-config" 572 | version = "0.3.26" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 575 | 576 | [[package]] 577 | name = "proc-macro2" 578 | version = "1.0.49" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" 581 | dependencies = [ 582 | "unicode-ident", 583 | ] 584 | 585 | [[package]] 586 | name = "quote" 587 | version = "1.0.23" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 590 | dependencies = [ 591 | "proc-macro2", 592 | ] 593 | 594 | [[package]] 595 | name = "redox_syscall" 596 | version = "0.2.16" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 599 | dependencies = [ 600 | "bitflags", 601 | ] 602 | 603 | [[package]] 604 | name = "remove_dir_all" 605 | version = "0.5.3" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 608 | dependencies = [ 609 | "winapi", 610 | ] 611 | 612 | [[package]] 613 | name = "reqwest" 614 | version = "0.11.13" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" 617 | dependencies = [ 618 | "base64", 619 | "bytes", 620 | "encoding_rs", 621 | "futures-core", 622 | "futures-util", 623 | "h2", 624 | "http", 625 | "http-body", 626 | "hyper", 627 | "hyper-tls", 628 | "ipnet", 629 | "js-sys", 630 | "log", 631 | "mime", 632 | "native-tls", 633 | "once_cell", 634 | "percent-encoding", 635 | "pin-project-lite", 636 | "serde", 637 | "serde_json", 638 | "serde_urlencoded", 639 | "tokio", 640 | "tokio-native-tls", 641 | "tower-service", 642 | "url", 643 | "wasm-bindgen", 644 | "wasm-bindgen-futures", 645 | "web-sys", 646 | "winreg", 647 | ] 648 | 649 | [[package]] 650 | name = "ryu" 651 | version = "1.0.12" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" 654 | 655 | [[package]] 656 | name = "schannel" 657 | version = "0.1.20" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" 660 | dependencies = [ 661 | "lazy_static", 662 | "windows-sys 0.36.1", 663 | ] 664 | 665 | [[package]] 666 | name = "scopeguard" 667 | version = "1.1.0" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 670 | 671 | [[package]] 672 | name = "security-framework" 673 | version = "2.7.0" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" 676 | dependencies = [ 677 | "bitflags", 678 | "core-foundation", 679 | "core-foundation-sys", 680 | "libc", 681 | "security-framework-sys", 682 | ] 683 | 684 | [[package]] 685 | name = "security-framework-sys" 686 | version = "2.6.1" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" 689 | dependencies = [ 690 | "core-foundation-sys", 691 | "libc", 692 | ] 693 | 694 | [[package]] 695 | name = "serde" 696 | version = "1.0.152" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" 699 | 700 | [[package]] 701 | name = "serde_json" 702 | version = "1.0.91" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" 705 | dependencies = [ 706 | "itoa", 707 | "ryu", 708 | "serde", 709 | ] 710 | 711 | [[package]] 712 | name = "serde_urlencoded" 713 | version = "0.7.1" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 716 | dependencies = [ 717 | "form_urlencoded", 718 | "itoa", 719 | "ryu", 720 | "serde", 721 | ] 722 | 723 | [[package]] 724 | name = "serial_test" 725 | version = "0.10.0" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "1c789ec87f4687d022a2405cf46e0cd6284889f1839de292cadeb6c6019506f2" 728 | dependencies = [ 729 | "dashmap", 730 | "futures", 731 | "lazy_static", 732 | "log", 733 | "parking_lot", 734 | "serial_test_derive", 735 | ] 736 | 737 | [[package]] 738 | name = "serial_test_derive" 739 | version = "0.10.0" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "b64f9e531ce97c88b4778aad0ceee079216071cffec6ac9b904277f8f92e7fe3" 742 | dependencies = [ 743 | "proc-macro2", 744 | "quote", 745 | "syn", 746 | ] 747 | 748 | [[package]] 749 | name = "signal-hook-registry" 750 | version = "1.4.0" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 753 | dependencies = [ 754 | "libc", 755 | ] 756 | 757 | [[package]] 758 | name = "slab" 759 | version = "0.4.7" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 762 | dependencies = [ 763 | "autocfg", 764 | ] 765 | 766 | [[package]] 767 | name = "smallvec" 768 | version = "1.10.0" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 771 | 772 | [[package]] 773 | name = "socket2" 774 | version = "0.4.7" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 777 | dependencies = [ 778 | "libc", 779 | "winapi", 780 | ] 781 | 782 | [[package]] 783 | name = "syn" 784 | version = "1.0.107" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 787 | dependencies = [ 788 | "proc-macro2", 789 | "quote", 790 | "unicode-ident", 791 | ] 792 | 793 | [[package]] 794 | name = "tempfile" 795 | version = "3.3.0" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 798 | dependencies = [ 799 | "cfg-if", 800 | "fastrand", 801 | "libc", 802 | "redox_syscall", 803 | "remove_dir_all", 804 | "winapi", 805 | ] 806 | 807 | [[package]] 808 | name = "tinyvec" 809 | version = "1.6.0" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 812 | dependencies = [ 813 | "tinyvec_macros", 814 | ] 815 | 816 | [[package]] 817 | name = "tinyvec_macros" 818 | version = "0.1.0" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 821 | 822 | [[package]] 823 | name = "tokio" 824 | version = "1.23.0" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" 827 | dependencies = [ 828 | "autocfg", 829 | "bytes", 830 | "libc", 831 | "memchr", 832 | "mio", 833 | "num_cpus", 834 | "parking_lot", 835 | "pin-project-lite", 836 | "signal-hook-registry", 837 | "socket2", 838 | "tokio-macros", 839 | "windows-sys 0.42.0", 840 | ] 841 | 842 | [[package]] 843 | name = "tokio-macros" 844 | version = "1.8.2" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" 847 | dependencies = [ 848 | "proc-macro2", 849 | "quote", 850 | "syn", 851 | ] 852 | 853 | [[package]] 854 | name = "tokio-native-tls" 855 | version = "0.3.0" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" 858 | dependencies = [ 859 | "native-tls", 860 | "tokio", 861 | ] 862 | 863 | [[package]] 864 | name = "tokio-stream" 865 | version = "0.1.11" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" 868 | dependencies = [ 869 | "futures-core", 870 | "pin-project-lite", 871 | "tokio", 872 | ] 873 | 874 | [[package]] 875 | name = "tokio-test" 876 | version = "0.4.2" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "53474327ae5e166530d17f2d956afcb4f8a004de581b3cae10f12006bc8163e3" 879 | dependencies = [ 880 | "async-stream", 881 | "bytes", 882 | "futures-core", 883 | "tokio", 884 | "tokio-stream", 885 | ] 886 | 887 | [[package]] 888 | name = "tokio-util" 889 | version = "0.7.4" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" 892 | dependencies = [ 893 | "bytes", 894 | "futures-core", 895 | "futures-sink", 896 | "pin-project-lite", 897 | "tokio", 898 | "tracing", 899 | ] 900 | 901 | [[package]] 902 | name = "tower-service" 903 | version = "0.3.2" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 906 | 907 | [[package]] 908 | name = "tracing" 909 | version = "0.1.37" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 912 | dependencies = [ 913 | "cfg-if", 914 | "pin-project-lite", 915 | "tracing-core", 916 | ] 917 | 918 | [[package]] 919 | name = "tracing-core" 920 | version = "0.1.30" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 923 | dependencies = [ 924 | "once_cell", 925 | ] 926 | 927 | [[package]] 928 | name = "try-lock" 929 | version = "0.2.3" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 932 | 933 | [[package]] 934 | name = "unicode-bidi" 935 | version = "0.3.8" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 938 | 939 | [[package]] 940 | name = "unicode-ident" 941 | version = "1.0.6" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 944 | 945 | [[package]] 946 | name = "unicode-normalization" 947 | version = "0.1.22" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 950 | dependencies = [ 951 | "tinyvec", 952 | ] 953 | 954 | [[package]] 955 | name = "url" 956 | version = "2.3.1" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 959 | dependencies = [ 960 | "form_urlencoded", 961 | "idna", 962 | "percent-encoding", 963 | ] 964 | 965 | [[package]] 966 | name = "vcpkg" 967 | version = "0.2.15" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 970 | 971 | [[package]] 972 | name = "want" 973 | version = "0.3.0" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 976 | dependencies = [ 977 | "log", 978 | "try-lock", 979 | ] 980 | 981 | [[package]] 982 | name = "wasi" 983 | version = "0.11.0+wasi-snapshot-preview1" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 986 | 987 | [[package]] 988 | name = "wasm-bindgen" 989 | version = "0.2.83" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" 992 | dependencies = [ 993 | "cfg-if", 994 | "wasm-bindgen-macro", 995 | ] 996 | 997 | [[package]] 998 | name = "wasm-bindgen-backend" 999 | version = "0.2.83" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" 1002 | dependencies = [ 1003 | "bumpalo", 1004 | "log", 1005 | "once_cell", 1006 | "proc-macro2", 1007 | "quote", 1008 | "syn", 1009 | "wasm-bindgen-shared", 1010 | ] 1011 | 1012 | [[package]] 1013 | name = "wasm-bindgen-futures" 1014 | version = "0.4.33" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" 1017 | dependencies = [ 1018 | "cfg-if", 1019 | "js-sys", 1020 | "wasm-bindgen", 1021 | "web-sys", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "wasm-bindgen-macro" 1026 | version = "0.2.83" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" 1029 | dependencies = [ 1030 | "quote", 1031 | "wasm-bindgen-macro-support", 1032 | ] 1033 | 1034 | [[package]] 1035 | name = "wasm-bindgen-macro-support" 1036 | version = "0.2.83" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" 1039 | dependencies = [ 1040 | "proc-macro2", 1041 | "quote", 1042 | "syn", 1043 | "wasm-bindgen-backend", 1044 | "wasm-bindgen-shared", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "wasm-bindgen-shared" 1049 | version = "0.2.83" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" 1052 | 1053 | [[package]] 1054 | name = "web-sys" 1055 | version = "0.3.60" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" 1058 | dependencies = [ 1059 | "js-sys", 1060 | "wasm-bindgen", 1061 | ] 1062 | 1063 | [[package]] 1064 | name = "winapi" 1065 | version = "0.3.9" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1068 | dependencies = [ 1069 | "winapi-i686-pc-windows-gnu", 1070 | "winapi-x86_64-pc-windows-gnu", 1071 | ] 1072 | 1073 | [[package]] 1074 | name = "winapi-i686-pc-windows-gnu" 1075 | version = "0.4.0" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1078 | 1079 | [[package]] 1080 | name = "winapi-x86_64-pc-windows-gnu" 1081 | version = "0.4.0" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1084 | 1085 | [[package]] 1086 | name = "windows-sys" 1087 | version = "0.36.1" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 1090 | dependencies = [ 1091 | "windows_aarch64_msvc 0.36.1", 1092 | "windows_i686_gnu 0.36.1", 1093 | "windows_i686_msvc 0.36.1", 1094 | "windows_x86_64_gnu 0.36.1", 1095 | "windows_x86_64_msvc 0.36.1", 1096 | ] 1097 | 1098 | [[package]] 1099 | name = "windows-sys" 1100 | version = "0.42.0" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1103 | dependencies = [ 1104 | "windows_aarch64_gnullvm", 1105 | "windows_aarch64_msvc 0.42.0", 1106 | "windows_i686_gnu 0.42.0", 1107 | "windows_i686_msvc 0.42.0", 1108 | "windows_x86_64_gnu 0.42.0", 1109 | "windows_x86_64_gnullvm", 1110 | "windows_x86_64_msvc 0.42.0", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "windows_aarch64_gnullvm" 1115 | version = "0.42.0" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" 1118 | 1119 | [[package]] 1120 | name = "windows_aarch64_msvc" 1121 | version = "0.36.1" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 1124 | 1125 | [[package]] 1126 | name = "windows_aarch64_msvc" 1127 | version = "0.42.0" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" 1130 | 1131 | [[package]] 1132 | name = "windows_i686_gnu" 1133 | version = "0.36.1" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 1136 | 1137 | [[package]] 1138 | name = "windows_i686_gnu" 1139 | version = "0.42.0" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" 1142 | 1143 | [[package]] 1144 | name = "windows_i686_msvc" 1145 | version = "0.36.1" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 1148 | 1149 | [[package]] 1150 | name = "windows_i686_msvc" 1151 | version = "0.42.0" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" 1154 | 1155 | [[package]] 1156 | name = "windows_x86_64_gnu" 1157 | version = "0.36.1" 1158 | source = "registry+https://github.com/rust-lang/crates.io-index" 1159 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 1160 | 1161 | [[package]] 1162 | name = "windows_x86_64_gnu" 1163 | version = "0.42.0" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" 1166 | 1167 | [[package]] 1168 | name = "windows_x86_64_gnullvm" 1169 | version = "0.42.0" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" 1172 | 1173 | [[package]] 1174 | name = "windows_x86_64_msvc" 1175 | version = "0.36.1" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 1178 | 1179 | [[package]] 1180 | name = "windows_x86_64_msvc" 1181 | version = "0.42.0" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" 1184 | 1185 | [[package]] 1186 | name = "winreg" 1187 | version = "0.10.1" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 1190 | dependencies = [ 1191 | "winapi", 1192 | ] 1193 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lil_http" 3 | version = "0.1.1" 4 | edition = "2021" 5 | description = "A simple web framework with no external dependencies" 6 | license = "MIT" 7 | repository = "https://github.com/m1guelpf/lil-http-rs" 8 | readme = "README.md" 9 | keywords = ["web", "framework", "http"] 10 | categories = ["web-programming::http-server"] 11 | 12 | [dependencies] 13 | anyhow = "1.0.68" 14 | serde_json = "1.0.91" 15 | tokio = { version = "1.23.0", features = ["full"] } 16 | 17 | [dev-dependencies] 18 | tokio-test = "0.4.2" 19 | serial_test = "0.10.0" 20 | reqwest = { version = "0.11.13", features = ["json"] } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Miguel Piedrafita 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 | # lil-http 2 | 3 | A barebones HTTP 1.1 framework, built in Rust with no external dependencies (other than tokio). 4 | 5 | ## Features 6 | 7 | - [x] Listening to incoming requests 8 | - [x] Parsing method, path, query, headers, and body according to the HTTP 1.1 spec 9 | - [x] Responding to requests with an arbitrary body and headers 10 | - [x] Helpers for responding with text or JSON 11 | - [x] Allow defining routes and methods as closures 12 | - [x] Appropiately routing the request to its function, or 404'ing otherwise 13 | - [x] Appropiately crafting and returning 405 errors on invalid methods. 14 | 15 | ## Usage 16 | 17 | ```rust 18 | use lil_http::{Body, Response, Server}; 19 | 20 | #[tokio::main] 21 | async fn main() { 22 | let mut http = Server::new().await.unwrap(); 23 | 24 | http.routes 25 | .get("/", |request| { 26 | println!("Received {} request to {}", request.method, request.path); 27 | 28 | Response::text( 29 | format!( 30 | "Hello, {}!", 31 | request.query.get("name").unwrap_or(&"World".to_string()) 32 | ) 33 | .as_str(), 34 | ) 35 | }) 36 | .get("/api/user", |request| { 37 | println!("Received {} request to {}", request.method, request.path); 38 | 39 | Response::json(&serde_json::json!({ 40 | "name": "Miguel Piedrafita", 41 | "age": 20, 42 | })) 43 | }) 44 | .post("/api/hello", |request| { 45 | println!("Received {} request to {}", request.method, request.path); 46 | 47 | let Body::Json(body) = request.body else { 48 | return Response::invalid_request(); 49 | }; 50 | 51 | let Some(name) = body.get("name") else { 52 | return Response::invalid_request(); 53 | }; 54 | 55 | Response::json(&serde_json::json!({ 56 | "message": format!("Hello, {name}!"), 57 | })) 58 | }); 59 | 60 | http.run().await; 61 | } 62 | ``` 63 | 64 | ## License 65 | 66 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 67 | -------------------------------------------------------------------------------- /examples/main.rs: -------------------------------------------------------------------------------- 1 | use lil_http::{Body, Response, Server}; 2 | 3 | #[tokio::main] 4 | async fn main() { 5 | let mut http = Server::new().await.unwrap(); 6 | 7 | http.routes 8 | .get("/", |request| { 9 | println!("Received {} request to {}", request.method, request.path); 10 | 11 | Response::text( 12 | format!( 13 | "Hello, {}!", 14 | request.query.get("name").unwrap_or(&"World".to_string()) 15 | ) 16 | .as_str(), 17 | ) 18 | }) 19 | .get("/api/user", |request| { 20 | println!("Received {} request to {}", request.method, request.path); 21 | 22 | Response::json(&serde_json::json!({ 23 | "name": "Miguel Piedrafita", 24 | "age": 20, 25 | })) 26 | }) 27 | .post("/api/hello", |request| { 28 | println!("Received {} request to {}", request.method, request.path); 29 | 30 | let Body::Json(body) = request.body else { 31 | return Response::invalid_request(); 32 | }; 33 | 34 | let Some(name) = body.get("name") else { 35 | return Response::invalid_request(); 36 | }; 37 | 38 | Response::json(&serde_json::json!({ 39 | "message": format!("Hello, {name}!"), 40 | })) 41 | }); 42 | 43 | http.run().await; 44 | } 45 | -------------------------------------------------------------------------------- /src/http/mod.rs: -------------------------------------------------------------------------------- 1 | mod request; 2 | mod response; 3 | 4 | pub use request::{Body, Method, Request}; 5 | pub use response::{Response, StatusCode}; 6 | -------------------------------------------------------------------------------- /src/http/request.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use std::{ 3 | collections::HashMap, 4 | fmt::{Display, Formatter}, 5 | io::Error, 6 | }; 7 | 8 | const CRLF: &str = "\r\n"; 9 | 10 | /// The HTTP Method of a request. 11 | /// 12 | /// See [RFC 7231](https://tools.ietf.org/html/rfc7231#section-4) for more information. 13 | #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] 14 | pub enum Method { 15 | /// HEAD method. 16 | Head, 17 | /// GET method. 18 | Get, 19 | /// POST method. 20 | Post, 21 | /// PUT method. 22 | Put, 23 | /// DELETE method. 24 | Delete, 25 | } 26 | 27 | impl From for Method { 28 | fn from(val: String) -> Self { 29 | match val.as_str() { 30 | "HEAD" => Self::Head, 31 | "GET" => Self::Get, 32 | "POST" => Self::Post, 33 | "PUT" => Self::Put, 34 | "DELETE" => Self::Delete, 35 | _ => todo!(), 36 | } 37 | } 38 | } 39 | 40 | impl Display for Method { 41 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 42 | match self { 43 | Self::Head => write!(f, "HEAD"), 44 | Self::Get => write!(f, "GET"), 45 | Self::Post => write!(f, "POST"), 46 | Self::Put => write!(f, "PUT"), 47 | Self::Delete => write!(f, "DELETE"), 48 | } 49 | } 50 | } 51 | 52 | /// The HTTP Body of a request. 53 | /// 54 | /// See [RFC 7230](https://tools.ietf.org/html/rfc7230#section-3.3) for more information. 55 | #[derive(Debug, Clone, Eq, PartialEq)] 56 | pub enum Body { 57 | /// No body. 58 | None, 59 | /// A text/plain body. 60 | Text(String), 61 | /// A deserialized application/json body. 62 | Json(Value), 63 | } 64 | 65 | impl Body { 66 | /// # Panics 67 | /// 68 | /// Will panic if the content type is `application/json` and the body is not valid JSON. 69 | #[must_use] 70 | pub fn parse(body: String, content_type: Option<&String>) -> Self { 71 | match content_type { 72 | Some(content_type) => match content_type.as_str() { 73 | "application/json" => Self::Json(serde_json::from_str(&body).unwrap()), 74 | _ => Self::Text(body), 75 | }, 76 | None => Self::Text(body), 77 | } 78 | } 79 | } 80 | 81 | impl Display for Body { 82 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 83 | match self { 84 | Self::None => write!(f, ""), 85 | Self::Text(body) => write!(f, "{body}"), 86 | Self::Json(body) => write!(f, "{body}"), 87 | } 88 | } 89 | } 90 | 91 | /// An HTTP 1.1 request. 92 | /// 93 | /// See [RFC 7230](https://tools.ietf.org/html/rfc7230) for more information. 94 | #[derive(Debug, Clone)] 95 | pub struct Request { 96 | /// The HTTP method of the request. 97 | pub method: Method, 98 | /// The path of the request. 99 | pub path: String, 100 | /// The parsed query of the request. 101 | pub query: HashMap, 102 | /// The parsed headers of the request. 103 | pub headers: HashMap, 104 | /// The body of the request. 105 | pub body: Body, 106 | } 107 | 108 | /// Try to parse a request object from a buffer. 109 | /// 110 | /// # Errors 111 | /// 112 | /// Will return an error if the buffer is empty. 113 | /// 114 | /// # Panics 115 | /// 116 | /// Will panic if the request method is not valid. 117 | impl TryFrom<&[u8; 1024]> for Request { 118 | type Error = Error; 119 | 120 | fn try_from(buf: &[u8; 1024]) -> Result { 121 | let mut body = Body::None; 122 | let mut headers = HashMap::new(); 123 | 124 | if buf[0] == 0 { 125 | return Err(Error::new(std::io::ErrorKind::InvalidData, "Empty request")); 126 | } 127 | 128 | let mut buff_read: usize = 2; 129 | let mut lines = buf.split(|&byte| byte == b'\n'); 130 | 131 | let request_line = lines.next().unwrap(); 132 | buff_read += request_line.len() + 1; 133 | let mut request_line = request_line.split(|&byte| byte == b' '); 134 | 135 | let method: Method = String::from_utf8_lossy(request_line.next().unwrap()) 136 | .to_string() 137 | .into(); 138 | 139 | let uri = String::from_utf8_lossy(request_line.next().unwrap()); 140 | let mut uri = uri.splitn(2, |byte| byte == '?'); 141 | 142 | let path = uri.next().unwrap().trim().to_string(); 143 | 144 | let query = uri.next().map_or_else(HashMap::new, |query| { 145 | query 146 | .trim() 147 | .split('&') 148 | .map(|pair| { 149 | let mut pair = pair.split('='); 150 | let key = pair.next().unwrap().trim().to_lowercase(); 151 | let value = pair.next().unwrap().trim().to_string(); 152 | 153 | (key, value) 154 | }) 155 | .collect::>() 156 | }); 157 | 158 | for line in lines { 159 | if line == b"\r" { 160 | break; 161 | } 162 | 163 | let mut header = line.splitn(2, |&byte| byte == b':'); 164 | let name = header.next().unwrap(); 165 | let value = header.next().unwrap(); 166 | 167 | let value = String::from_utf8_lossy(value).trim().to_string(); 168 | let name = String::from_utf8_lossy(name) 169 | .trim() 170 | .to_lowercase() 171 | .to_string(); 172 | 173 | headers.insert(name, value); 174 | buff_read += line.len() + 1; 175 | } 176 | 177 | if let Some(content_length) = headers.get("content-length") { 178 | let content_length = content_length.parse::().unwrap(); 179 | 180 | body = Body::parse( 181 | String::from_utf8_lossy(&buf[buff_read..buff_read + content_length]) 182 | .trim() 183 | .to_string(), 184 | headers.get("content-type"), 185 | ); 186 | } 187 | 188 | Ok(Self { 189 | method, 190 | path, 191 | query, 192 | headers, 193 | body, 194 | }) 195 | } 196 | } 197 | 198 | impl Display for Request { 199 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 200 | let mut str_request = String::new(); 201 | 202 | str_request.push_str(&format!("{} {} HTTP/1.1{CRLF}", self.method, self.path)); 203 | for (name, value) in &self.headers { 204 | str_request.push_str(&format!("{name}: {value}{CRLF}")); 205 | } 206 | str_request.push_str(CRLF); 207 | str_request.push_str(&self.body.to_string()); 208 | 209 | write!(f, "{str_request}") 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/http/response.rs: -------------------------------------------------------------------------------- 1 | use crate::{Body, Method}; 2 | use serde_json::Value; 3 | use std::{ 4 | collections::HashMap, 5 | fmt::{Display, Formatter}, 6 | }; 7 | 8 | const CRLF: &str = "\r\n"; 9 | 10 | /// The HTTP status code of a response. 11 | /// 12 | /// See [RFC 7231](https://tools.ietf.org/html/rfc7231#section-6) for more information. 13 | #[derive(Debug, Clone, Eq, PartialEq)] 14 | pub enum StatusCode { 15 | /// 200 OK 16 | Ok, 17 | /// 404 Not Found 18 | NotFound, 19 | /// 400 Bad Request 20 | BadRequest, 21 | /// 405 Method Not Allowed 22 | MethodNotAllowed, 23 | } 24 | 25 | impl Display for StatusCode { 26 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 27 | write!( 28 | f, 29 | "{}", 30 | match self { 31 | Self::Ok => "200 OK", 32 | Self::NotFound => "404 Not Found", 33 | Self::BadRequest => "400 Bad Request", 34 | Self::MethodNotAllowed => "405 Method Not Allowed", 35 | } 36 | ) 37 | } 38 | } 39 | 40 | /// An HTTP 1.1 response. 41 | /// 42 | /// See [RFC 2616](https://www.rfc-editor.org/rfc/rfc2616#section-6) for more information. 43 | #[derive(Debug, Clone)] 44 | pub struct Response { 45 | /// The status code of the response. 46 | pub status_code: StatusCode, 47 | /// The HTTP headers of the response. 48 | pub headers: HashMap, 49 | /// The body of the response. 50 | pub body: Body, 51 | } 52 | 53 | impl Response { 54 | /// Create an empty response with the 200 OK status code. 55 | /// 56 | /// # Example 57 | /// ``` 58 | /// use lil_http::Response; 59 | /// # use lil_http::{StatusCode, Body}; 60 | /// 61 | /// let response = Response::ok(); 62 | /// 63 | /// # assert_eq!(response.body, Body::None); 64 | /// # assert_eq!(response.headers.len(), 0); 65 | /// # assert_eq!(response.status_code, StatusCode::Ok); 66 | /// ``` 67 | #[must_use] 68 | pub fn ok() -> Self { 69 | Self { 70 | body: Body::None, 71 | status_code: StatusCode::Ok, 72 | headers: HashMap::new(), 73 | } 74 | } 75 | 76 | /// Create a new text response with the given content. 77 | /// 78 | /// # Example 79 | /// ``` 80 | /// use lil_http::Response; 81 | /// # use lil_http::{StatusCode, Body}; 82 | /// 83 | /// let response = Response::text("Hello, World!"); 84 | /// 85 | /// # assert_eq!(response.status_code, StatusCode::Ok); 86 | /// # assert_eq!(response.body, Body::Text("Hello, World!".to_string())); 87 | /// # assert_eq!(response.headers.get("Content-Type"), Some(&"text/plain".to_string())); 88 | /// ``` 89 | #[must_use] 90 | pub fn text(body: &str) -> Self { 91 | Self::ok() 92 | .header("Content-Type", "text/plain") 93 | .body(Body::Text(body.to_string())) 94 | } 95 | 96 | /// Create a new JSON response with the given content. 97 | /// 98 | /// # Example 99 | /// ``` 100 | /// use lil_http::Response; 101 | /// # use lil_http::{StatusCode, Body}; 102 | /// use serde_json::json; 103 | /// 104 | /// let response = Response::json(&json!({ 105 | /// "message": "Hello, World!" 106 | /// })); 107 | /// 108 | /// # assert_eq!(response.status_code, StatusCode::Ok); 109 | /// # assert_eq!(response.body, Body::Json(json!({ "message": "Hello, World!" }))); 110 | /// # assert_eq!(response.headers.get("Content-Type"), Some(&"application/json".to_string())); 111 | /// ``` 112 | #[must_use] 113 | pub fn json(body: &Value) -> Self { 114 | Self::ok() 115 | .header("Content-Type", "application/json") 116 | .body(Body::Json(body.clone())) 117 | } 118 | 119 | /// Create a 404 Not Found response. 120 | /// 121 | /// # Example 122 | /// ``` 123 | /// use lil_http::Response; 124 | /// # use lil_http::{StatusCode, Body}; 125 | /// 126 | /// let response = Response::not_found(); 127 | /// 128 | /// # assert_eq!(response.status_code, StatusCode::NotFound); 129 | /// # assert_eq!(response.body, Body::Text("Not Found".to_string())); 130 | /// # assert_eq!(response.headers.get("Content-Type"), Some(&"text/plain".to_string())); 131 | /// ``` 132 | #[must_use] 133 | pub fn not_found() -> Self { 134 | Self::text("Not Found").status(StatusCode::NotFound) 135 | } 136 | 137 | /// Create a 400 Bad Request response. 138 | /// 139 | /// # Example 140 | /// ``` 141 | /// use lil_http::Response; 142 | /// # use lil_http::{StatusCode, Body}; 143 | /// 144 | /// let response = Response::invalid_request(); 145 | /// 146 | /// # assert_eq!(response.status_code, StatusCode::BadRequest); 147 | /// # assert_eq!(response.body, Body::Text("Invalid Request".to_string())); 148 | /// # assert_eq!(response.headers.get("Content-Type"), Some(&"text/plain".to_string())); 149 | /// ``` 150 | #[must_use] 151 | pub fn invalid_request() -> Self { 152 | Self::text("Invalid Request").status(StatusCode::BadRequest) 153 | } 154 | 155 | /// Create a 405 Method Not Allowed response. 156 | /// The `methods` parameter is a list of allowed methods. 157 | /// The `Allow` header will be set to a comma-separated list of the methods. 158 | /// 159 | /// # Example 160 | /// ``` 161 | /// use lil_http::{Response, Method}; 162 | /// # use lil_http::{StatusCode, Body}; 163 | /// 164 | /// let response = Response::method_not_allowed(&[Method::Get, Method::Post]); 165 | /// 166 | /// # assert_eq!(response.status_code, StatusCode::MethodNotAllowed); 167 | /// # assert_eq!(response.body, Body::Text("Method Not Allowed".to_string())); 168 | /// # assert_eq!(response.headers.get("Allow"), Some(&"GET, POST".to_string())); 169 | /// ``` 170 | #[must_use] 171 | pub fn method_not_allowed(methods: &[Method]) -> Self { 172 | let mut methods = methods 173 | .iter() 174 | .map(std::string::ToString::to_string) 175 | .collect::>(); 176 | methods.sort(); 177 | 178 | Self::text("Method Not Allowed") 179 | .status(StatusCode::MethodNotAllowed) 180 | .header("Allow", &methods.join(", ")) 181 | } 182 | 183 | /// Set the status code of the response. 184 | /// 185 | /// # Example 186 | /// 187 | /// ``` 188 | /// use lil_http::{Response, StatusCode}; 189 | /// 190 | /// let response = Response::text("...") 191 | /// .status(StatusCode::NotFound); 192 | /// 193 | /// # assert_eq!(response.status_code, StatusCode::NotFound); 194 | #[must_use] 195 | pub fn status(&mut self, code: StatusCode) -> Self { 196 | self.status_code = code; 197 | 198 | self.clone() 199 | } 200 | 201 | /// Add a header to the response. 202 | /// If the header already exists, it will be overwritten. 203 | /// 204 | /// # Example 205 | /// ``` 206 | /// use lil_http::Response; 207 | /// 208 | /// let response = Response::text("Hello, World!") 209 | /// .header("X-Example", "test-header"); 210 | /// 211 | /// # assert_eq!(response.headers.get("X-Example"), Some(&"test-header".to_string())); 212 | #[must_use] 213 | pub fn header(&mut self, name: &str, value: &str) -> Self { 214 | self.headers.insert(name.into(), value.into()); 215 | 216 | self.clone() 217 | } 218 | 219 | /// Set the body of the response. 220 | /// 221 | /// # Example 222 | /// ``` 223 | /// use lil_http::{Response, Body}; 224 | /// use serde_json::json; 225 | /// 226 | /// let response = Response::ok().body( 227 | /// Body::Json(json!({ "message": "Hello, World!" })) 228 | /// ); 229 | /// 230 | /// 231 | /// # assert_eq!(response.body, Body::Json(json!({ "message": "Hello, World!" }))); 232 | #[must_use] 233 | pub fn body(&mut self, body: Body) -> Self { 234 | self.body = body; 235 | 236 | self.clone() 237 | } 238 | } 239 | 240 | /// Convert a Response object into a HTTP 1.1 response string. 241 | impl ToString for Response { 242 | fn to_string(&self) -> String { 243 | let mut str_response = String::new(); 244 | 245 | str_response.push_str(&format!("HTTP/1.1 {}{CRLF}", self.status_code)); 246 | for (name, value) in &self.headers { 247 | str_response.push_str(&format!("{name}: {value}{CRLF}")); 248 | } 249 | str_response.push_str(CRLF); 250 | str_response.push_str(&self.body.to_string()); 251 | 252 | str_response 253 | } 254 | } 255 | 256 | #[cfg(test)] 257 | mod tests { 258 | use super::*; 259 | 260 | #[test] 261 | fn test_response_to_string() { 262 | let response = Response::text("Hello, World!"); 263 | 264 | assert_eq!( 265 | response.to_string(), 266 | "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello, World!" 267 | ); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(rustdoc_missing_doc_code_examples)] 2 | #![warn( 3 | clippy::all, 4 | clippy::pedantic, 5 | clippy::nursery, 6 | clippy::cargo, 7 | missing_docs, 8 | rustdoc::all 9 | )] 10 | 11 | //! `lil_http` is a simple HTTP server library for Rust. 12 | //! 13 | //! ## Example 14 | //! 15 | //! ```rust,no_run 16 | //! use lil_http::{Server, Response}; 17 | //! 18 | //! #[tokio::main] 19 | //! async fn main() { 20 | //! let mut http = Server::new().await.unwrap(); 21 | //! 22 | //! http.routes 23 | //! .get("/", |request| { 24 | //! println!("Received {} request to {}", request.method, request.path); 25 | //! 26 | //! Response::text("Hello, World!") 27 | //! }); 28 | //! 29 | //! http.run().await; 30 | //! } 31 | //! ``` 32 | 33 | mod http; 34 | mod router; 35 | mod server; 36 | 37 | pub use http::{Body, Method, Request, Response, StatusCode}; 38 | pub use server::Server; 39 | -------------------------------------------------------------------------------- /src/router.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use crate::http::{Method, Request, Response}; 4 | 5 | #[derive(Debug, Clone, Hash, Eq, PartialEq)] 6 | struct Route { 7 | path: String, 8 | methods: Vec, 9 | } 10 | 11 | impl From<&Request> for Route { 12 | fn from(val: &Request) -> Self { 13 | Self { 14 | path: val.path.clone(), 15 | methods: vec![val.method], 16 | } 17 | } 18 | } 19 | 20 | /// The router is responsible for matching requests to handlers. 21 | #[derive(Clone)] 22 | pub struct Router { 23 | routes: HashMap Response + Sync + Send>>, 24 | } 25 | 26 | impl Router { 27 | /// Create a new router instance. 28 | pub(crate) fn new() -> Self { 29 | Self { 30 | routes: HashMap::new(), 31 | } 32 | } 33 | 34 | /// Match a route to a handler. 35 | pub(crate) fn r#match( 36 | &mut self, 37 | methods: Vec, 38 | path: &str, 39 | handler: impl Fn(Request) -> Response + Send + Sync + 'static, 40 | ) -> &mut Self { 41 | self.routes.insert( 42 | Route { 43 | methods, 44 | path: path.to_string(), 45 | }, 46 | Arc::new(handler), 47 | ); 48 | 49 | self 50 | } 51 | 52 | /// Register a GET route. 53 | pub fn get( 54 | &mut self, 55 | path: &str, 56 | handler: impl Fn(Request) -> Response + Send + Sync + 'static, 57 | ) -> &mut Self { 58 | self.r#match(vec![Method::Get], path, handler) 59 | } 60 | 61 | /// Register a POST route. 62 | pub fn post( 63 | &mut self, 64 | path: &str, 65 | handler: impl Fn(Request) -> Response + Send + Sync + 'static, 66 | ) -> &mut Self { 67 | self.r#match(vec![Method::Post], path, handler) 68 | } 69 | 70 | /// Register a PUT route. 71 | pub fn put( 72 | &mut self, 73 | path: &str, 74 | handler: impl Fn(Request) -> Response + Send + Sync + 'static, 75 | ) -> &mut Self { 76 | self.r#match(vec![Method::Put], path, handler) 77 | } 78 | 79 | /// Register a DELETE route. 80 | pub fn delete( 81 | &mut self, 82 | path: &str, 83 | handler: impl Fn(Request) -> Response + Send + Sync + 'static, 84 | ) -> &mut Self { 85 | self.r#match(vec![Method::Delete], path, handler) 86 | } 87 | 88 | /// Handle an incoming request. 89 | /// If no route is found, a 404 response is returned. 90 | /// If a route is found, but the method is not allowed, a 405 response is returned. 91 | pub(crate) fn handle(&self, request: Request) -> Response { 92 | let path_routes = self 93 | .routes 94 | .iter() 95 | .filter(|(route, _)| route.path == request.path); 96 | 97 | if path_routes.clone().count() == 0 { 98 | return Response::not_found(); 99 | } 100 | 101 | match path_routes 102 | .clone() 103 | .find(|(route, _)| route.methods.contains(&request.method)) 104 | { 105 | Some((_, handler)) => handler(request), 106 | None => Response::method_not_allowed( 107 | &path_routes 108 | .flat_map(|(route, _)| route.methods.clone()) 109 | .collect::>(), 110 | ), 111 | } 112 | } 113 | } 114 | 115 | impl Default for Router { 116 | fn default() -> Self { 117 | Self::new() 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use super::*; 124 | use crate::{Body, StatusCode}; 125 | 126 | #[test] 127 | fn test_router_default() { 128 | let router = Router::default(); 129 | 130 | assert_eq!(router.routes.len(), 0); 131 | } 132 | 133 | #[test] 134 | fn test_route_from_request() { 135 | let request = Request { 136 | method: Method::Put, 137 | path: "/test/path".to_string(), 138 | query: HashMap::new(), 139 | headers: HashMap::new(), 140 | body: Body::None, 141 | }; 142 | 143 | let route = Route::from(&request); 144 | 145 | assert_eq!(route.path, "/test/path"); 146 | assert_eq!(route.methods, vec![Method::Put]); 147 | } 148 | 149 | #[test] 150 | fn test_router_match() { 151 | let mut router = Router::new(); 152 | 153 | router.r#match(vec![Method::Put], "/test/path", |_request| { 154 | Response::text("") 155 | }); 156 | 157 | assert_eq!(router.routes.len(), 1); 158 | assert!(router.routes.contains_key(&Route { 159 | path: "/test/path".to_string(), 160 | methods: vec![Method::Put] 161 | })); 162 | } 163 | 164 | #[test] 165 | fn test_router_get() { 166 | let mut router = Router::new(); 167 | 168 | router.get("/test/path", |_request| Response::text("")); 169 | 170 | assert_eq!(router.routes.len(), 1); 171 | assert!(router.routes.contains_key(&Route { 172 | path: "/test/path".to_string(), 173 | methods: vec![Method::Get] 174 | })); 175 | } 176 | 177 | #[test] 178 | fn test_router_post() { 179 | let mut router = Router::new(); 180 | 181 | router.post("/test/path", |_request| Response::text("")); 182 | 183 | assert_eq!(router.routes.len(), 1); 184 | assert!(router.routes.contains_key(&Route { 185 | path: "/test/path".to_string(), 186 | methods: vec![Method::Post] 187 | })); 188 | } 189 | 190 | #[test] 191 | fn test_router_put() { 192 | let mut router = Router::new(); 193 | 194 | router.put("/test/path", |_request| Response::text("")); 195 | 196 | assert_eq!(router.routes.len(), 1); 197 | assert!(router.routes.contains_key(&Route { 198 | path: "/test/path".to_string(), 199 | methods: vec![Method::Put] 200 | })); 201 | } 202 | 203 | #[test] 204 | fn test_router_delete() { 205 | let mut router = Router::new(); 206 | 207 | router.delete("/test/path", |_request| Response::text("")); 208 | 209 | assert_eq!(router.routes.len(), 1); 210 | assert!(router.routes.contains_key(&Route { 211 | path: "/test/path".to_string(), 212 | methods: vec![Method::Delete] 213 | })); 214 | } 215 | 216 | #[test] 217 | fn test_router_handle() { 218 | let mut router = Router::new(); 219 | 220 | router.get("/test/path", |_request| Response::text("test response")); 221 | 222 | let request = Request { 223 | method: Method::Get, 224 | path: "/test/path".to_string(), 225 | query: HashMap::new(), 226 | headers: HashMap::new(), 227 | body: Body::None, 228 | }; 229 | 230 | let response = router.handle(request); 231 | 232 | assert_eq!(response.status_code, StatusCode::Ok); 233 | assert_eq!(response.body, Body::Text("test response".to_string())); 234 | } 235 | 236 | #[test] 237 | fn test_returns_404_if_no_routes_match() { 238 | let router = Router::new(); 239 | 240 | let request = Request { 241 | method: Method::Get, 242 | path: "/".to_string(), 243 | query: HashMap::new(), 244 | headers: HashMap::new(), 245 | body: Body::None, 246 | }; 247 | 248 | let response = router.handle(request); 249 | 250 | assert_eq!(response.status_code, StatusCode::NotFound); 251 | assert_eq!(response.body, Body::Text("Not Found".to_string())); 252 | } 253 | 254 | #[test] 255 | fn test_returns_405_if_method_does_not_match() { 256 | let mut router = Router::new(); 257 | 258 | router 259 | .put("/test/path", |_| Response::text("test response")) 260 | .get("/test/path", |_| Response::text("test response")); 261 | 262 | let request = Request { 263 | method: Method::Post, 264 | path: "/test/path".to_string(), 265 | query: HashMap::new(), 266 | headers: HashMap::new(), 267 | body: Body::None, 268 | }; 269 | 270 | let response = router.handle(request); 271 | 272 | assert_eq!(response.status_code, StatusCode::MethodNotAllowed); 273 | assert_eq!(response.body, Body::Text("Method Not Allowed".to_string())); 274 | assert_eq!( 275 | response.headers.get("Allow").unwrap(), 276 | &"GET, PUT".to_string() 277 | ); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use crate::{http::Request, router::Router}; 2 | use anyhow::Result; 3 | use tokio::{ 4 | io::{AsyncReadExt, AsyncWriteExt}, 5 | net::{TcpListener, TcpStream}, 6 | }; 7 | 8 | /// The server is responsible for accepting connections and routing requests. 9 | /// 10 | /// # Example 11 | /// 12 | /// ```rust,no_run 13 | /// use lil_http::{Server, Response}; 14 | /// 15 | /// #[tokio::main] 16 | /// async fn main() { 17 | /// let mut http = Server::new().await.unwrap(); 18 | /// 19 | /// http.routes 20 | /// .get("/", |request| { 21 | /// println!("Received {} request to {}", request.method, request.path); 22 | /// 23 | /// Response::text("Hello, World!") 24 | /// }); 25 | /// 26 | /// http.run().await; 27 | /// } 28 | /// ``` 29 | /// 30 | /// # Notes 31 | /// 32 | /// The server will not stop until the process is killed. 33 | pub struct Server { 34 | /// The underlying TCP listener. 35 | listener: TcpListener, 36 | /// The router instance that will handle requests. 37 | pub routes: Router, 38 | } 39 | 40 | impl Server { 41 | /// # Errors 42 | /// 43 | /// Will return an error if port 3000 is already in use. 44 | pub async fn new() -> Result { 45 | Self::with_port("3000").await 46 | } 47 | 48 | /// # Errors 49 | /// 50 | /// Will return an error if the port is already in use. 51 | pub async fn with_port(port: &str) -> Result { 52 | let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await?; 53 | println!("Server listening on port {port}"); 54 | 55 | Ok(Self { 56 | listener, 57 | routes: Router::new(), 58 | }) 59 | } 60 | 61 | /// # Panics 62 | /// 63 | /// Will panic if the server fails to accept a connection. 64 | pub async fn run(&self) { 65 | loop { 66 | let incoming = self.listener.accept().await; 67 | 68 | match incoming { 69 | Ok((mut stream, _)) => { 70 | let router = self.routes.clone(); 71 | 72 | tokio::spawn(async move { 73 | Self::handle_connection(&mut stream, router).await.unwrap(); 74 | }); 75 | } 76 | Err(e) => { 77 | println!("error: {e}"); 78 | } 79 | } 80 | } 81 | } 82 | 83 | async fn handle_connection(stream: &mut TcpStream, router: Router) -> Result<()> { 84 | loop { 85 | let mut buf = [0; 1024]; 86 | _ = stream.read(&mut buf).await?; 87 | let Ok(request) = Request::try_from(&buf) else { break }; 88 | 89 | let response = router.handle(request); 90 | 91 | stream.write_all(response.to_string().as_bytes()).await?; 92 | stream.shutdown().await?; 93 | } 94 | 95 | Ok(()) 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod tests { 101 | use serial_test::serial; 102 | 103 | use super::*; 104 | 105 | #[tokio::test] 106 | #[serial] 107 | async fn test_can_create_server() { 108 | let _ = Server::new().await.unwrap(); 109 | } 110 | 111 | #[tokio::test] 112 | #[serial] 113 | async fn test_can_create_server_with_port() { 114 | let _ = Server::with_port("3001").await.unwrap(); 115 | } 116 | 117 | #[tokio::test] 118 | #[serial] 119 | async fn test_can_run_server() { 120 | let http = Server::with_port("4012").await.unwrap(); 121 | 122 | tokio::spawn(async move { 123 | http.run().await; 124 | }); 125 | 126 | let _ = TcpStream::connect("127.0.0.1:4012").await.unwrap(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use lil_http::{Body, Response, Server}; 2 | use serde_json::{json, Value}; 3 | use serial_test::serial; 4 | use std::collections::HashMap; 5 | 6 | #[tokio::test] 7 | #[serial] 8 | async fn test_http_lifecycle() { 9 | let mut server = Server::new().await.unwrap(); 10 | 11 | server.routes.get("/", |_| Response::text("Hello, World!")); 12 | 13 | tokio::spawn(async move { 14 | server.run().await; 15 | }); 16 | 17 | let client = reqwest::Client::new(); 18 | let response = client.get("http://localhost:3000/").send().await.unwrap(); 19 | 20 | assert_eq!(response.status(), 200); 21 | assert_eq!(response.text().await.unwrap(), "Hello, World!"); 22 | } 23 | 24 | #[tokio::test] 25 | #[serial] 26 | async fn test_http_lifecycle_with_port() { 27 | let mut server = Server::with_port("3001").await.unwrap(); 28 | 29 | server.routes.get("/", |_| Response::text("Hello, World!")); 30 | 31 | tokio::spawn(async move { 32 | server.run().await; 33 | }); 34 | 35 | let client = reqwest::Client::new(); 36 | let response = client.get("http://localhost:3001/").send().await.unwrap(); 37 | 38 | assert_eq!(response.status(), 200); 39 | assert_eq!(response.text().await.unwrap(), "Hello, World!"); 40 | } 41 | 42 | #[tokio::test] 43 | #[serial] 44 | async fn test_post_with_body_and_response() { 45 | let mut server = Server::new().await.unwrap(); 46 | 47 | server.routes.post("/", |request| { 48 | dbg!(&request.body); 49 | let Body::Json(body) = request.body else { 50 | return Response::invalid_request(); 51 | }; 52 | 53 | let Some(name) = body.get("name") else { 54 | return Response::invalid_request(); 55 | }; 56 | 57 | Response::json(&json!({ 58 | "message": format!("Hello, {}!", name.as_str().unwrap()) 59 | })) 60 | }); 61 | 62 | tokio::spawn(async move { 63 | server.run().await; 64 | }); 65 | 66 | let mut body = HashMap::new(); 67 | body.insert("name", "Miguel"); 68 | 69 | let client = reqwest::Client::new(); 70 | let response = client 71 | .post("http://localhost:3000/") 72 | .json(&body) 73 | .send() 74 | .await 75 | .unwrap(); 76 | 77 | assert_eq!(response.status(), 200); 78 | assert_eq!( 79 | response.json::().await.unwrap(), 80 | json!({ "message": "Hello, Miguel!" }) 81 | ); 82 | } 83 | --------------------------------------------------------------------------------