initial commit
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
target
|
target
|
||||||
|
rls
|
||||||
|
|||||||
512
Cargo.lock
generated
512
Cargo.lock
generated
@@ -1,4 +1,512 @@
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "superagent"
|
name = "ascii"
|
||||||
version = "0.1.0"
|
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"
|
||||||
|
|||||||
16
Cargo.toml
16
Cargo.toml
@@ -1,6 +1,20 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "superagent"
|
name = "ureq"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Martin Algesten <martin@algesten.se>"]
|
authors = ["Martin Algesten <martin@algesten.se>"]
|
||||||
|
|
||||||
[dependencies]
|
[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"
|
||||||
|
|||||||
315
src/agent.rs
Normal file
315
src/agent.rs
Normal file
@@ -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<Header>,
|
||||||
|
pub auth: Option<(String, String)>,
|
||||||
|
pub pool: Arc<Mutex<Option<ConnectionPool>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<K, V>(&mut self, header: K, value: V) -> &mut Agent
|
||||||
|
where
|
||||||
|
K: Into<String>,
|
||||||
|
V: Into<String>,
|
||||||
|
{
|
||||||
|
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<K, V, I>(&mut self, headers: I) -> &mut Agent
|
||||||
|
where
|
||||||
|
K: Into<String>,
|
||||||
|
V: Into<String>,
|
||||||
|
I: IntoIterator<Item = (K, V)>,
|
||||||
|
{
|
||||||
|
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<S, T>(&mut self, user: S, pass: T) -> &mut Agent
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
T: Into<String>,
|
||||||
|
{
|
||||||
|
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<S, T>(&mut self, kind: S, pass: T) -> &mut Agent
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
T: Into<String>,
|
||||||
|
{
|
||||||
|
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<M, S>(&self, method: M, path: S) -> Request
|
||||||
|
where
|
||||||
|
M: Into<String>,
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
Request::new(&self, method.into(), path.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<S>(&self, path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
self.request("GET", path)
|
||||||
|
}
|
||||||
|
pub fn head<S>(&self, path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
self.request("HEAD", path)
|
||||||
|
}
|
||||||
|
pub fn post<S>(&self, path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
self.request("POST", path)
|
||||||
|
}
|
||||||
|
pub fn put<S>(&self, path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
self.request("PUT", path)
|
||||||
|
}
|
||||||
|
pub fn delete<S>(&self, path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
self.request("DELETE", path)
|
||||||
|
}
|
||||||
|
pub fn trace<S>(&self, path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
self.request("TRACE", path)
|
||||||
|
}
|
||||||
|
pub fn options<S>(&self, path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
self.request("OPTIONS", path)
|
||||||
|
}
|
||||||
|
pub fn connect<S>(&self, path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
self.request("CONNECT", path)
|
||||||
|
}
|
||||||
|
pub fn patch<S>(&self, path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
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::<Response>().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::<Response>().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::<Response>().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::<Response>().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::<Response>().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::<Response>().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::<Response>().unwrap();
|
||||||
|
let v = resp.into_json().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
v,
|
||||||
|
"{\"hello\":\"world\"}"
|
||||||
|
.parse::<serde_json::Value>()
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_borked_header() {
|
||||||
|
let s = format!("HTTP/1.1 BORKED\r\n");
|
||||||
|
let resp: Response = s.parse::<Response>().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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
193
src/conn.rs
Normal file
193
src/conn.rs
Normal file
@@ -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<Response, Error> {
|
||||||
|
//
|
||||||
|
// 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<u8> = 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<Stream, Error> {
|
||||||
|
//
|
||||||
|
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<Stream, Error> {
|
||||||
|
//
|
||||||
|
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<TcpStream, Error> {
|
||||||
|
//
|
||||||
|
let ips: Vec<IpAddr> =
|
||||||
|
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::<usize>().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<R, W>(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(())
|
||||||
|
}
|
||||||
61
src/error.rs
Normal file
61
src/error.rs
Normal file
@@ -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<IoError> for Error {
|
||||||
|
fn from(err: IoError) -> Error {
|
||||||
|
Error::Io(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/header.rs
Normal file
58
src/header.rs
Normal file
@@ -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::<ureq::Header>().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::<ureq::Header>().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::<ureq::Header>().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<Self, Self::Err> {
|
||||||
|
//
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
141
src/lib.rs
141
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<M, S>(method: M, path: S) -> Request
|
||||||
|
where
|
||||||
|
M: Into<String>,
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
Agent::new().request(method, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<S>(path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
request("GET", path)
|
||||||
|
}
|
||||||
|
pub fn head<S>(path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
request("HEAD", path)
|
||||||
|
}
|
||||||
|
pub fn post<S>(path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
request("POST", path)
|
||||||
|
}
|
||||||
|
pub fn put<S>(path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
request("PUT", path)
|
||||||
|
}
|
||||||
|
pub fn delete<S>(path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
request("DELETE", path)
|
||||||
|
}
|
||||||
|
pub fn trace<S>(path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
request("TRACE", path)
|
||||||
|
}
|
||||||
|
pub fn options<S>(path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
request("OPTIONS", path)
|
||||||
|
}
|
||||||
|
pub fn connect<S>(path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
request("CONNECT", path)
|
||||||
|
}
|
||||||
|
pub fn patch<S>(path: S) -> Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
request("PATCH", path)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_works() {
|
fn connect_http_google() {
|
||||||
assert_eq!(2 + 2, 4);
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
513
src/request.rs
Normal file
513
src/request.rs
Normal file
@@ -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<Mutex<Option<ConnectionPool>>>,
|
||||||
|
|
||||||
|
// via agent
|
||||||
|
method: String,
|
||||||
|
path: String,
|
||||||
|
|
||||||
|
// from request itself
|
||||||
|
headers: Vec<Header>,
|
||||||
|
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<Read + 'static>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Payload {
|
||||||
|
fn default() -> Payload {
|
||||||
|
Payload::Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Payload {
|
||||||
|
fn into_read(self) -> (Option<usize>, Box<Read + 'static>) {
|
||||||
|
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<S>(&self, data: S) -> Response
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
let text = data.into();
|
||||||
|
self.do_call(Payload::Text(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send data from a reader.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
///
|
||||||
|
pub fn send<R>(&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<K, V>(&mut self, header: K, value: V) -> &mut Request
|
||||||
|
where
|
||||||
|
K: Into<String>,
|
||||||
|
V: Into<String>,
|
||||||
|
{
|
||||||
|
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<K, V, I>(&mut self, headers: I) -> &mut Request
|
||||||
|
where
|
||||||
|
K: Into<String>,
|
||||||
|
V: Into<String>,
|
||||||
|
I: IntoIterator<Item = (K, V)>,
|
||||||
|
{
|
||||||
|
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<K, V>(&mut self, param: K, value: V) -> &mut Request
|
||||||
|
where
|
||||||
|
K: Into<String>,
|
||||||
|
V: Into<String>,
|
||||||
|
{
|
||||||
|
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<K, V, I>(&mut self, params: I) -> &mut Request
|
||||||
|
where
|
||||||
|
K: Into<String>,
|
||||||
|
V: Into<String>,
|
||||||
|
I: IntoIterator<Item = (K, V)>,
|
||||||
|
{
|
||||||
|
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<S>(&mut self, query: S) -> &mut Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
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<S>(&mut self, c: S) -> &mut Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
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<S>(&mut self, accept: S) -> &mut Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
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<S, T>(&mut self, user: S, pass: T) -> &mut Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
T: Into<String>,
|
||||||
|
{
|
||||||
|
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<S, T>(&mut self, kind: S, pass: T) -> &mut Request
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
T: Into<String>,
|
||||||
|
{
|
||||||
|
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<Fn(&str, &str) -> usize>) -> Request {
|
||||||
|
// unimplemented!()
|
||||||
|
// }
|
||||||
|
// pub fn ca<S>(&self, accept: S) -> Request
|
||||||
|
// where S: Into<String> {
|
||||||
|
// unimplemented!()
|
||||||
|
// }
|
||||||
|
// pub fn cert<S>(&self, accept: S) -> Request
|
||||||
|
// where S: Into<String> {
|
||||||
|
// unimplemented!()
|
||||||
|
// }
|
||||||
|
// pub fn key<S>(&self, accept: S) -> Request
|
||||||
|
// where S: Into<String> {
|
||||||
|
// unimplemented!()
|
||||||
|
// }
|
||||||
|
// pub fn pfx<S>(&self, accept: S) -> Request // TODO what type? u8?
|
||||||
|
// where S: Into<String> {
|
||||||
|
// unimplemented!()
|
||||||
|
// }
|
||||||
|
|
||||||
|
fn to_url(&self) -> Result<Url, Error> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
277
src/response.rs
Normal file
277
src/response.rs
Normal file
@@ -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<Header>,
|
||||||
|
reader: Option<Box<Read + Send + 'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String> {
|
||||||
|
let encoding = encoding_from_whatwg_label(self.charset())
|
||||||
|
.or_else(|| encoding_from_whatwg_label(DEFAULT_CHARACTER_SET))
|
||||||
|
.unwrap();
|
||||||
|
let mut buf: Vec<u8> = vec![];
|
||||||
|
self.into_reader().read_to_end(&mut buf)?;
|
||||||
|
Ok(encoding.decode(&buf, DecoderTrap::Replace).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_json(self) -> IoResult<serde_json::Value> {
|
||||||
|
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::<Response>()
|
||||||
|
.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<Response, Error>
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// 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<Header> = 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::<Header>() {
|
||||||
|
headers.push(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Response {
|
||||||
|
status_line,
|
||||||
|
index,
|
||||||
|
status,
|
||||||
|
headers,
|
||||||
|
reader: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_reader<R>(&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::<u16>().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<Self, Self::Err> {
|
||||||
|
let mut read = VecRead::from_str(s);
|
||||||
|
let mut resp = Self::do_from_read(&mut read)?;
|
||||||
|
resp.set_reader(read);
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Response> 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<R: Read>(reader: &mut R) -> IoResult<AsciiString> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/stream.rs
Normal file
34
src/stream.rs
Normal file
@@ -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<usize> {
|
||||||
|
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<usize> {
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/util/macros.rs
Normal file
10
src/util/macros.rs
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
);
|
||||||
29
src/util/mod.rs
Normal file
29
src/util/mod.rs
Normal file
@@ -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: Into<String>>(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()
|
||||||
|
}
|
||||||
286
src/util/serde_macros.rs
Normal file
286
src/util/serde_macros.rs
Normal file
@@ -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 <LICENSE-APACHE or
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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<String>`. 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
34
src/util/vecread.rs
Normal file
34
src/util/vecread.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
use std::io::Result;
|
||||||
|
|
||||||
|
pub struct VecRead {
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VecRead {
|
||||||
|
pub fn new(bytes: &[u8]) -> Self {
|
||||||
|
Self::from_vec(bytes.to_owned())
|
||||||
|
}
|
||||||
|
pub fn from_vec(bytes: Vec<u8>) -> 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<usize> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user