diff --git a/.gitignore b/.gitignore index eb5a316..ed092f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ target +rls diff --git a/Cargo.lock b/Cargo.lock index 94cd5df..448cee4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,4 +1,512 @@ [[package]] -name = "superagent" -version = "0.1.0" +name = "ascii" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "base64" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bitflags" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byteorder" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cc" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "chunked_transfer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "dns-lookup" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "socket2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "dtoa" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding-index-korean 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding-index-simpchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding-index-singlebyte 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding-index-tradchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "idna" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazy_static" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.42" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "log" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "log" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "matches" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "mime" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mime_guess" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "phf 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_codegen 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "phf" +version = "0.7.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_codegen" +version = "0.7.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_generator 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_generator" +version = "0.7.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_shared" +version = "0.7.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "qstring" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ring" +version = "0.13.0-alpha5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustls" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.13.0-alpha5 (registry+https://github.com/rust-lang/crates.io-index)", + "sct 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.18.0-alpha4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "safemem" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "sct" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ring 0.13.0-alpha5 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_json" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "siphasher" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "socket2" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicase" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "untrusted" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ureq" +version = "0.1.0" +dependencies = [ + "ascii 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "chunked_transfer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dns-lookup 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mime_guess 1.8.4 (registry+https://github.com/rust-lang/crates.io-index)", + "qstring 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.20 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.18.0-alpha4 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki-roots 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "url" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "version_check" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "webpki" +version = "0.18.0-alpha4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ring 0.13.0-alpha5 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "webpki-roots" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "untrusted 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.18.0-alpha4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum ascii 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2b3c943947ef9a60212bd70d62f5775333c7739cf44b1b4abcbf4a57976f1867" +"checksum base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "85415d2594767338a74a30c1d370b2f3262ec1b4ed2d7bba5b3faf4de40467d9" +"checksum bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d0c54bb8f454c567f21197eefcdbf5679d0bd99f2ddbe52e84c77061952e6789" +"checksum byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "74c0b906e9446b0a2e4f760cdb3fa4b2c48cdc6db8766a845c54b6ff063fd2e9" +"checksum cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "49ec142f5768efb5b7622aebc3fdbdbb8950a4b9ba996393cb76ef7466e8747d" +"checksum cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "405216fd8fe65f718daa7102ea808a946b6ce40c742998fbfd3463645552de18" +"checksum chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1cce36c92cb605414e9b824f866f5babe0a0368e39ea07393b9b63cf3844c0e6" +"checksum chunked_transfer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "498d20a7aaf62625b9bf26e637cf7736417cde1d0c99f1d04d1170229a85cf87" +"checksum dns-lookup 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "54810764899241c707428f4a1989351f30c0c2bda5ea07ff2e43148f8935039f" +"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" +"checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +"checksum encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +"checksum encoding-index-korean 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +"checksum encoding-index-simpchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +"checksum encoding-index-singlebyte 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +"checksum encoding-index-tradchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +"checksum encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" +"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" +"checksum itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c069bbec61e1ca5a596166e55dfe4773ff745c3d16b700013bcaff9a6df2c682" +"checksum lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e6412c5e2ad9584b0b8e979393122026cdd6d2a80b933f890dcd694ddbe73739" +"checksum libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b685088df2b950fccadf07a7187c8ef846a959c142338a48f9dc0b94517eb5f1" +"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +"checksum log 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6fddaa003a65722a7fb9e26b0ce95921fe4ba590542ced664d8ce2fa26f9f3ac" +"checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376" +"checksum mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" +"checksum mime_guess 1.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b7e2b09d08313f84e0fb82d13a4d859109a17543fe9af3b6d941dc1431f7de79" +"checksum num-integer 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)" = "6ac0ea58d64a89d9d6b7688031b3be9358d6c919badcf7fbb0527ccfd891ee45" +"checksum num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "775393e285254d2f5004596d69bb8bc1149754570dcc08cf30cabeba67955e28" +"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" +"checksum phf 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "7d37a244c75a9748e049225155f56dbcb98fe71b192fd25fd23cb914b5ad62f2" +"checksum phf_codegen 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "4e4048fe7dd7a06b8127ecd6d3803149126e9b33c7558879846da3a63f734f2b" +"checksum phf_generator 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "05a079dd052e7b674d21cb31cbb6c05efd56a2cd2827db7692e2f1a507ebd998" +"checksum phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "c2261d544c2bb6aa3b10022b0be371b9c7c64f762ef28c6f5d4f1ef6d97b5930" +"checksum qstring 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d3f5d9b355cd0db9ef4b46075e6dd578bf9700448ca7f3b8af1481b3c495d4" +"checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" +"checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" +"checksum ring 0.13.0-alpha5 (registry+https://github.com/rust-lang/crates.io-index)" = "3845516753f91b4511f9b17c917ea6fa4bc5a7853a9947b0f66731aff51cdef5" +"checksum rustls 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ab72e4883a4fc9fd5cd462a51c55d79f6a7b5c9483e8d73a2b7bca0b18430bcd" +"checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" +"checksum sct 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4540aed8d71a5de961a8902cf356e28122bd62695eb5be1c214f84d8704097c" +"checksum serde 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)" = "e9a2d9a9ac5120e0f768801ca2b58ad6eec929dc9d1d616c162f208869c2ce95" +"checksum serde_json 1.0.20 (registry+https://github.com/rust-lang/crates.io-index)" = "fc97cccc2959f39984524026d760c08ef0dd5f0f5948c8d31797dbfae458c875" +"checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537" +"checksum socket2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "06dc9f86ee48652b7c80f3d254e3b9accb67a928c562c64d10d7b016d3d98dab" +"checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" +"checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" +"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +"checksum unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "6a0180bc61fc5a987082bfa111f4cc95c4caff7f9799f3e46df09163a937aa25" +"checksum untrusted 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "70afa43c8c5d23a53a3c39ec9b56232c5badc19f6bb5ad529c1d6448a7241365" +"checksum url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f808aadd8cfec6ef90e4a14eb46f24511824d1ac596b9682703c87056c8678b7" +"checksum version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6b772017e347561807c1aa192438c5fd74242a670a6cffacc40f2defd1dc069d" +"checksum webpki 0.18.0-alpha4 (registry+https://github.com/rust-lang/crates.io-index)" = "724897af4bb44f3e0142b9cca300eb15f61b9b34fa559440bed8c43f2ff7afc0" +"checksum webpki-roots 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "edbd75d6abf044ef0c9d7ec92b9e8c518bcd93a15bb7bd9a92239e035248fc17" +"checksum winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "773ef9dcc5f24b7d850d0ff101e542ff24c3b090a9768e03ff889fdef41f00fd" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 8a9784e..faa190e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,20 @@ [package] -name = "superagent" +name = "ureq" version = "0.1.0" authors = ["Martin Algesten "] [dependencies] +ascii = "0.9" +base64 = "*" +chrono = "0.4" +chunked_transfer = "0.3" +dns-lookup = "0.9.1" +encoding = "0.2" +lazy_static = "1" +mime_guess = "1" +qstring = "0.5" +rustls = "0.12" +serde_json = "1" +url = "1.6" +webpki = "0.18.0-alpha3" +webpki-roots = "0.14.0" diff --git a/src/agent.rs b/src/agent.rs new file mode 100644 index 0000000..c4f5a63 --- /dev/null +++ b/src/agent.rs @@ -0,0 +1,315 @@ +use std::str::FromStr; +use std::sync::Mutex; + +use header::Header; +use util::*; + +// to get to share private fields +include!("request.rs"); +include!("response.rs"); +include!("conn.rs"); + +#[derive(Debug, Default, Clone)] +pub struct Agent { + pub headers: Vec
, + pub auth: Option<(String, String)>, + pub pool: Arc>>, +} + +impl Agent { + pub fn new() -> Agent { + Default::default() + } + + /// Create a new agent after treating it as a builder. + /// This actually clones the internal state to a new one and instantiates + /// a new connection pool that is reused between connects. + pub fn build(&self) -> Self { + Agent { + headers: self.headers.clone(), + auth: self.auth.clone(), + pool: Arc::new(Mutex::new(Some(ConnectionPool::new()))), + } + } + + /// Set a header field that will be present in all requests using the agent. + /// + /// ``` + /// let agent = ureq::agent() + /// .set("X-API-Key", "foobar") + /// .set("Accept", "application/json") + /// .build(); + /// + /// let r = agent + /// .get("/my-page") + /// .call(); + /// + /// if r.ok() { + /// println!("yay got {}", r.into_json().unwrap()); + /// } else { + /// println!("Oh no error!"); + /// } + /// ``` + pub fn set(&mut self, header: K, value: V) -> &mut Agent + where + K: Into, + V: Into, + { + add_agent_header(self, header.into(), value.into()); + self + } + + /// Set many headers that will be present in all requests using the agent. + /// + /// ``` + /// #[macro_use] + /// extern crate ureq; + /// + /// fn main() { + /// let agent = ureq::agent() + /// .set_map(map!{ + /// "X-API-Key" => "foobar", + /// "Accept" => "application/json" + /// }) + /// .build(); + /// + /// let r = agent + /// .get("/my_page") + /// .call(); + /// + /// if r.ok() { + /// println!("yay got {}", r.into_json().unwrap()); + /// } + /// } + /// ``` + pub fn set_map(&mut self, headers: I) -> &mut Agent + where + K: Into, + V: Into, + I: IntoIterator, + { + for (k, v) in headers.into_iter() { + add_agent_header(self, k.into(), v.into()); + } + self + } + + /// Basic auth that will be present in all requests using the agent. + /// + /// ``` + /// let agent = ureq::agent() + /// .auth("martin", "rubbermashgum") + /// .build(); + /// + /// let r = agent + /// .get("/my_page") + /// .call(); + /// println!("{:?}", r); + /// ``` + pub fn auth(&mut self, user: S, pass: T) -> &mut Agent + where + S: Into, + T: Into, + { + let u = user.into(); + let p = pass.into(); + let pass = basic_auth(&u, &p); + self.auth_kind("Basic", pass) + } + + /// Auth of other kinds such as `Digest`, `Token` etc, that will be present + /// in all requests using the agent. + /// + /// ``` + /// let agent = ureq::agent() + /// .auth_kind("token", "secret") + /// .build(); + /// + /// let r = agent + /// .get("/my_page") + /// .call(); + /// ``` + pub fn auth_kind(&mut self, kind: S, pass: T) -> &mut Agent + where + S: Into, + T: Into, + { + self.auth = Some((kind.into(), pass.into())); + self + } + + /// Request by providing the HTTP verb such as `GET`, `POST`... + /// + /// ``` + /// let agent = ureq::agent(); + /// + /// let r = agent + /// .request("GET", "/my_page") + /// .call(); + /// println!("{:?}", r); + /// ``` + pub fn request(&self, method: M, path: S) -> Request + where + M: Into, + S: Into, + { + Request::new(&self, method.into(), path.into()) + } + + pub fn get(&self, path: S) -> Request + where + S: Into, + { + self.request("GET", path) + } + pub fn head(&self, path: S) -> Request + where + S: Into, + { + self.request("HEAD", path) + } + pub fn post(&self, path: S) -> Request + where + S: Into, + { + self.request("POST", path) + } + pub fn put(&self, path: S) -> Request + where + S: Into, + { + self.request("PUT", path) + } + pub fn delete(&self, path: S) -> Request + where + S: Into, + { + self.request("DELETE", path) + } + pub fn trace(&self, path: S) -> Request + where + S: Into, + { + self.request("TRACE", path) + } + pub fn options(&self, path: S) -> Request + where + S: Into, + { + self.request("OPTIONS", path) + } + pub fn connect(&self, path: S) -> Request + where + S: Into, + { + self.request("CONNECT", path) + } + pub fn patch(&self, path: S) -> Request + where + S: Into, + { + self.request("PATCH", path) + } +} + +fn add_agent_header(agent: &mut Agent, k: String, v: String) { + if let Ok(h) = Header::from_str(&format!("{}: {}", k, v)) { + agent.headers.push(h); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + ///////////////////// AGENT TESTS ////////////////////////////// + + #[test] + fn agent_implements_send() { + let mut agent = Agent::new(); + ::std::thread::spawn(move || { + agent.set("Foo", "Bar"); + }); + } + + //////////////////// REQUEST TESTS ///////////////////////////// + + #[test] + fn request_implements_send() { + let agent = Agent::new(); + let mut request = Request::new(&agent, "GET".to_string(), "/foo".to_string()); + ::std::thread::spawn(move || { + request.set("Foo", "Bar"); + }); + } + + //////////////////// RESPONSE TESTS ///////////////////////////// + + #[test] + fn content_type_without_charset() { + let s = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\nOK"; + let resp = s.parse::().unwrap(); + assert_eq!("application/json", resp.content_type()); + } + + #[test] + fn content_type_with_charset() { + let s = "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=iso-8859-4\r\n\r\nOK"; + let resp = s.parse::().unwrap(); + assert_eq!("application/json", resp.content_type()); + } + + #[test] + fn content_type_default() { + let s = "HTTP/1.1 200 OK\r\n\r\nOK"; + let resp = s.parse::().unwrap(); + assert_eq!("text/plain", resp.content_type()); + } + + #[test] + fn charset() { + let s = "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=iso-8859-4\r\n\r\nOK"; + let resp = s.parse::().unwrap(); + assert_eq!("iso-8859-4", resp.charset()); + } + + #[test] + fn charset_default() { + let s = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\nOK"; + let resp = s.parse::().unwrap(); + assert_eq!("utf-8", resp.charset()); + } + + #[test] + fn chunked_transfer() { + let s = "HTTP/1.1 200 OK\r\nTransfer-Encoding: Chunked\r\n\r\n3\r\nhel\r\nb\r\nlo world!!!\r\n0\r\n\r\n"; + let resp = s.parse::().unwrap(); + assert_eq!("hello world!!!", resp.into_string().unwrap()); + } + + #[test] + fn parse_simple_json() { + let s = format!("HTTP/1.1 200 OK\r\n\r\n{{\"hello\":\"world\"}}"); + let resp = s.parse::().unwrap(); + let v = resp.into_json().unwrap(); + assert_eq!( + v, + "{\"hello\":\"world\"}" + .parse::() + .unwrap() + ); + } + + #[test] + fn parse_borked_header() { + let s = format!("HTTP/1.1 BORKED\r\n"); + let resp: Response = s.parse::().unwrap_err().into(); + assert_eq!(resp.http_version(), "HTTP/1.1"); + assert_eq!(*resp.status(), 500); + assert_eq!(resp.status_text(), "Bad Status"); + assert_eq!(resp.content_type(), "text/plain"); + let v = resp.into_string().unwrap(); + assert_eq!(v, "Bad Status\n"); + } + +} diff --git a/src/conn.rs b/src/conn.rs new file mode 100644 index 0000000..08d19e9 --- /dev/null +++ b/src/conn.rs @@ -0,0 +1,193 @@ +use dns_lookup; +use rustls; +use std::io::Write; +use std::net::IpAddr; +use std::net::SocketAddr; +use std::net::TcpStream; +use std::time::Duration; +use stream::Stream; +use url::Url; +use webpki; +use webpki_roots; + +const CHUNK_SIZE: usize = 1024 * 1024; + +#[derive(Debug, Default, Clone)] +pub struct ConnectionPool {} + +impl ConnectionPool { + fn new() -> Self { + ConnectionPool {} + } + + fn connect( + &mut self, + request: &Request, + method: &str, + url: &Url, + redirects: u32, + payload: Payload, + ) -> Result { + // + // open connection + let mut stream = match url.scheme() { + "http" => connect_http(request, &url), + "https" => connect_https(request, &url), + _ => Err(Error::UnknownScheme(url.scheme().to_string())), + }?; + + // send the request start + headers + let mut prelude: Vec = vec![]; + write!(prelude, "{} {} HTTP/1.1\r\n", method, url.path())?; + if !request.has("host") { + write!(prelude, "Host: {}\r\n", url.host().unwrap())?; + } + for header in request.headers.iter() { + write!(prelude, "{}: {}\r\n", header.name(), header.value())?; + } + write!(prelude, "\r\n")?; + + stream.write_all(&mut prelude[..])?; + + // start reading the response to check it it's a redirect + let mut resp = Response::from_read(&mut stream); + + // handle redirects + if resp.redirect() { + if redirects == 0 { + return Err(Error::TooManyRedirects); + } + + // the location header + let location = resp.get("location"); + if let Some(location) = location { + // join location header to current url in case it it relative + let new_url = url.join(location) + .map_err(|_| Error::BadUrl(format!("Bad redirection: {}", location)))?; + + // perform the redirect differently depending on 3xx code. + return match resp.status { + 301 | 302 | 303 => { + send_payload(&request, payload, &mut stream)?; + self.connect(request, "GET", &new_url, redirects - 1, Payload::Empty) + } + 307 | 308 | _ => { + self.connect(request, method, &new_url, redirects - 1, payload) + } + }; + } + } + + // send the payload (which can be empty now depending on redirects) + send_payload(&request, payload, &mut stream)?; + + // since it is not a redirect, give away the incoming stream to the response object + resp.set_reader(stream); + + // release the response + Ok(resp) + } +} + +fn connect_http(request: &Request, url: &Url) -> Result { + // + let hostname = url.host_str().unwrap(); + let port = url.port().unwrap_or(80); + + connect_host(request, hostname, port).map(|tcp| Stream::Http(tcp)) +} + +fn connect_https(request: &Request, url: &Url) -> Result { + // + let hostname = url.host_str().unwrap(); + let port = url.port().unwrap_or(443); + + // TODO let user override TLS roots. + let mut config = rustls::ClientConfig::new(); + config + .root_store + .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + let rc_config = Arc::new(config); + + let socket = connect_host(request, hostname, port)?; + + webpki::DNSNameRef::try_from_ascii_str(&hostname) + .map_err(|_| Error::ConnectionFailed(format!("Invalid TLS name: {}", hostname))) + .map(|webpki| rustls::ClientSession::new(&rc_config, webpki)) + .map(|client| Stream::Https(client, socket)) +} + +fn connect_host(request: &Request, hostname: &str, port: u16) -> Result { + // + let ips: Vec = + dns_lookup::lookup_host(hostname).map_err(|e| Error::DnsFailed(format!("{}", e)))?; + + if ips.len() == 0 { + return Err(Error::DnsFailed(format!("No ip address for {}", hostname))); + } + + // pick first ip, or should we randomize? + let sock_addr = SocketAddr::new(ips[0], port); + + // connect with a configured timeout. + let stream = match request.timeout { + 0 => TcpStream::connect(&sock_addr), + _ => TcpStream::connect_timeout(&sock_addr, Duration::from_millis(request.timeout as u64)), + }.map_err(|err| Error::ConnectionFailed(format!("{}", err)))?; + + // rust's absurd api returns Err if we set 0. + if request.timeout_read > 0 { + stream + .set_read_timeout(Some(Duration::from_millis(request.timeout_read as u64))) + .ok(); + } + if request.timeout_write > 0 { + stream + .set_write_timeout(Some(Duration::from_millis(request.timeout_write as u64))) + .ok(); + } + + Ok(stream) +} + +fn send_payload(request: &Request, payload: Payload, stream: &mut Stream) -> IoResult<()> { + // + let (size, reader) = payload.into_read(); + + let do_chunk = request.get("transfer-encoding") + // if the user has set an encoding header, obey that. + .map(|enc| enc.eq_ignore_ascii_case("chunked")) + // if the content has a size + .ok_or_else(|| size. + // or if the user set a content-length header + or_else(|| + request.get("content-length").map(|len| len.parse::().unwrap_or(0))) + // and that size is larger than 1MB, chunk, + .map(|size| size > CHUNK_SIZE)) + // otherwise, assume chunking since it can be really big. + .unwrap_or(true); + + if do_chunk { + pipe(reader, chunked_transfer::Encoder::new(stream))?; + } else { + pipe(reader, stream)?; + } + + Ok(()) +} + +fn pipe(mut reader: R, mut writer: W) -> IoResult<()> +where + R: Read, + W: Write, +{ + let mut buf = [0_u8; CHUNK_SIZE]; + loop { + let len = reader.read(&mut buf)?; + if len == 0 { + break; + } + writer.write_all(&buf[0..len])?; + } + Ok(()) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..cbc458a --- /dev/null +++ b/src/error.rs @@ -0,0 +1,61 @@ +use std::io::Error as IoError; + +#[derive(Debug)] +pub enum Error { + BadUrl(String), + UnknownScheme(String), + DnsFailed(String), + ConnectionFailed(String), + TooManyRedirects, + BadStatus, + BadHeader, + Io(IoError), +} + +impl Error { + pub fn status(&self) -> u16 { + match self { + Error::BadUrl(_) => 400, + Error::UnknownScheme(_) => 400, + Error::DnsFailed(_) => 400, + Error::ConnectionFailed(_) => 500, + Error::TooManyRedirects => 400, + Error::BadStatus => 500, + Error::BadHeader => 500, + Error::Io(_) => 500, + } + } + pub fn status_text(&self) -> &str { + match self { + Error::BadUrl(e) => { + println!("{}", e); + "Bad URL" + }, + Error::UnknownScheme(_) => "Unknown Scheme", + Error::DnsFailed(_) => "Dns Failed", + Error::ConnectionFailed(_) => "Connection Failed", + Error::TooManyRedirects => "Too Many Redirects", + Error::BadStatus => "Bad Status", + Error::BadHeader => "Bad Header", + Error::Io(_) => "Network Error", + } + } + pub fn body_text(&self) -> String { + match self { + Error::BadUrl(url) => format!("Bad URL: {}", url), + Error::UnknownScheme(scheme) => format!("Unknown Scheme: {}", scheme), + Error::DnsFailed(err) => format!("Dns Failed: {}", err), + Error::ConnectionFailed(err) => format!("Connection Failed: {}", err), + Error::TooManyRedirects => "Too Many Redirects".to_string(), + Error::BadStatus => "Bad Status".to_string(), + Error::BadHeader => "Bad Header".to_string(), + Error::Io(ioe) => format!("Network Error: {}", ioe), + } + } +} + +impl From for Error { + fn from(err: IoError) -> Error { + Error::Io(err) + } +} diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 0000000..81c2900 --- /dev/null +++ b/src/header.rs @@ -0,0 +1,58 @@ +use ascii::AsciiString; +use error::Error; +use std::str::FromStr; + +#[derive(Debug, Clone)] +/// Wrapper type for a header line. +pub struct Header { + line: AsciiString, + index: usize, +} + +impl Header { + /// The header name. + /// + /// ``` + /// let header = "X-Forwarded-For: 127.0.0.1".parse::().unwrap(); + /// assert_eq!("X-Forwarded-For", header.name()); + /// ``` + pub fn name(&self) -> &str { + &self.line.as_str()[0..self.index] + } + + /// The header value. + /// + /// ``` + /// let header = "X-Forwarded-For: 127.0.0.1".parse::().unwrap(); + /// assert_eq!("127.0.0.1", header.value()); + /// ``` + pub fn value(&self) -> &str { + &self.line.as_str()[self.index + 1..].trim() + } + + /// Compares the given str to the header name ignoring case. + /// + /// ``` + /// let header = "X-Forwarded-For: 127.0.0.1".parse::().unwrap(); + /// assert!(header.is_name("x-forwarded-for")); + /// ``` + pub fn is_name(&self, other: &str) -> bool { + self.name().eq_ignore_ascii_case(other) + } +} + +impl FromStr for Header { + type Err = Error; + fn from_str(s: &str) -> Result { + // + let line = AsciiString::from_str(s).map_err(|_| Error::BadHeader)?; + let index = s.find(":").ok_or_else(|| Error::BadHeader)?; + + // no value? + if index >= s.len() { + return Err(Error::BadHeader); + } + + Ok(Header { line, index }) + } +} diff --git a/src/lib.rs b/src/lib.rs index 31e1bb2..ccf85f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,144 @@ +extern crate ascii; +extern crate base64; +extern crate chrono; +extern crate chunked_transfer; +extern crate dns_lookup; +extern crate encoding; +#[macro_use] +extern crate lazy_static; +extern crate mime_guess; +extern crate qstring; +extern crate rustls; +extern crate serde_json; +extern crate url; +extern crate webpki; +extern crate webpki_roots; + +#[macro_use] +mod agent; +mod error; +mod header; +mod stream; +mod util; + +pub use agent::{Agent, Request, Response}; +pub use header::Header; + +// re-export +pub use serde_json::{to_value, Map, Value}; + +/// Agents keep state between requests. +/// +/// By default, no state, such as cookies, is kept between requests. +/// But by creating an agent as entry point for the request, we +/// can keep state. +/// +/// ``` +/// let agent = ureq::agent(); +/// +/// let auth = agent +/// .post("/login") +/// .auth("martin", "rubbermashgum") +/// .call(); // blocks. puts auth cookies in agent. +/// +/// if !auth.ok() { +/// println!("Noes!"); +/// } +/// +/// let secret = agent +/// .get("/my-protected-page") +/// .call(); // blocks and waits for request. +/// +/// if !secret.ok() { +/// println!("Wot?!"); +/// } +/// +/// println!("Secret is: {}", secret.into_string().unwrap()); +/// ``` +pub fn agent() -> Agent { + Agent::new() +} + +pub fn request(method: M, path: S) -> Request +where + M: Into, + S: Into, +{ + Agent::new().request(method, path) +} + +pub fn get(path: S) -> Request +where + S: Into, +{ + request("GET", path) +} +pub fn head(path: S) -> Request +where + S: Into, +{ + request("HEAD", path) +} +pub fn post(path: S) -> Request +where + S: Into, +{ + request("POST", path) +} +pub fn put(path: S) -> Request +where + S: Into, +{ + request("PUT", path) +} +pub fn delete(path: S) -> Request +where + S: Into, +{ + request("DELETE", path) +} +pub fn trace(path: S) -> Request +where + S: Into, +{ + request("TRACE", path) +} +pub fn options(path: S) -> Request +where + S: Into, +{ + request("OPTIONS", path) +} +pub fn connect(path: S) -> Request +where + S: Into, +{ + request("CONNECT", path) +} +pub fn patch(path: S) -> Request +where + S: Into, +{ + request("PATCH", path) +} + #[cfg(test)] mod tests { + use super::*; + #[test] - fn it_works() { - assert_eq!(2 + 2, 4); + fn connect_http_google() { + let resp = get("http://www.google.com/").call(); + println!("{:?}", resp); + assert_eq!("text/html; charset=ISO-8859-1", resp.get("content-type").unwrap()); + assert_eq!("text/html", resp.content_type()); + } + + #[test] + fn connect_https_google() { + let resp = get("https://www.google.com/").call(); + println!("{:?}", resp); + assert_eq!("text/html; charset=ISO-8859-1", resp.get("content-type").unwrap()); + assert_eq!("text/html", resp.content_type()); } } diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000..774829e --- /dev/null +++ b/src/request.rs @@ -0,0 +1,513 @@ +use qstring::QString; +use serde_json; +use std::sync::Arc; + +lazy_static! { + static ref URL_BASE: Url = { Url::parse("http://localhost/").expect("Failed to parse URL_BASE") }; +} + +#[derive(Clone, Default)] +pub struct Request { + pool: Arc>>, + + // via agent + method: String, + path: String, + + // from request itself + headers: Vec
, + auth: Option<(String, String)>, + query: QString, + timeout: u32, + timeout_read: u32, + timeout_write: u32, + redirects: u32, +} + +enum Payload { + Empty, + Text(String), + JSON(serde_json::Value), + Reader(Box), +} + +impl Default for Payload { + fn default() -> Payload { + Payload::Empty + } +} + +impl Payload { + fn into_read(self) -> (Option, Box) { + match self { + Payload::Empty => (Some(0), Box::new(VecRead::from_str(""))), + Payload::Text(s) => { + let read = VecRead::from_str(&s); + (Some(read.len()), Box::new(read)) + } + Payload::JSON(v) => { + let vec = serde_json::to_vec(&v).expect("Bad JSON in payload"); + let read = VecRead::from_vec(vec); + (Some(read.len()), Box::new(read)) + } + Payload::Reader(read) => (None, read), + } + } +} + +impl Request { + fn new(agent: &Agent, method: String, path: String) -> Request { + Request { + pool: Arc::clone(&agent.pool), + method, + path, + headers: agent.headers.clone(), + auth: agent.auth.clone(), + redirects: 5, + ..Default::default() + } + } + + /// "Builds" this request which is effectively the same as cloning. + /// This is needed when we use a chain of request builders, but + /// don't want to send the request at the end of the chain. + /// + /// ``` + /// let r = ureq::get("/my_page") + /// .set("X-Foo-Bar", "Baz") + /// .build(); + /// ``` + pub fn build(&self) -> Request { + self.clone() + } + + /// Executes the request and blocks the caller until done. + /// + /// Use `.timeout()` and `.timeout_read()` to avoid blocking forever. + /// + /// ``` + /// let r = ureq::get("/my_page") + /// .timeout(10_000) // max 10 seconds + /// .call(); + /// + /// println!("{:?}", r); + /// ``` + pub fn call(&self) -> Response { + self.do_call(Payload::Empty) + } + + fn do_call(&self, payload: Payload) -> Response { + let mut lock = self.pool.lock().unwrap(); + self.to_url() + .and_then(|url| { + if lock.is_none() { + // create a one off pool. + ConnectionPool::new().connect(self, &self.method, &url, self.redirects, payload) + } else { + // reuse connection pool. + lock.as_mut().unwrap().connect( + self, + &self.method, + &url, + self.redirects, + payload, + ) + } + }) + .unwrap_or_else(|e| e.into()) + } + + /// Send data a json value. + /// + /// ``` + /// #[macro_use] + /// extern crate ureq; + /// + /// fn main() { + /// let r = ureq::post("/my_page") + /// .send_json(json!({ "name": "martin", "rust": true })); + /// println!("{:?}", r); + /// } + /// ``` + pub fn send_json(&self, data: serde_json::Value) -> Response { + self.do_call(Payload::JSON(data)) + } + + /// Send data as a string. + /// + /// ``` + /// let r = ureq::post("/my_page") + /// .content_type("text/plain") + /// .send_str("Hello World!"); + /// println!("{:?}", r); + /// ``` + pub fn send_str(&self, data: S) -> Response + where + S: Into, + { + let text = data.into(); + self.do_call(Payload::Text(text)) + } + + /// Send data from a reader. + /// + /// + /// + pub fn send(&self, reader: R) -> Response + where + R: Read + Send + 'static, + { + self.do_call(Payload::Reader(Box::new(reader))) + } + + /// Set a header field. + /// + /// ``` + /// let r = ureq::get("/my_page") + /// .set("X-API-Key", "foobar") + /// .set("Accept", "application/json") + /// .call(); + /// + /// if r.ok() { + /// println!("yay got {}", r.into_json().unwrap()); + /// } else { + /// println!("Oh no error!"); + /// } + /// ``` + pub fn set(&mut self, header: K, value: V) -> &mut Request + where + K: Into, + V: Into, + { + add_request_header(self, header.into(), value.into()); + self + } + + /// Returns the value for a set header. + /// + /// ``` + /// let req = ureq::get("/my_page") + /// .set("X-API-Key", "foobar") + /// .build(); + /// assert_eq!("foobar", req.get("x-api-Key").unwrap()); + /// ``` + pub fn get<'a>(&self, name: &'a str) -> Option<&str> { + self.headers + .iter() + .find(|h| h.is_name(name)) + .map(|h| h.value()) + } + + /// Tells if the header has been set. + /// + /// ``` + /// let req = ureq::get("/my_page") + /// .set("X-API-Key", "foobar") + /// .build(); + /// assert_eq!(true, req.has("x-api-Key")); + /// ``` + pub fn has<'a>(&self, name: &'a str) -> bool { + self.get(name).is_some() + } + + /// Set many headers. + /// + /// ``` + /// #[macro_use] + /// extern crate ureq; + /// + /// fn main() { + /// let r = ureq::get("/my_page") + /// .set_map(map!{ + /// "X-API-Key" => "foobar", + /// "Accept" => "application/json" + /// }) + /// .call(); + /// + /// if r.ok() { + /// println!("yay got {}", r.into_json().unwrap()); + /// } + /// } + /// ``` + pub fn set_map(&mut self, headers: I) -> &mut Request + where + K: Into, + V: Into, + I: IntoIterator, + { + for (k, v) in headers.into_iter() { + add_request_header(self, k.into(), v.into()); + } + self + } + + /// Set a query parameter. + /// + /// For example, to set `?format=json&dest=/login` + /// + /// ``` + /// let r = ureq::get("/my_page") + /// .query("format", "json") + /// .query("dest", "/login") + /// .call(); + /// + /// println!("{:?}", r); + /// ``` + pub fn query(&mut self, param: K, value: V) -> &mut Request + where + K: Into, + V: Into, + { + self.query.add_pair((param.into(), value.into())); + self + } + + /// Set many query parameters. + /// + /// For example, to set `?format=json&dest=/login` + /// + /// ``` + /// #[macro_use] + /// extern crate ureq; + /// + /// fn main() { + /// let r = ureq::get("/my_page") + /// .query_map(map!{ + /// "format" => "json", + /// "dest" => "/login" + /// }) + /// .call(); + /// + /// println!("{:?}", r); + /// } + /// ``` + pub fn query_map(&mut self, params: I) -> &mut Request + where + K: Into, + V: Into, + I: IntoIterator, + { + for (k, v) in params.into_iter() { + self.query.add_pair((k.into(), v.into())); + } + self + } + + /// Set query parameters as a string. + /// + /// For example, to set `?format=json&dest=/login` + /// + /// ``` + /// let r = ureq::get("/my_page") + /// .query_str("?format=json&dest=/login") + /// .call(); + /// println!("{:?}", r); + /// ``` + pub fn query_str(&mut self, query: S) -> &mut Request + where + S: Into, + { + let s = query.into(); + self.query.add_str(&s); + self + } + + /// Set the `Content-Type` header. + /// + /// The default is `application/json`. + /// + /// As a short-hand the `.content_type()` method accepts the + /// canonicalized MIME type name complete with + /// type/subtype, or simply the extension name such as + /// "xml", "json", "png". + /// + /// These are all the same. + /// + /// ``` + /// ureq::post("/my_page") + /// .set("Content-Type", "text/plain") + /// .call(); + /// + /// ureq::post("/my_page") + /// .content_type("text/plain") + /// .call(); + /// + /// ureq::post("/my_page") + /// .content_type("txt") + /// .call(); + /// ``` + pub fn content_type(&mut self, c: S) -> &mut Request + where + S: Into, + { + self.set("Content-Type", mime_of(c)) + } + + /// Sets the `Accept` header in the same way as `content_type()`. + /// + /// The short-hand `.accept()` method accepts the + /// canonicalized MIME type name complete with + /// type/subtype, or simply the extension name such as + /// "xml", "json", "png". + /// + /// These are all the same. + /// + /// ``` + /// ureq::get("/my_page") + /// .set("Accept", "text/plain") + /// .call(); + /// + /// ureq::get("/my_page") + /// .accept("text/plain") + /// .call(); + /// + /// ureq::get("/my_page") + /// .accept("txt") + /// .call(); + /// ``` + pub fn accept(&mut self, accept: S) -> &mut Request + where + S: Into, + { + self.set("Accept", mime_of(accept)) + } + + /// Timeout for the socket connection to be successful. + /// + /// The default is `0`, which means a request can block forever. + /// + /// ``` + /// let r = ureq::get("/my_page") + /// .timeout(1_000) // wait max 1 second to connect + /// .call(); + /// println!("{:?}", r); + /// ``` + pub fn timeout(&mut self, millis: u32) -> &mut Request { + self.timeout = millis; + self + } + + /// Timeout for the individual reads of the socket. + /// + /// The default is `0`, which means it can block forever. + /// + /// ``` + /// let r = ureq::get("/my_page") + /// .timeout_read(1_000) // wait max 1 second for the read + /// .call(); + /// println!("{:?}", r); + /// ``` + pub fn timeout_read(&mut self, millis: u32) -> &mut Request { + self.timeout_read = millis; + self + } + + /// Timeout for the individual writes to the socket. + /// + /// The default is `0`, which means it can block forever. + /// + /// ``` + /// let r = ureq::get("/my_page") + /// .timeout_write(1_000) // wait max 1 second for sending. + /// .call(); + /// println!("{:?}", r); + /// ``` + pub fn timeout_write(&mut self, millis: u32) -> &mut Request { + self.timeout_write = millis; + self + } + + /// Basic auth. + /// + /// These are the same + /// + /// ``` + /// let r1 = ureq::get("http://localhost/my_page") + /// .auth("martin", "rubbermashgum") + /// .call(); + /// println!("{:?}", r1); + /// + /// let r2 = ureq::get("http://martin:rubbermashgum@localhost/my_page").call(); + /// println!("{:?}", r2); + /// ``` + pub fn auth(&mut self, user: S, pass: T) -> &mut Request + where + S: Into, + T: Into, + { + let u = user.into(); + let p = pass.into(); + let pass = basic_auth(&u, &p); + self.auth_kind("Basic", pass) + } + + /// Auth of other kinds such as `Digest`, `Token` etc. + /// + /// ``` + /// let r = ureq::get("http://localhost/my_page") + /// .auth_kind("token", "secret") + /// .call(); + /// println!("{:?}", r); + /// ``` + pub fn auth_kind(&mut self, kind: S, pass: T) -> &mut Request + where + S: Into, + T: Into, + { + self.auth = Some((kind.into(), pass.into())); + self + } + + /// How many redirects to follow. + /// + /// Defaults to `5`. + /// + /// ``` + /// let r = ureq::get("/my_page") + /// .redirects(10) + /// .call(); + /// println!("{:?}", r); + /// ``` + pub fn redirects(&mut self, n: u32) -> &mut Request { + self.redirects = n; + self + } + + // pub fn retry(&self, times: u16) -> Request { + // unimplemented!() + // } + // pub fn sortQuery(&self) -> Request { + // unimplemented!() + // } + // pub fn sortQueryBy(&self, by: Box usize>) -> Request { + // unimplemented!() + // } + // pub fn ca(&self, accept: S) -> Request + // where S: Into { + // unimplemented!() + // } + // pub fn cert(&self, accept: S) -> Request + // where S: Into { + // unimplemented!() + // } + // pub fn key(&self, accept: S) -> Request + // where S: Into { + // unimplemented!() + // } + // pub fn pfx(&self, accept: S) -> Request // TODO what type? u8? + // where S: Into { + // unimplemented!() + // } + + fn to_url(&self) -> Result { + URL_BASE + .join(&self.path) + .map_err(|e| Error::BadUrl(format!("{}", e))) + } +} + +fn add_request_header(request: &mut Request, k: String, v: String) { + if let Ok(h) = Header::from_str(&format!("{}: {}", k, v)) { + request.headers.push(h) + } +} diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..a2c8e5d --- /dev/null +++ b/src/response.rs @@ -0,0 +1,277 @@ +use ascii::AsciiString; +use chunked_transfer; +use encoding::label::encoding_from_whatwg_label; +use encoding::DecoderTrap; +use std::io::Error as IoError; +use std::io::ErrorKind; +use std::io::Read; +use std::io::Result as IoResult; + +use error::Error; + +const DEFAULT_CONTENT_TYPE: &'static str = "text/plain"; +const DEFAULT_CHARACTER_SET: &'static str = "utf-8"; + +// buffered +// "text/", "/json", or "x-www-form-urlencoded" +pub struct Response { + status_line: AsciiString, + index: (usize, usize), // index into status_line where we split: HTTP/1.1 200 OK + status: u16, + headers: Vec
, + reader: Option>, +} + +impl ::std::fmt::Debug for Response { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::result::Result<(), ::std::fmt::Error> { + write!( + f, + "Response[status: {}, status_text: {}]", + self.status(), + self.status_text() + ) + } +} + +impl Response { + /// The entire status line like: HTTP/1.1 200 OK + pub fn status_line(&self) -> &str { + self.status_line.as_str() + } + + /// The http version: HTTP/1.1 + pub fn http_version(&self) -> &str { + &self.status_line.as_str()[0..self.index.0] + } + + /// The status as a u16: 200 + pub fn status(&self) -> &u16 { + &self.status + } + + /// The status text: OK + pub fn status_text(&self) -> &str { + &self.status_line.as_str()[self.index.1 + 1..].trim() + } + + /// The header corresponding header value for the give name, if any. + pub fn get<'a>(&self, name: &'a str) -> Option<&str> { + self.headers + .iter() + .find(|h| h.is_name(name)) + .map(|h| h.value()) + } + + /// Tells if the response has the named header. + pub fn has<'a>(&self, name: &'a str) -> bool { + self.get(name).is_some() + } + + /// All headers corresponding values for the give name, or empty vector. + pub fn get_all<'a>(&self, name: &'a str) -> Vec<&str> { + self.headers + .iter() + .filter(|h| h.is_name(name)) + .map(|h| h.value()) + .collect() + } + + /// Whether the response status is: 200 <= status <= 299 + pub fn ok(&self) -> bool { + self.status >= 200 && self.status <= 299 + } + + pub fn redirect(&self) -> bool { + self.status >= 300 && self.status <= 399 + } + + /// Whether the response status is: 400 <= status <= 499 + pub fn client_error(&self) -> bool { + self.status >= 400 && self.status <= 499 + } + + /// Whether the response status is: 500 <= status <= 599 + pub fn server_error(&self) -> bool { + self.status >= 500 && self.status <= 599 + } + + /// Whether the response status is: 400 <= status <= 599 + pub fn error(&self) -> bool { + self.client_error() || self.server_error() + } + + /// The content type part of the "Content-Type" header without + /// the charset. + /// + /// Example: + /// + /// ``` + /// let resp = ureq::get("https://www.google.com/").call(); + /// assert_eq!("text/html; charset=ISO-8859-1", resp.get("content-type").unwrap()); + /// assert_eq!("text/html", resp.content_type()); + /// ``` + pub fn content_type(&self) -> &str { + self.get("content-type") + .map(|header| { + header + .find(";") + .map(|index| &header[0..index]) + .unwrap_or(header) + }) + .unwrap_or(DEFAULT_CONTENT_TYPE) + } + pub fn charset(&self) -> &str { + self.get("content-type") + .and_then(|header| { + header.find(";").and_then(|semi| { + (&header[semi + 1..]) + .find("=") + .map(|equal| (&header[semi + equal + 2..]).trim()) + }) + }) + .unwrap_or(DEFAULT_CHARACTER_SET) + } + + pub fn into_reader(self) -> impl Read { + let is_chunked = self.get("transfer-encoding") + .map(|enc| enc.eq_ignore_ascii_case("chunked")) + .unwrap_or(false); + let reader = self.reader.expect("No reader in response?!"); + match is_chunked { + true => Box::new(chunked_transfer::Decoder::new(reader)), + false => reader, + } + } + + pub fn into_string(self) -> IoResult { + let encoding = encoding_from_whatwg_label(self.charset()) + .or_else(|| encoding_from_whatwg_label(DEFAULT_CHARACTER_SET)) + .unwrap(); + let mut buf: Vec = vec![]; + self.into_reader().read_to_end(&mut buf)?; + Ok(encoding.decode(&buf, DecoderTrap::Replace).unwrap()) + } + + pub fn into_json(self) -> IoResult { + let reader = self.into_reader(); + serde_json::from_reader(reader).map_err(|e| { + IoError::new( + ErrorKind::InvalidData, + format!("Failed to read JSON: {}", e), + ) + }) + } + + pub fn new(status: u16, status_text: &str, body: &str) -> Self { + let r = format!("HTTP/1.1 {} {}\r\n\r\n{}\n", status, status_text, body); + (r.as_ref() as &str) + .parse::() + .unwrap_or_else(|e| e.into()) + } + + pub fn from_read(reader: impl Read) -> Self + { + Self::do_from_read(reader).unwrap_or_else(|e| e.into()) + } + + fn do_from_read(mut reader: impl Read) -> Result + { + // + // HTTP/1.1 200 OK\r\n + let status_line = read_next_line(&mut reader).map_err(|_| Error::BadStatus)?; + + let (index, status) = parse_status_line(status_line.as_str())?; + + let mut headers: Vec
= Vec::new(); + loop { + let line = read_next_line(&mut reader).map_err(|_| Error::BadHeader)?; + if line.len() == 0 { + break; + } + if let Ok(header) = line.as_str().parse::
() { + headers.push(header); + } + } + + Ok(Response { + status_line, + index, + status, + headers, + reader: None, + }) + } + + fn set_reader(&mut self, reader: R) where R: Read + Send + 'static { + self.reader = Some(Box::new(reader)); + } + +} + +fn parse_status_line(line: &str) -> Result<((usize, usize), u16), Error> { + // HTTP/1.1 200 OK\r\n + let mut split = line.splitn(3, ' '); + + let http_version = split.next().ok_or_else(|| Error::BadStatus)?; + if http_version.len() < 5 { + return Err(Error::BadStatus); + } + let index1 = http_version.len(); + + let status = split.next().ok_or_else(|| Error::BadStatus)?; + if status.len() < 3 { + return Err(Error::BadStatus); + } + let index2 = index1 + status.len(); + + let status = status.parse::().map_err(|_| Error::BadStatus)?; + + let status_text = split.next().ok_or_else(|| Error::BadStatus)?; + if status_text.len() == 0 { + return Err(Error::BadStatus); + } + + Ok(((index1, index2), status)) +} + +impl FromStr for Response { + type Err = Error; + fn from_str(s: &str) -> Result { + let mut read = VecRead::from_str(s); + let mut resp = Self::do_from_read(&mut read)?; + resp.set_reader(read); + Ok(resp) + } +} + +impl Into for Error { + fn into(self) -> Response { + Response::new(self.status(), self.status_text(), &self.body_text()) + } +} + +// application/x-www-form-urlencoded, application/json, and multipart/form-data + +fn read_next_line(reader: &mut R) -> IoResult { + let mut buf = Vec::new(); + let mut prev_byte_was_cr = false; + + loop { + let byte = reader.bytes().next(); + + let byte = match byte { + Some(b) => try!(b), + None => return Err(IoError::new(ErrorKind::ConnectionAborted, "Unexpected EOF")), + }; + + if byte == b'\n' && prev_byte_was_cr { + buf.pop(); // removing the '\r' + return AsciiString::from_ascii(buf) + .map_err(|_| IoError::new(ErrorKind::InvalidInput, "Header is not in ASCII")); + } + + prev_byte_was_cr = byte == b'\r'; + + buf.push(byte); + } +} diff --git a/src/stream.rs b/src/stream.rs new file mode 100644 index 0000000..30b46bd --- /dev/null +++ b/src/stream.rs @@ -0,0 +1,34 @@ +use rustls; +use std::io::Read; +use std::io::Result; +use std::io::Write; +use std::net::TcpStream; + +pub enum Stream { + Http(TcpStream), + Https(rustls::ClientSession, TcpStream), +} + +impl Read for Stream { + fn read(&mut self, buf: &mut [u8]) -> Result { + match self { + Stream::Http(sock) => sock.read(buf), + Stream::Https(sess, sock) => rustls::Stream::new(sess, sock).read(buf), + } + } +} + +impl Write for Stream { + fn write(&mut self, buf: &[u8]) -> Result { + match self { + Stream::Http(sock) => sock.write(buf), + Stream::Https(sess, sock) => rustls::Stream::new(sess, sock).write(buf), + } + } + fn flush(&mut self) -> Result<()> { + match self { + Stream::Http(sock) => sock.flush(), + Stream::Https(sess, sock) => rustls::Stream::new(sess, sock).flush(), + } + } +} diff --git a/src/util/macros.rs b/src/util/macros.rs new file mode 100644 index 0000000..cd33913 --- /dev/null +++ b/src/util/macros.rs @@ -0,0 +1,10 @@ +#[macro_export] +macro_rules! map( + { $($key:expr => $value:expr),* } => { + { + let mut m = ::std::collections::HashMap::new(); + $(m.insert($key, $value);)+ + m + } + }; +); diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..c69aa90 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,29 @@ +#[allow(dead_code)] +mod macros; +mod serde_macros; +mod vecread; + +use base64; +use mime_guess::get_mime_type_str; + +pub use util::vecread::VecRead; + +pub fn basic_auth(user: &str, pass: &str) -> String { + let safe = match user.find(":") { + Some(idx) => &user[..idx], + None => user, + }; + base64::encode(&format!("{}:{}", safe, pass)) +} + +pub fn mime_of>(s: S) -> String { + let s = s.into(); + match &s[..] { + "json" => "application/json", + "form" => "application/x-www-form-urlencoded", + _ => match get_mime_type_str(&s) { + Some(mime) => mime, + None => "foo", + }, + }.to_string() +} diff --git a/src/util/serde_macros.rs b/src/util/serde_macros.rs new file mode 100644 index 0000000..dcab2ac --- /dev/null +++ b/src/util/serde_macros.rs @@ -0,0 +1,286 @@ + +// this file is borrowed in its entirety until macro_reexport stabilizes. +// https://github.com/rust-lang/rust/issues/29638 + +// Copyright 2017 Serde Developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +/// Construct a `serde_json::Value` from a JSON literal. +/// +/// ```rust +/// # #[macro_use] +/// # extern crate serde_json; +/// # +/// # fn main() { +/// let value = json!({ +/// "code": 200, +/// "success": true, +/// "payload": { +/// "features": [ +/// "serde", +/// "json" +/// ] +/// } +/// }); +/// # } +/// ``` +/// +/// Variables or expressions can be interpolated into the JSON literal. Any type +/// interpolated into an array element or object value must implement Serde's +/// `Serialize` trait, while any type interpolated into a object key must +/// implement `Into`. If the `Serialize` implementation of the +/// interpolated type decides to fail, or if the interpolated type contains a +/// map with non-string keys, the `json!` macro will panic. +/// +/// ```rust +/// # #[macro_use] +/// # extern crate serde_json; +/// # +/// # fn main() { +/// let code = 200; +/// let features = vec!["serde", "json"]; +/// +/// let value = json!({ +/// "code": code, +/// "success": code == 200, +/// "payload": { +/// features[0]: features[1] +/// } +/// }); +/// # } +/// ``` +/// +/// Trailing commas are allowed inside both arrays and objects. +/// +/// ```rust +/// # #[macro_use] +/// # extern crate serde_json; +/// # +/// # fn main() { +/// let value = json!([ +/// "notice", +/// "the", +/// "trailing", +/// "comma -->", +/// ]); +/// # } +/// ``` +#[macro_export] +macro_rules! json { + // Hide distracting implementation details from the generated rustdoc. + ($($json:tt)+) => { + json_internal!($($json)+) + }; +} + +// Rocket relies on this because they export their own `json!` with a different +// doc comment than ours, and various Rust bugs prevent them from calling our +// `json!` from their `json!` so they call `json_internal!` directly. Check with +// @SergioBenitez before making breaking changes to this macro. +// +// Changes are fine as long as `json_internal!` does not call any new helper +// macros and can still be invoked as `json_internal!($($json)+)`. +#[macro_export] +#[doc(hidden)] +macro_rules! json_internal { + ////////////////////////////////////////////////////////////////////////// + // TT muncher for parsing the inside of an array [...]. Produces a vec![...] + // of the elements. + // + // Must be invoked as: json_internal!(@array [] $($tt)*) + ////////////////////////////////////////////////////////////////////////// + + // Done with trailing comma. + (@array [$($elems:expr,)*]) => { + vec![$($elems,)*] + }; + + // Done without trailing comma. + (@array [$($elems:expr),*]) => { + vec![$($elems),*] + }; + + // Next element is `null`. + (@array [$($elems:expr,)*] null $($rest:tt)*) => { + json_internal!(@array [$($elems,)* json_internal!(null)] $($rest)*) + }; + + // Next element is `true`. + (@array [$($elems:expr,)*] true $($rest:tt)*) => { + json_internal!(@array [$($elems,)* json_internal!(true)] $($rest)*) + }; + + // Next element is `false`. + (@array [$($elems:expr,)*] false $($rest:tt)*) => { + json_internal!(@array [$($elems,)* json_internal!(false)] $($rest)*) + }; + + // Next element is an array. + (@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => { + json_internal!(@array [$($elems,)* json_internal!([$($array)*])] $($rest)*) + }; + + // Next element is a map. + (@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => { + json_internal!(@array [$($elems,)* json_internal!({$($map)*})] $($rest)*) + }; + + // Next element is an expression followed by comma. + (@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => { + json_internal!(@array [$($elems,)* json_internal!($next),] $($rest)*) + }; + + // Last element is an expression with no trailing comma. + (@array [$($elems:expr,)*] $last:expr) => { + json_internal!(@array [$($elems,)* json_internal!($last)]) + }; + + // Comma after the most recent element. + (@array [$($elems:expr),*] , $($rest:tt)*) => { + json_internal!(@array [$($elems,)*] $($rest)*) + }; + + ////////////////////////////////////////////////////////////////////////// + // TT muncher for parsing the inside of an object {...}. Each entry is + // inserted into the given map variable. + // + // Must be invoked as: json_internal!(@object $map () ($($tt)*) ($($tt)*)) + // + // We require two copies of the input tokens so that we can match on one + // copy and trigger errors on the other copy. + ////////////////////////////////////////////////////////////////////////// + + // Done. + (@object $object:ident () () ()) => {}; + + // Insert the current entry followed by trailing comma. + (@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => { + $object.insert(($($key)+).into(), $value); + json_internal!(@object $object () ($($rest)*) ($($rest)*)); + }; + + // Insert the last entry without trailing comma. + (@object $object:ident [$($key:tt)+] ($value:expr)) => { + $object.insert(($($key)+).into(), $value); + }; + + // Next value is `null`. + (@object $object:ident ($($key:tt)+) (: null $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!(null)) $($rest)*); + }; + + // Next value is `true`. + (@object $object:ident ($($key:tt)+) (: true $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!(true)) $($rest)*); + }; + + // Next value is `false`. + (@object $object:ident ($($key:tt)+) (: false $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!(false)) $($rest)*); + }; + + // Next value is an array. + (@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!([$($array)*])) $($rest)*); + }; + + // Next value is a map. + (@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!({$($map)*})) $($rest)*); + }; + + // Next value is an expression followed by comma. + (@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!($value)) , $($rest)*); + }; + + // Last value is an expression with no trailing comma. + (@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => { + json_internal!(@object $object [$($key)+] (json_internal!($value))); + }; + + // Missing value for last entry. Trigger a reasonable error message. + (@object $object:ident ($($key:tt)+) (:) $copy:tt) => { + // "unexpected end of macro invocation" + json_internal!(); + }; + + // Missing colon and value for last entry. Trigger a reasonable error + // message. + (@object $object:ident ($($key:tt)+) () $copy:tt) => { + // "unexpected end of macro invocation" + json_internal!(); + }; + + // Misplaced colon. Trigger a reasonable error message. + (@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => { + // Takes no arguments so "no rules expected the token `:`". + unimplemented!($colon); + }; + + // Found a comma inside a key. Trigger a reasonable error message. + (@object $object:ident ($($key:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => { + // Takes no arguments so "no rules expected the token `,`". + unimplemented!($comma); + }; + + // Key is fully parenthesized. This avoids clippy double_parens false + // positives because the parenthesization may be necessary here. + (@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*)); + }; + + // Munch a token into the current key. + (@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => { + json_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*)); + }; + + ////////////////////////////////////////////////////////////////////////// + // The main implementation. + // + // Must be invoked as: json_internal!($($json)+) + ////////////////////////////////////////////////////////////////////////// + + (null) => { + $crate::Value::Null + }; + + (true) => { + $crate::Value::Bool(true) + }; + + (false) => { + $crate::Value::Bool(false) + }; + + ([]) => { + $crate::Value::Array(vec![]) + }; + + ([ $($tt:tt)+ ]) => { + $crate::Value::Array(json_internal!(@array [] $($tt)+)) + }; + + ({}) => { + $crate::Value::Object($crate::Map::new()) + }; + + ({ $($tt:tt)+ }) => { + $crate::Value::Object({ + let mut object = $crate::Map::new(); + json_internal!(@object object () ($($tt)+) ($($tt)+)); + object + }) + }; + + // Any Serialize type: numbers, strings, struct literals, variables etc. + // Must be below every other rule. + ($other:expr) => { + $crate::to_value(&$other).unwrap() + }; +} diff --git a/src/util/vecread.rs b/src/util/vecread.rs new file mode 100644 index 0000000..1cc8def --- /dev/null +++ b/src/util/vecread.rs @@ -0,0 +1,34 @@ +use std::io::Read; +use std::io::Result; + +pub struct VecRead { + bytes: Vec, + index: usize, +} + +impl VecRead { + pub fn new(bytes: &[u8]) -> Self { + Self::from_vec(bytes.to_owned()) + } + pub fn from_vec(bytes: Vec) -> Self { + VecRead { + bytes, + index: 0, + } + } + pub fn from_str(s: &str) -> Self { + Self::new(s.as_bytes()) + } + pub fn len(&self) -> usize { + self.bytes.len() + } +} + +impl Read for VecRead { + fn read(&mut self, buf: &mut [u8]) -> Result { + let len = buf.len().min(self.bytes.len() - self.index); + (&mut buf[0..len]).copy_from_slice(&self.bytes[self.index..self.index + len]); + self.index += len; + Ok(len) + } +}