├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── blocking.rs └── lib.rs └── tests ├── auth.rs ├── auth_async.rs ├── delete.rs ├── delete_async.rs ├── error.rs ├── error_async.rs ├── get.rs ├── get_async.rs ├── headers.rs ├── headers_async.rs ├── patch.rs ├── patch_async.rs ├── post.rs ├── post_async.rs ├── put.rs └── put_async.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "restson" 3 | version = "1.5.0" 4 | authors = ["Sami Pietikäinen"] 5 | description = "Easy-to-use REST client with automatic serialization and deserialization." 6 | repository = "https://github.com/spietika/restson-rust" 7 | keywords = ["rest", "client", "http", "json", "async"] 8 | categories = ["network-programming", "web-programming::http-client"] 9 | readme = "README.md" 10 | license = "MIT" 11 | edition = "2021" 12 | 13 | [dependencies] 14 | hyper = { version = "0.14", features = ["client", "http1", "http2"] } 15 | hyper-tls = { version = "0.5", optional = true } 16 | hyper-rustls = { version = "0.24", features = ["http2"], optional = true } 17 | futures = "^0.3" 18 | tokio = { version = "1", features = ["time"] } 19 | serde = { version = "^1.0", features = ["derive"], optional = true } 20 | serde_json = { version = "1.0", optional = true } 21 | simd-json = { version = "0.1", optional = true } 22 | url = "2" 23 | log = "^0.4.6" 24 | base64 = "0.13" 25 | 26 | [dev-dependencies] 27 | serde_derive = "^1.0" 28 | tokio = { version = "1", features = ["macros"] } 29 | 30 | [features] 31 | default = ["blocking", "lib-serde-json", "native-tls"] 32 | blocking = [] 33 | lib-serde-json = ["serde", "serde_json"] 34 | lib-simd-json = ["serde", "simd-json", "serde_json"] 35 | native-tls = ["hyper-tls"] 36 | rustls = ["hyper-rustls"] 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sami Pietikäinen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![crates.io](https://img.shields.io/crates/v/restson.svg)](https://crates.io/crates/restson) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://raw.githubusercontent.com/spietika/restson-rust/master/LICENSE) [![Docs: latest](https://img.shields.io/badge/Docs-latest-green.svg)](https://docs.rs/restson/) 3 | 4 | # Restson Rust 5 | 6 | Easy-to-use REST client for Rust programming language that provides automatic serialization and deserialization from Rust structs. Provides async interface and an easy wrapper for synchronous calls. The library is implemented using [Hyper](https://github.com/hyperium/hyper) and [Serde JSON](https://github.com/serde-rs/json). 7 | 8 | ## Getting started 9 | 10 | Add the following lines to your project `Cargo.toml` file: 11 | 12 | ```toml 13 | [dependencies] 14 | restson = "^1.5" 15 | serde = "^1.0" 16 | serde_derive = "^1.0" 17 | ``` 18 | This adds dependencies for the Restson library and also for Serde which is needed to derive `Serialize` and `Deserialize` for user defined data structures. 19 | 20 | ### Features 21 | 22 | | Feature | Description | Default | 23 | |:---------------|:---|:---| 24 | | blocking | This option enables support for sync, blocking, client. When only async is used this can be disabled to remove unnecessary dependencies. | Yes | 25 | | lib-serde-json | This option enables Serde JSON parser for GET requests. Alternative for lib-simd-json. | Yes | 26 | | lib-simd-json | This option enables JSON parsing with simd-json for GET requests. This option can improve parsing performance if SIMD is supported on the target hardware. Alternative for lib-serde-json. | No | 27 | | native-tls | This option selects `native_tls` as TLS provider. Alternative for `rustls`. | Yes | 28 | | rustls | This option selects `rustls` as TLS provider. Alternative for `native-tls`. | No | 29 | 30 | ### Data structures 31 | 32 | Next, the data structures for the REST interface should be defined. The struct fields need to match with the API JSON fields. The whole JSON does not need to be defined, the struct can also contain a subset of the fields. Structs that are used with `GET` should derive `Deserialize` and structs that are used with `POST` should derive `Serialize`. 33 | 34 | Example JSON (subset of http://httpbin.org/anything response): 35 | ```json 36 | { 37 | "method": "GET", 38 | "origin": "1.2.3.4", 39 | "url": "https://httpbin.org/anything" 40 | } 41 | ``` 42 | Corresponding Rust struct: 43 | ```rust 44 | #[macro_use] 45 | extern crate serde_derive; 46 | 47 | #[derive(Serialize,Deserialize)] 48 | struct HttpBinAnything { 49 | method: String, 50 | url: String, 51 | } 52 | ``` 53 | 54 | These definitions allow to automatically serialize/deserialize the data structures to/from JSON when requests are processed. For more complex scenarios, see the Serde [examples](https://serde.rs/examples.html). 55 | 56 | ### REST paths 57 | 58 | In Restson library the API resource paths are associated with types. That is, the URL is constructed automatically and not given as parameter to requests. This allows to easily parametrize the paths without manual URL processing and reduces URL literals in the code. 59 | 60 | Each type that is used with REST requests needs to implement `RestPath` trait. The trait can be implemented multiple times with different generic parameters for the same type as shown below. The `get_path` can also return error to indicate that the parameters were not valid. This error is propagated directly to the client caller. 61 | 62 | ```rust 63 | // plain API call without parameters 64 | impl RestPath<()> for HttpBinAnything { 65 | fn get_path(_: ()) -> Result { Ok(String::from("anything")) } 66 | } 67 | 68 | // API call with one u32 parameter (e.g. "http://httpbin.org/anything/1234") 69 | impl RestPath for HttpBinAnything { 70 | fn get_path(param: u32) -> Result { Ok(format!("anything/{}", param)) } 71 | } 72 | ``` 73 | 74 | ### Requests 75 | 76 | To run requests a client instance needs to be created first. The client can be created as asynchronous which can be used with Rust async/await system or as synchronous that will block until the HTTP request has been finished and directly returns the value. The base URL of the resource is given as parameter. 77 | ```rust 78 | // async client 79 | let async_client = RestClient::new("http://httpbin.org").unwrap(); 80 | 81 | // sync client 82 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 83 | ``` 84 | 85 | This creates a client instance with default configuration. To configure the client, it is created with a `Builder` 86 | 87 | ```rust 88 | // async client 89 | let async_client = RestClient::builder().dns_workers(1) 90 | .build("http://httpbin.org").unwrap(); 91 | 92 | // sync client 93 | let client = RestClient::builder().dns_workers(1) 94 | .blocking("http://httpbin.org").unwrap(); 95 | ``` 96 | 97 | **GET** 98 | 99 | The following snippet shows an example `GET` request: 100 | 101 | ```rust 102 | // Gets https://httpbin.org/anything/1234 and deserializes the JSON to data variable 103 | // (data is struct HttpBinAnything) 104 | let data = client.get::<_, HttpBinAnything>(1234).unwrap(); 105 | ``` 106 | 107 | The request functions call the `get_path` automatically from `RestPath` to construct the URL from the given parameter. The type of the URL parameter (`_` above, compiler infers the correct type) and returned data (`HttpBinAnything`) are annotated in the request. 108 | 109 | Restson also provides `get_with` function which is similar to the basic `get` but it also accepts additional query parameters that are added to the request URL. 110 | ```rust 111 | // Gets http://httpbin.org/anything/1234?a=2&b=abcd 112 | let query = vec![("a","2"), ("b","abcd")]; 113 | let data = client.get_with::<_, HttpBinAnything>((), &query).unwrap(); 114 | ``` 115 | Both GET interfaces return `Result, Error>` where T is the target type in which the returned JSON is deserialized to. 116 | 117 | **POST** 118 | 119 | The following snippets show an example `POST` request: 120 | ```rust 121 | #[derive(Serialize)] 122 | struct HttpBinPost { 123 | data: String, 124 | } 125 | 126 | impl RestPath<()> for HttpBinPost { 127 | fn get_path(_: ()) -> Result { Ok(String::from("post")) } 128 | } 129 | ``` 130 | ```rust 131 | let data = HttpBinPost { data: String::from("test data")}; 132 | // Posts data to http://httpbin.org/post 133 | client.post((), &data).unwrap(); 134 | ``` 135 | In addition to the basic `post` interface, it is also possible to provide query parameters with `post_with` function. Also, `post_capture` and `post_capture_with` interfaces allow to capture and deserialize the message body returned by the server in the POST request (capture requests need type-annotation in the call). 136 | 137 | **PUT** 138 | 139 | HTTP PUT requests are also supported and the interface is similar to POST interface: `put`, `put_with`, `put_capture` and `put_capture_with` functions are available (capture requests need type-annotation in the call). 140 | 141 | **PATCH** 142 | 143 | HTTP PATCH requests are also supported and the interface is similar to POST and PUT interface: `patch` and `patch_with` functions are available. 144 | 145 | **DELETE** 146 | 147 | Restson supports HTTP DELETE requests to API paths. Normally DELETE request is sent to API URL without message body. However, if message body or query parameters are needed, `delete_with` can be used. Moreover, while it is not very common for the server to send response body to DELETE request, it is still possible to use `delete_capture` and `delete_capture_with` functions to capture it. 148 | 149 | Similarly with other requests, the path is obtained from `RestPath` trait. 150 | 151 | ```rust 152 | struct HttpBinDelete { 153 | } 154 | 155 | impl RestPath<()> for HttpBinDelete { 156 | fn get_path(_: ()) -> Result { Ok(String::from("delete")) } 157 | } 158 | ``` 159 | 160 | The `delete` function does not return any data (only possible error) so the type needs to be annotated. 161 | 162 | ```rust 163 | // DELETE request to http://httpbin.org/delete 164 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 165 | client.delete::<(), HttpBinDelete>(()).unwrap(); 166 | ``` 167 | 168 | ### Concurrent requests 169 | 170 | When using the async client, it is possible to run multiple requests concurrently as shown below: 171 | 172 | ```rust 173 | let client = RestClient::new("https://httpbin.org").unwrap(); 174 | 175 | // all three GET requests are done concurrently, and then joined 176 | let (data1, data2, data3) = tokio::try_join!( 177 | client.get::<_, HttpBinAnything>(1), 178 | client.get::<_, HttpBinAnything>(2), 179 | client.get::<_, HttpBinAnything>(3) 180 | ).unwrap(); 181 | ``` 182 | 183 | ### JSON with array root element 184 | 185 | In all of the examples above the JSON structure consists of key-value pairs that can be represented with Rust structs. However, it is also possible that valid JSON has array root element without a key. For example, the following is valid JSON. 186 | 187 | ```json 188 | ["a","b","c"] 189 | ``` 190 | 191 | It is possible to work with APIs returning arrays in Restson. However instead of a struct, the user type needs to be a container. `Vec` in this case. The type also needs to implement the `RestPath` trait as explained before, and easiest way to do so is to wrap the container in a `struct`. 192 | 193 | ```rust 194 | #[derive(Serialize,Deserialize,Debug,Clone)] 195 | struct Products ( pub Vec ); 196 | 197 | #[derive(Serialize,Deserialize,Debug,Clone)] 198 | pub struct Product { 199 | pub name: String, 200 | //... 201 | } 202 | 203 | impl RestPath<()> for Products { 204 | fn get_path(_: ()) -> Result { Ok(String::from("/api/objects/products"))} 205 | } 206 | 207 | pub fn products(&self) -> Vec { 208 | let client = RestClient::new_blocking("http://localhost:8080").unwrap(); 209 | client.get::<_, Products>(()).unwrap().0 210 | } 211 | ``` 212 | 213 | ### Relative paths 214 | 215 | It is possible to use relative paths in the base URL to avoid having to return version or other prefix from the `get_path()` implementation. For instance, endpoint `http://localhost:8080/api/v1/ep` could be handled by setting `http://localhost:8080/api/v1/` as base URL and returning `ep` from the `get_path()`. Note: the trailing slash in the base URL is significant! Without it, the last element is replaced instead of appended when the elements are joined (see [here](https://docs.rs/url/2.1.1/url/struct.Url.html#method.join) for more information). 216 | 217 | ### Body wash 218 | 219 | For some APIs it is necessary to remove magic values or otherwise clean/process the returned response before it is deserialized. It is possible to provide a custom processing function with `set_body_wash_fn()` which is called with the raw returned body before passing it to the deserialization step. 220 | 221 | ### Request headers 222 | 223 | Custom headers can be added to requests by using `set_headers()`. The headers are added to all subsequent GET and POST requests until they are cleared with `clear_headers()` call. 224 | 225 | ### Logging 226 | The library uses the `log` crate to provide debug and trace logs. These logs allow to easily see both outgoing requests as well as incoming responses from the server. See the [log crate documentation](https://docs.rs/log/*/log/) for details. 227 | 228 | ### Examples 229 | For more examples see *tests* directory. 230 | 231 | ## Migrations 232 | 233 | ### Migration to v1.0 234 | 235 | The version 1.0 adds new features that change the main interface of the client, most notably async support. To migrate existing code from 0.x versions, the `RestClient` creation needs to be updated. `RestClient::new_blocking` or `RestClient::builder().blocking("http://httpbin.org")` should be used to create synchronous client. 236 | 237 | ### Migration to v1.2 238 | 239 | The version 1.2 allows to use immutable client for requests. This has benefits such as allowing concurrent requests. However, this also changes how the server response is returned, and now all `get` requests (and other requests that capture data) need to be type-annotated. For example, previously `let data: HttpBinAnything = client.get(1234).unwrap();` was allowed, but now it has to be written as `let data = client.get::<_, HttpBinAnything>(1234).unwrap();`. 240 | 241 | ## License 242 | 243 | The library is released under the MIT license. See [LICENSE](https://raw.githubusercontent.com/spietika/restson-rust/master/LICENSE) for details. 244 | -------------------------------------------------------------------------------- /src/blocking.rs: -------------------------------------------------------------------------------- 1 | //! Blocking variant of the `RestClient` 2 | 3 | use crate::{Error, Query, Response, RestClient as AsyncRestClient, RestPath}; 4 | use hyper::header::HeaderValue; 5 | use std::{convert::TryFrom, time::Duration}; 6 | use tokio::runtime::{Builder, Runtime}; 7 | 8 | /// REST client to make HTTP GET and POST requests. Blocking version. 9 | pub struct RestClient { 10 | inner_client: AsyncRestClient, 11 | runtime: Runtime, 12 | } 13 | 14 | impl TryFrom for RestClient { 15 | type Error = Error; 16 | 17 | fn try_from(other: AsyncRestClient) -> Result { 18 | match Builder::new_current_thread().enable_all().build() { 19 | Ok(runtime) => Ok(Self { inner_client: other, runtime }), 20 | Err(e) => Err(Error::IoError(e)), 21 | } 22 | } 23 | } 24 | 25 | impl RestClient { 26 | /// Set whether a message body consisting only 'null' (from serde serialization) 27 | /// is sent in POST/PUT 28 | pub fn set_send_null_body(&mut self, send_null: bool) { 29 | self.inner_client.send_null_body = send_null; 30 | } 31 | 32 | /// Set credentials for HTTP Basic authentication. 33 | pub fn set_auth(&mut self, user: &str, pass: &str) { 34 | let mut s: String = user.to_owned(); 35 | s.push(':'); 36 | s.push_str(pass); 37 | self.inner_client.auth = Some("Basic ".to_owned() + &base64::encode(&s)); 38 | } 39 | 40 | /// Set a function that cleans the response body up before deserializing it. 41 | pub fn set_body_wash_fn(&mut self, func: fn(String) -> String) { 42 | self.inner_client.body_wash_fn = func; 43 | } 44 | 45 | /// Set request timeout 46 | pub fn set_timeout(&mut self, timeout: Duration) { 47 | self.inner_client.timeout = timeout; 48 | } 49 | 50 | /// Set HTTP header from string name and value. 51 | /// 52 | /// The header is added to all subsequent GET and POST requests 53 | /// unless the headers are cleared with `clear_headers()` call. 54 | pub fn set_header(&mut self, name: &'static str, value: &str) -> Result<(), Error> { 55 | let value = HeaderValue::from_str(value).map_err(|_| Error::InvalidValue)?; 56 | self.inner_client.headers.insert(name, value); 57 | Ok(()) 58 | } 59 | 60 | /// Clear all previously set headers 61 | pub fn clear_headers(&mut self) { 62 | self.inner_client.headers.clear(); 63 | } 64 | 65 | /// Make a GET request. 66 | pub fn get(&self, params: U) -> Result, Error> 67 | where 68 | T: serde::de::DeserializeOwned + RestPath, 69 | { 70 | self.runtime.block_on(self.inner_client.get(params)) 71 | } 72 | 73 | /// Make a GET request with query parameters. 74 | pub fn get_with(&self, params: U, query: &Query<'_>) -> Result, Error> 75 | where 76 | T: serde::de::DeserializeOwned + RestPath, 77 | { 78 | self.runtime.block_on(self.inner_client.get_with(params, query)) 79 | } 80 | 81 | /// Make a POST request. 82 | pub fn post(&self, params: U, data: &T) -> Result, Error> 83 | where 84 | T: serde::Serialize + RestPath, 85 | { 86 | self.runtime.block_on(self.inner_client.post(params, data)) 87 | } 88 | 89 | /// Make a PUT request. 90 | pub fn put(&self, params: U, data: &T) -> Result, Error> 91 | where 92 | T: serde::Serialize + RestPath, 93 | { 94 | self.runtime.block_on(self.inner_client.put(params, data)) 95 | } 96 | 97 | /// Make a PATCH request. 98 | pub fn patch(&self, params: U, data: &T) -> Result, Error> 99 | where 100 | T: serde::Serialize + RestPath, 101 | { 102 | self.runtime.block_on(self.inner_client.patch(params, data)) 103 | } 104 | 105 | /// Make POST request with query parameters. 106 | pub fn post_with(&self, params: U, data: &T, query: &Query<'_>) -> Result, Error> 107 | where 108 | T: serde::Serialize + RestPath, 109 | { 110 | self.runtime.block_on(self.inner_client.post_with(params, data, query)) 111 | } 112 | 113 | /// Make PUT request with query parameters. 114 | pub fn put_with(&self, params: U, data: &T, query: &Query<'_>) -> Result, Error> 115 | where 116 | T: serde::Serialize + RestPath, 117 | { 118 | self.runtime.block_on(self.inner_client.put_with(params, data, query)) 119 | } 120 | 121 | /// Make PATCH request with query parameters. 122 | pub fn patch_with(&self, params: U, data: &T, query: &Query<'_>) -> Result, Error> 123 | where 124 | T: serde::Serialize + RestPath, 125 | { 126 | self.runtime.block_on(self.inner_client.patch_with(params, data, query)) 127 | } 128 | 129 | /// Make a POST request and capture returned body. 130 | pub fn post_capture(&self, params: U, data: &T) -> Result, Error> 131 | where 132 | T: serde::Serialize + RestPath, 133 | K: serde::de::DeserializeOwned, 134 | { 135 | self.runtime.block_on(self.inner_client.post_capture(params, data)) 136 | } 137 | 138 | /// Make a PUT request and capture returned body. 139 | pub fn put_capture(&self, params: U, data: &T) -> Result, Error> 140 | where 141 | T: serde::Serialize + RestPath, 142 | K: serde::de::DeserializeOwned, 143 | { 144 | self.runtime.block_on(self.inner_client.put_capture(params, data)) 145 | } 146 | 147 | /// Make a PATCH request and capture returned body. 148 | pub fn patch_capture(&self, params: U, data: &T) -> Result, Error> 149 | where 150 | T: serde::Serialize + RestPath, 151 | K: serde::de::DeserializeOwned, 152 | { 153 | self.runtime.block_on(self.inner_client.patch_capture(params, data)) 154 | } 155 | 156 | /// Make a POST request with query parameters and capture returned body. 157 | pub fn post_capture_with( 158 | &self, 159 | params: U, 160 | data: &T, 161 | query: &Query<'_>, 162 | ) -> Result, Error> 163 | where 164 | T: serde::Serialize + RestPath, 165 | K: serde::de::DeserializeOwned, 166 | { 167 | self.runtime.block_on(self.inner_client.post_capture_with(params, data, query)) 168 | } 169 | 170 | /// Make a PUT request with query parameters and capture returned body. 171 | pub fn put_capture_with( 172 | &self, 173 | params: U, 174 | data: &T, 175 | query: &Query<'_>, 176 | ) -> Result, Error> 177 | where 178 | T: serde::Serialize + RestPath, 179 | K: serde::de::DeserializeOwned, 180 | { 181 | self.runtime.block_on(self.inner_client.put_capture_with(params, data, query)) 182 | } 183 | 184 | /// Make a PATCH request with query parameters and capture returned body. 185 | pub fn patch_capture_with( 186 | &self, 187 | params: U, 188 | data: &T, 189 | query: &Query<'_>, 190 | ) -> Result, Error> 191 | where 192 | T: serde::Serialize + RestPath, 193 | K: serde::de::DeserializeOwned, 194 | { 195 | self.runtime.block_on(self.inner_client.patch_capture_with(params, data, query)) 196 | } 197 | 198 | /// Make a DELETE request. 199 | pub fn delete(&self, params: U) -> Result, Error> 200 | where 201 | T: RestPath, 202 | { 203 | self.runtime.block_on(self.inner_client.delete::(params)) 204 | } 205 | 206 | /// Make a DELETE request with query and body. 207 | pub fn delete_with(&self, params: U, data: &T, query: &Query<'_>) -> Result, Error> 208 | where 209 | T: serde::Serialize + RestPath, 210 | { 211 | self.runtime.block_on(self.inner_client.delete_with(params, data, query)) 212 | } 213 | 214 | /// Make a DELETE request and capture returned body. 215 | pub fn delete_capture(&self, params: U, data: &T) -> Result, Error> 216 | where 217 | T: serde::Serialize + RestPath, 218 | K: serde::de::DeserializeOwned, 219 | { 220 | self.runtime.block_on(self.inner_client.delete_capture(params, data)) 221 | } 222 | 223 | /// Make a DELETE request with query parameters and capture returned body. 224 | pub fn delete_capture_with( 225 | &self, 226 | params: U, 227 | data: &T, 228 | query: &Query<'_>, 229 | ) -> Result, Error> 230 | where 231 | T: serde::Serialize + RestPath, 232 | K: serde::de::DeserializeOwned, 233 | { 234 | self.runtime.block_on(self.inner_client.delete_capture_with(params, data, query)) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Easy-to-use REST client for Rust programming language that provides 3 | //! automatic serialization and deserialization from Rust structs. The library 4 | //! is implemented using [Hyper](https://github.com/hyperium/hyper) and 5 | //! [Serde JSON](https://github.com/serde-rs/json). 6 | //! 7 | //! # Examples 8 | //! ``` 9 | //! extern crate restson; 10 | //! #[macro_use] 11 | //! extern crate serde_derive; 12 | //! 13 | //! use restson::{RestClient,RestPath,Error}; 14 | //! 15 | //! // Data structure that matches with REST API JSON 16 | //! #[derive(Serialize,Deserialize,Debug)] 17 | //! struct HttpBinAnything { 18 | //! method: String, 19 | //! url: String, 20 | //! } 21 | //! 22 | //! // Path of the REST endpoint: e.g. http:///anything 23 | //! impl RestPath<()> for HttpBinAnything { 24 | //! fn get_path(_: ()) -> Result { Ok(String::from("anything")) } 25 | //! } 26 | //! 27 | //! #[tokio::main(flavor = "current_thread")] 28 | //! async fn main() { 29 | //! // Create new client with API base URL 30 | //! let mut client = RestClient::new("http://httpbin.org").unwrap(); 31 | //! 32 | //! // GET http://httpbin.org/anything and deserialize the result automatically 33 | //! let data = client.get::<_, HttpBinAnything>(()).await.unwrap().into_inner(); 34 | //! println!("{:?}", data); 35 | //! } 36 | //! ``` 37 | 38 | use tokio::time::timeout; 39 | use hyper::header::*; 40 | use hyper::body::Buf; 41 | use hyper::{Client, Method, Request}; 42 | use log::{debug, trace, error}; 43 | use std::{error, fmt}; 44 | use std::ops::Deref; 45 | use std::time::Duration; 46 | use url::Url; 47 | 48 | #[cfg(feature = "native-tls")] 49 | use hyper_tls::HttpsConnector; 50 | #[cfg(feature = "rustls")] 51 | use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; 52 | 53 | #[cfg(feature = "blocking")] 54 | pub mod blocking; 55 | 56 | static VERSION: &str = env!("CARGO_PKG_VERSION"); 57 | 58 | /// Type for URL query parameters. 59 | /// 60 | /// Slice of tuples in which the first field is parameter name and second is value. 61 | /// These parameters are used with `get_with` and `post_with` functions. 62 | /// 63 | /// # Examples 64 | /// The vector 65 | /// ```ignore 66 | /// vec![("param1", "1234"), ("param2", "abcd")] 67 | /// ``` 68 | /// would be parsed to **param1=1234¶m2=abcd** in the request URL. 69 | pub type Query<'a> = [(&'a str, &'a str)]; 70 | 71 | pub type HyperClient = Client>; 72 | 73 | /// Type returned by client query functions 74 | #[derive(Debug)] 75 | pub struct Response { 76 | body: T, 77 | headers: HeaderMap, 78 | } 79 | 80 | impl Response { 81 | /// Unwraps the response, getting the owned inner body 82 | pub fn into_inner(self) -> T { 83 | self.body 84 | } 85 | 86 | /// Response headers sent by the server 87 | pub fn headers(&self) -> &HeaderMap { 88 | &self.headers 89 | } 90 | } 91 | 92 | impl Response { 93 | /// Parse a response body 94 | fn parse(self) -> Result, Error> { 95 | #[cfg(feature = "lib-serde-json")] 96 | { 97 | let Self { body, headers } = self; 98 | serde_json::from_str(&body) 99 | .map(|body| Response { body, headers }) 100 | .map_err(|err| Error::DeserializeParseError(err, body)) 101 | } 102 | 103 | #[cfg(feature = "lib-simd-json")] 104 | { 105 | let Self { mut body, headers } = self; 106 | simd_json::serde::from_str(&mut body) 107 | .map(|body| Response { body, headers }) 108 | .map_err(|err| Error::DeserializeParseSimdJsonError(err, body)) 109 | } 110 | } 111 | } 112 | 113 | impl Deref for Response { 114 | type Target = T; 115 | 116 | fn deref(&self) -> &Self::Target { 117 | &self.body 118 | } 119 | } 120 | 121 | /// REST client to make HTTP GET and POST requests. 122 | pub struct RestClient { 123 | client: HyperClient, 124 | baseurl: url::Url, 125 | auth: Option, 126 | headers: HeaderMap, 127 | timeout: Duration, 128 | send_null_body: bool, 129 | body_wash_fn: fn(String) -> String, 130 | } 131 | 132 | /// Restson error return type. 133 | #[derive(Debug)] 134 | pub enum Error { 135 | /// HTTP client creation failed 136 | HttpClientError, 137 | 138 | /// Failed to parse final URL. 139 | UrlError, 140 | 141 | /// Failed to serialize struct to JSON (in POST). 142 | SerializeParseError(serde_json::Error), 143 | 144 | /// Failed to deserialize data to struct (in GET or POST response). 145 | DeserializeParseError(serde_json::Error, String), 146 | 147 | /// Failed to deserialize data to struct from simd_json crate (in GET or POST response). 148 | #[cfg(feature = "lib-simd-json")] 149 | DeserializeParseSimdJsonError(simd_json::Error, String), 150 | 151 | /// Failed to make the outgoing request. 152 | RequestError, 153 | 154 | /// Failed to perform HTTP call using Hyper 155 | HyperError(hyper::Error), 156 | 157 | /// Failed to perform IO operation 158 | IoError(std::io::Error), 159 | 160 | /// Server returned non-success status. 161 | HttpError(u16, String), 162 | 163 | /// Request has timed out 164 | TimeoutError, 165 | 166 | /// Invalid parameter value 167 | InvalidValue, 168 | } 169 | 170 | /// Builder for `RestClient` 171 | pub struct Builder { 172 | /// Request timeout 173 | timeout: Duration, 174 | 175 | /// Send null body 176 | send_null_body: bool, 177 | 178 | /// Hyper client to use for the connection 179 | client: Option, 180 | } 181 | 182 | impl fmt::Display for Error { 183 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 184 | let desc = match *self { 185 | Error::HttpClientError => "HTTP Client creation failed", 186 | Error::UrlError => "Failed to parse final URL", 187 | Error::SerializeParseError(_) => "Failed to serialize struct to JSON (in POST)", 188 | Error::DeserializeParseError(_, _) => { 189 | "Failed to deserialize data to struct (in GET or POST)" 190 | } 191 | #[cfg(feature = "lib-simd-json")] 192 | Error::DeserializeParseSimdJsonError(_, _) => { 193 | "Failed to deserialize data to struct by simd_json crate (in GET or POST)" 194 | } 195 | Error::RequestError => "Failed to make the outgoing request", 196 | Error::HyperError(_) => "Failed to make the outgoing request due to Hyper error", 197 | Error::IoError(_) => "Failed to make the outgoing request due to IO error", 198 | Error::HttpError(_, _) => "Server returned non-success status", 199 | Error::TimeoutError => "Request has timed out", 200 | Error::InvalidValue => "Invalid parameter value", 201 | }; 202 | fmt.write_str(desc)?; 203 | match *self { 204 | Error::SerializeParseError(ref err) => write!(fmt, ": {err}"), 205 | Error::DeserializeParseError(ref err, _) => write!(fmt, ": {err}"), 206 | #[cfg(feature = "lib-simd-json")] 207 | Error::DeserializeParseSimdJsonError(ref err, _) => write!(fmt, ": {err}"), 208 | Error::HyperError(ref err) => write!(fmt, ": {err}"), 209 | Error::IoError(ref err) => write!(fmt, ": {err}"), 210 | Error::HttpError(status, ref body) => write!(fmt, ": HTTP status {status}: {body}"), 211 | _ => Ok(()), 212 | } 213 | } 214 | } 215 | 216 | impl error::Error for Error { 217 | fn cause(&self) -> Option<&dyn error::Error> { 218 | match *self { 219 | Error::SerializeParseError(ref err) => Some(err), 220 | Error::DeserializeParseError(ref err, _) => Some(err), 221 | Error::HyperError(ref err) => Some(err), 222 | #[cfg(feature = "lib-simd-json")] 223 | Error::DeserializeParseSimdJsonError(ref err, _) => Some(err), 224 | _ => None, 225 | } 226 | } 227 | } 228 | 229 | impl std::convert::From for Error { 230 | fn from(e: hyper::Error) -> Self { 231 | Error::HyperError(e) 232 | } 233 | } 234 | 235 | impl std::convert::From for Error { 236 | fn from(_e: tokio::time::error::Elapsed) -> Self { 237 | Error::TimeoutError 238 | } 239 | } 240 | 241 | impl Default for Builder { 242 | fn default() -> Self { 243 | Self { 244 | timeout: Duration::from_secs(std::u64::MAX), 245 | send_null_body: true, 246 | client: None, 247 | } 248 | } 249 | } 250 | 251 | impl Builder { 252 | /// Set request timeout 253 | /// 254 | /// Default is no timeout 255 | #[inline] 256 | pub fn timeout(mut self, timeout: Duration) -> Self { 257 | self.timeout = timeout; 258 | self 259 | } 260 | 261 | /// Send null body in POST/PUT 262 | /// 263 | /// Default is yes 264 | #[inline] 265 | pub fn send_null_body(mut self, value: bool) -> Self { 266 | self.send_null_body = value; 267 | self 268 | } 269 | 270 | pub fn with_client(mut self, client: HyperClient) -> Self { 271 | self.client = Some(client); 272 | self 273 | } 274 | 275 | /// Create `RestClient` with the configuration in this builder 276 | pub fn build(self, url: &str) -> Result { 277 | RestClient::with_builder(url, self) 278 | } 279 | 280 | #[cfg(feature = "blocking")] 281 | /// Create [`blocking::RestClient`](blocking/struct.RestClient.html) with the configuration in 282 | /// this builder 283 | pub fn blocking(self, url: &str) -> Result { 284 | RestClient::with_builder(url, self).and_then(|client| client.try_into()) 285 | } 286 | } 287 | 288 | /// Rest path builder trait for type. 289 | /// 290 | /// Provides implementation for `rest_path` function that builds 291 | /// type (and REST endpoint) specific API path from given parameter(s). 292 | /// The built REST path is appended to the base URL given to `RestClient`. 293 | /// If `Err` is returned, it is propagated directly to API caller. 294 | pub trait RestPath { 295 | /// Construct type specific REST API path from given parameters 296 | /// (e.g. "api/devices/1234"). 297 | fn get_path(par: T) -> Result; 298 | } 299 | 300 | impl RestClient { 301 | /// Construct new client with default configuration to make HTTP requests. 302 | /// 303 | /// Use `Builder` to configure the client. 304 | pub fn new(url: &str) -> Result { 305 | RestClient::with_builder(url, RestClient::builder()) 306 | } 307 | 308 | /// Construct new blocking client with default configuration to make HTTP requests. 309 | /// 310 | /// Use `Builder` to configure the client. 311 | #[cfg(feature = "blocking")] 312 | pub fn new_blocking(url: &str) -> Result { 313 | RestClient::new(url).and_then(|client| client.try_into()) 314 | } 315 | 316 | #[cfg(feature = "native-tls")] 317 | fn build_client() -> HyperClient 318 | { 319 | Client::builder().build(HttpsConnector::new()) 320 | } 321 | 322 | #[cfg(feature = "rustls")] 323 | fn build_client() -> HyperClient 324 | { 325 | let connector = HttpsConnectorBuilder::new() 326 | .with_native_roots() 327 | .https_or_http() 328 | .enable_all_versions() 329 | .build(); 330 | Client::builder().build(connector) 331 | } 332 | 333 | fn with_builder(url: &str, builder: Builder) -> Result { 334 | let client = match builder.client { 335 | Some(client) => client, 336 | None => { 337 | Self::build_client() 338 | } 339 | }; 340 | 341 | let baseurl = Url::parse(url).map_err(|_| Error::UrlError)?; 342 | 343 | debug!("new client for {}", baseurl); 344 | Ok(RestClient { 345 | client, 346 | baseurl, 347 | auth: None, 348 | headers: HeaderMap::new(), 349 | timeout: builder.timeout, 350 | send_null_body: builder.send_null_body, 351 | body_wash_fn: std::convert::identity, 352 | }) 353 | } 354 | 355 | /// Configure a client 356 | pub fn builder() -> Builder { 357 | Builder::default() 358 | } 359 | 360 | /// Set whether a message body consisting only 'null' (from serde serialization) 361 | /// is sent in POST/PUT 362 | pub fn set_send_null_body(&mut self, send_null: bool) { 363 | self.send_null_body = send_null; 364 | } 365 | 366 | /// Set credentials for HTTP Basic authentication. 367 | pub fn set_auth(&mut self, user: &str, pass: &str) { 368 | let mut s: String = user.to_owned(); 369 | s.push(':'); 370 | s.push_str(pass); 371 | self.auth = Some("Basic ".to_owned() + &base64::encode(&s)); 372 | } 373 | 374 | /// Set a function that cleans the response body up before deserializing it. 375 | pub fn set_body_wash_fn(&mut self, func: fn(String) -> String) { 376 | self.body_wash_fn = func; 377 | } 378 | 379 | /// Set request timeout 380 | pub fn set_timeout(&mut self, timeout: Duration) { 381 | self.timeout = timeout; 382 | } 383 | 384 | /// Set HTTP header from string name and value. 385 | /// 386 | /// The header is added to all subsequent GET and POST requests 387 | /// unless the headers are cleared with `clear_headers()` call. 388 | pub fn set_header(&mut self, name: &'static str, value: &str) -> Result<(), Error> { 389 | let value = HeaderValue::from_str(value).map_err(|_| Error::InvalidValue)?; 390 | self.headers.insert(name, value); 391 | Ok(()) 392 | } 393 | 394 | /// Clear all previously set headers 395 | pub fn clear_headers(&mut self) { 396 | self.headers.clear(); 397 | } 398 | 399 | /// Make a GET request. 400 | pub async fn get(&self, params: U) -> Result, Error> 401 | where 402 | T: serde::de::DeserializeOwned + RestPath, 403 | { 404 | let req = self.make_request::(Method::GET, params, None, None)?; 405 | let res = self.run_request(req).await?; 406 | 407 | res.parse() 408 | } 409 | 410 | /// Make a GET request with query parameters. 411 | pub async fn get_with(&self, params: U, query: &Query<'_>) -> Result, Error> 412 | where 413 | T: serde::de::DeserializeOwned + RestPath, 414 | { 415 | let req = self.make_request::(Method::GET, params, Some(query), None)?; 416 | let res = self.run_request(req).await?; 417 | 418 | res.parse() 419 | } 420 | 421 | /// Make a POST request. 422 | pub async fn post(&self, params: U, data: &T) -> Result, Error> 423 | where 424 | T: serde::Serialize + RestPath, 425 | { 426 | self.post_or_put(Method::POST, params, data).await 427 | } 428 | 429 | /// Make a PUT request. 430 | pub async fn put(&self, params: U, data: &T) -> Result, Error> 431 | where 432 | T: serde::Serialize + RestPath, 433 | { 434 | self.post_or_put(Method::PUT, params, data).await 435 | } 436 | 437 | /// Make a PATCH request. 438 | pub async fn patch(&self, params: U, data: &T) -> Result, Error> 439 | where 440 | T: serde::Serialize + RestPath, 441 | { 442 | self.post_or_put(Method::PATCH, params, data).await 443 | } 444 | 445 | async fn post_or_put(&self, method: Method, params: U, data: &T) -> Result, Error> 446 | where 447 | T: serde::Serialize + RestPath, 448 | { 449 | let data = serde_json::to_string(data).map_err(Error::SerializeParseError)?; 450 | 451 | let req = self.make_request::(method, params, None, Some(data))?; 452 | let res = self.run_request(req).await?; 453 | Ok(Response { body: (), headers: res.headers }) 454 | } 455 | 456 | /// Make POST request with query parameters. 457 | pub async fn post_with(&self, params: U, data: &T, query: &Query<'_>) -> Result, Error> 458 | where 459 | T: serde::Serialize + RestPath, 460 | { 461 | self.post_or_put_with(Method::POST, params, data, query).await 462 | } 463 | 464 | /// Make PUT request with query parameters. 465 | pub async fn put_with(&self, params: U, data: &T, query: &Query<'_>) -> Result, Error> 466 | where 467 | T: serde::Serialize + RestPath, 468 | { 469 | self.post_or_put_with(Method::PUT, params, data, query).await 470 | } 471 | 472 | /// Make PATCH request with query parameters. 473 | pub async fn patch_with(&self, params: U, data: &T, query: &Query<'_>) -> Result, Error> 474 | where 475 | T: serde::Serialize + RestPath, 476 | { 477 | self.post_or_put_with(Method::PATCH, params, data, query).await 478 | } 479 | 480 | async fn post_or_put_with( 481 | &self, 482 | method: Method, 483 | params: U, 484 | data: &T, 485 | query: &Query<'_>, 486 | ) -> Result, Error> 487 | where 488 | T: serde::Serialize + RestPath, 489 | { 490 | let data = serde_json::to_string(data).map_err(Error::SerializeParseError)?; 491 | 492 | let req = self.make_request::(method, params, Some(query), Some(data))?; 493 | let res = self.run_request(req).await?; 494 | Ok(Response { body: (), headers: res.headers }) 495 | } 496 | 497 | /// Make a POST request and capture returned body. 498 | pub async fn post_capture(&self, params: U, data: &T) -> Result, Error> 499 | where 500 | T: serde::Serialize + RestPath, 501 | K: serde::de::DeserializeOwned, 502 | { 503 | self.generic_capture(Method::POST, params, data).await 504 | } 505 | 506 | /// Make a PUT request and capture returned body. 507 | pub async fn put_capture(&self, params: U, data: &T) -> Result, Error> 508 | where 509 | T: serde::Serialize + RestPath, 510 | K: serde::de::DeserializeOwned, 511 | { 512 | self.generic_capture(Method::PUT, params, data).await 513 | } 514 | 515 | /// Make a PATCH request and capture returned body. 516 | pub async fn patch_capture(&self, params: U, data: &T) -> Result, Error> 517 | where 518 | T: serde::Serialize + RestPath, 519 | K: serde::de::DeserializeOwned, 520 | { 521 | self.generic_capture(Method::PATCH, params, data).await 522 | } 523 | 524 | /// Make a DELETE request and capture returned body. 525 | pub async fn delete_capture(&self, params: U, data: &T) -> Result, Error> 526 | where 527 | T: serde::Serialize + RestPath, 528 | K: serde::de::DeserializeOwned, 529 | { 530 | self.generic_capture(Method::DELETE, params, data).await 531 | } 532 | 533 | async fn generic_capture( 534 | &self, 535 | method: Method, 536 | params: U, 537 | data: &T, 538 | ) -> Result, Error> 539 | where 540 | T: serde::Serialize + RestPath, 541 | K: serde::de::DeserializeOwned, 542 | { 543 | let data = serde_json::to_string(data).map_err(Error::SerializeParseError)?; 544 | 545 | let req = self.make_request::(method, params, None, Some(data))?; 546 | let res = self.run_request(req).await?; 547 | res.parse() 548 | } 549 | 550 | /// Make a POST request with query parameters and capture returned body. 551 | pub async fn post_capture_with( 552 | &self, 553 | params: U, 554 | data: &T, 555 | query: &Query<'_>, 556 | ) -> Result, Error> 557 | where 558 | T: serde::Serialize + RestPath, 559 | K: serde::de::DeserializeOwned, 560 | { 561 | self.generic_capture_with(Method::POST, params, data, query).await 562 | } 563 | 564 | /// Make a PUT request with query parameters and capture returned body. 565 | pub async fn put_capture_with( 566 | &self, 567 | params: U, 568 | data: &T, 569 | query: &Query<'_>, 570 | ) -> Result, Error> 571 | where 572 | T: serde::Serialize + RestPath, 573 | K: serde::de::DeserializeOwned, 574 | { 575 | self.generic_capture_with(Method::PUT, params, data, query).await 576 | } 577 | 578 | /// Make a PATCH request with query parameters and capture returned body. 579 | pub async fn patch_capture_with( 580 | &self, 581 | params: U, 582 | data: &T, 583 | query: &Query<'_>, 584 | ) -> Result, Error> 585 | where 586 | T: serde::Serialize + RestPath, 587 | K: serde::de::DeserializeOwned, 588 | { 589 | self.generic_capture_with(Method::PATCH, params, data, query).await 590 | } 591 | 592 | /// Make a DELETE request with query parameters and capture returned body. 593 | pub async fn delete_capture_with( 594 | &self, 595 | params: U, 596 | data: &T, 597 | query: &Query<'_>, 598 | ) -> Result, Error> 599 | where 600 | T: serde::Serialize + RestPath, 601 | K: serde::de::DeserializeOwned, 602 | { 603 | self.generic_capture_with(Method::DELETE, params, data, query).await 604 | } 605 | 606 | async fn generic_capture_with( 607 | &self, 608 | method: Method, 609 | params: U, 610 | data: &T, 611 | query: &Query<'_>, 612 | ) -> Result, Error> 613 | where 614 | T: serde::Serialize + RestPath, 615 | K: serde::de::DeserializeOwned, 616 | { 617 | let data = serde_json::to_string(data).map_err(Error::SerializeParseError)?; 618 | 619 | let req = self.make_request::(method, params, Some(query), Some(data))?; 620 | let res = self.run_request(req).await?; 621 | res.parse() 622 | } 623 | 624 | /// Make a DELETE request. 625 | pub async fn delete(&self, params: U) -> Result, Error> 626 | where 627 | T: RestPath, 628 | { 629 | let req = self.make_request::(Method::DELETE, params, None, None)?; 630 | let res = self.run_request(req).await?; 631 | Ok(Response { body: (), headers: res.headers }) 632 | } 633 | 634 | /// Make a DELETE request with query and body. 635 | pub async fn delete_with(&self, params: U, data: &T, query: &Query<'_>) -> Result, Error> 636 | where 637 | T: serde::Serialize + RestPath, 638 | { 639 | let data = serde_json::to_string(data).map_err(Error::SerializeParseError)?; 640 | let req = self.make_request::(Method::DELETE, params, Some(query), Some(data))?; 641 | let res = self.run_request(req).await?; 642 | Ok(Response { body: (), headers: res.headers }) 643 | } 644 | 645 | async fn run_request(&self, req: hyper::Request) -> Result, Error> { 646 | debug!("{} {}", req.method(), req.uri()); 647 | trace!("{:?}", req); 648 | 649 | let duration = self.timeout; 650 | let work = async { 651 | let res = self.client.request(req).await?; 652 | 653 | let response_headers = res.headers().clone(); 654 | let status = res.status(); 655 | let mut body = hyper::body::aggregate(res).await?; 656 | let body = body.copy_to_bytes(body.remaining()); 657 | 658 | let body = String::from_utf8_lossy(&body); 659 | 660 | Ok::<_, hyper::Error>((response_headers, body.to_string(), status)) 661 | }; 662 | 663 | let res = if duration != Duration::from_secs(std::u64::MAX) { 664 | timeout(duration, work).await?? 665 | } else { 666 | work.await? 667 | }; 668 | 669 | let (response_headers, body, status) = res; 670 | 671 | if !status.is_success() { 672 | error!("server returned \"{}\" error", status); 673 | return Err(Error::HttpError(status.as_u16(), body)); 674 | } 675 | 676 | trace!("response headers: {:?}", response_headers); 677 | trace!("response body: {}", body); 678 | Ok(Response { body: (self.body_wash_fn)(body), headers: response_headers }) 679 | } 680 | 681 | fn make_request( 682 | &self, 683 | method: Method, 684 | params: U, 685 | query: Option<&Query>, 686 | body: Option, 687 | ) -> Result, Error> 688 | where 689 | T: RestPath, 690 | { 691 | let uri = self.make_uri(T::get_path(params)?.as_str(), query)?; 692 | let mut req = Request::new(hyper::Body::empty()); 693 | 694 | *req.method_mut() = method; 695 | *req.uri_mut() = uri; 696 | 697 | if let Some(body) = body { 698 | if self.send_null_body || body != "null" { 699 | let len = HeaderValue::from_str(&body.len().to_string()) 700 | .map_err(|_| Error::RequestError)?; 701 | req.headers_mut().insert(CONTENT_LENGTH, len); 702 | req.headers_mut().insert( 703 | CONTENT_TYPE, 704 | HeaderValue::from_str("application/json").unwrap(), 705 | ); 706 | trace!("set request body: {}", body); 707 | *req.body_mut() = hyper::Body::from(body); 708 | } 709 | } 710 | 711 | if let Some(ref auth) = self.auth { 712 | req.headers_mut().insert( 713 | AUTHORIZATION, 714 | HeaderValue::from_str(auth).map_err(|_| Error::RequestError)?, 715 | ); 716 | }; 717 | 718 | for (key, value) in self.headers.iter() { 719 | req.headers_mut().insert(key, value.clone()); 720 | } 721 | 722 | if !req.headers().contains_key(USER_AGENT) { 723 | req.headers_mut().insert( 724 | USER_AGENT, 725 | HeaderValue::from_str(&("restson/".to_owned() + VERSION)) 726 | .map_err(|_| Error::RequestError)?, 727 | ); 728 | } 729 | 730 | Ok(req) 731 | } 732 | 733 | fn make_uri(&self, path: &str, params: Option<&Query>) -> Result { 734 | let mut url = self.baseurl.clone() 735 | .join(path) 736 | .map_err(|_| Error::UrlError)?; 737 | 738 | if let Some(params) = params { 739 | for &(key, item) in params.iter() { 740 | url.query_pairs_mut().append_pair(key, item); 741 | } 742 | } 743 | 744 | url.as_str() 745 | .parse::() 746 | .map_err(|_| Error::UrlError) 747 | } 748 | } 749 | -------------------------------------------------------------------------------- /tests/auth.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::Deserialize; 3 | 4 | #[derive(Deserialize)] 5 | struct HttpBinBasicAuth {} 6 | 7 | impl<'a> RestPath<(&'a str, &'a str)> for HttpBinBasicAuth { 8 | fn get_path(auth: (&str, &str)) -> Result { 9 | let (user, pass) = auth; 10 | Ok(format!("basic-auth/{}/{}", user, pass)) 11 | } 12 | } 13 | 14 | #[test] 15 | fn basic_auth() { 16 | let mut client = RestClient::new_blocking("http://httpbin.org").unwrap(); 17 | 18 | client.set_auth("username", "passwd"); 19 | client 20 | .get::<_, HttpBinBasicAuth>(("username", "passwd")) 21 | .unwrap(); 22 | } 23 | 24 | #[test] 25 | fn basic_auth_fail() { 26 | let mut client = RestClient::new_blocking("http://httpbin.org").unwrap(); 27 | 28 | client.set_auth("username", "wrong_passwd"); 29 | match client.get::<_, HttpBinBasicAuth>(("username", "passwd")) { 30 | Err(Error::HttpError(s, _)) if s == 401 || s == 403 => (), 31 | _ => panic!("Expected Unauthorized/Forbidden HTTP error"), 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /tests/auth_async.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::Deserialize; 3 | 4 | #[derive(Deserialize)] 5 | struct HttpBinBasicAuth {} 6 | 7 | impl<'a> RestPath<(&'a str, &'a str)> for HttpBinBasicAuth { 8 | fn get_path(auth: (&str, &str)) -> Result { 9 | let (user, pass) = auth; 10 | Ok(format!("basic-auth/{}/{}", user, pass)) 11 | } 12 | } 13 | 14 | #[tokio::test] 15 | async fn basic_auth() { 16 | let mut client = RestClient::new("http://httpbin.org").unwrap(); 17 | 18 | client.set_auth("username", "passwd"); 19 | client 20 | .get::<_, HttpBinBasicAuth>(("username", "passwd")) 21 | .await 22 | .unwrap(); 23 | } 24 | 25 | #[tokio::test] 26 | async fn basic_auth_fail() { 27 | let mut client = RestClient::new("http://httpbin.org").unwrap(); 28 | 29 | client.set_auth("username", "wrong_passwd"); 30 | match client.get::<_, HttpBinBasicAuth>(("username", "passwd")).await { 31 | Err(Error::HttpError(s, _)) if s == 401 || s == 403 => (), 32 | _ => panic!("Expected Unauthorized/Forbidden HTTP error"), 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /tests/delete.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | struct HttpBinDelete { 6 | data: String, 7 | } 8 | 9 | #[derive(Deserialize)] 10 | struct HttpBinDeleteResp { 11 | json: HttpBinDelete, 12 | url: String, 13 | } 14 | 15 | impl RestPath<()> for HttpBinDelete { 16 | fn get_path(_: ()) -> Result { 17 | Ok(String::from("delete")) 18 | } 19 | } 20 | 21 | 22 | #[test] 23 | fn basic_delete() { 24 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 25 | 26 | client.delete::<(), HttpBinDelete>(()).unwrap(); 27 | } 28 | 29 | #[test] 30 | fn delete_with() { 31 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 32 | 33 | let params = vec![("a", "2"), ("b", "abcd")]; 34 | let data = HttpBinDelete { 35 | data: String::from("test data"), 36 | }; 37 | client.delete_with((), &data, ¶ms).unwrap(); 38 | 39 | client.delete_with((), &data, &vec![]).unwrap(); 40 | } 41 | 42 | #[test] 43 | fn delete_capture() { 44 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 45 | 46 | let data = HttpBinDelete { 47 | data: String::from("test data"), 48 | }; 49 | let resp = client.delete_capture::<_, _, HttpBinDeleteResp>((), &data).unwrap(); 50 | 51 | assert_eq!(resp.json.data, "test data"); 52 | assert_eq!(resp.url, "https://httpbin.org/delete"); 53 | } 54 | 55 | #[test] 56 | fn delete_capture_query_params() { 57 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 58 | 59 | let params = vec![("a", "2"), ("b", "abcd")]; 60 | let data = HttpBinDelete { 61 | data: String::from("test data"), 62 | }; 63 | let resp = client.delete_capture_with::<_, _, HttpBinDeleteResp>((), &data, ¶ms).unwrap(); 64 | 65 | assert_eq!(resp.json.data, "test data"); 66 | assert_eq!(resp.url, "https://httpbin.org/delete?a=2&b=abcd"); 67 | } -------------------------------------------------------------------------------- /tests/delete_async.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | struct HttpBinDelete { 6 | data: String, 7 | } 8 | 9 | #[derive(Deserialize)] 10 | struct HttpBinDeleteResp { 11 | json: HttpBinDelete, 12 | url: String, 13 | } 14 | 15 | impl RestPath<()> for HttpBinDelete { 16 | fn get_path(_: ()) -> Result { 17 | Ok(String::from("delete")) 18 | } 19 | } 20 | 21 | 22 | #[tokio::test] 23 | async fn basic_delete() { 24 | let client = RestClient::new("http://httpbin.org").unwrap(); 25 | 26 | client.delete::<(), HttpBinDelete>(()).await.unwrap(); 27 | } 28 | 29 | #[tokio::test] 30 | async fn delete_with() { 31 | let client = RestClient::new("http://httpbin.org").unwrap(); 32 | 33 | let params = vec![("a", "2"), ("b", "abcd")]; 34 | let data = HttpBinDelete { 35 | data: String::from("test data"), 36 | }; 37 | client.delete_with((), &data, ¶ms).await.unwrap(); 38 | 39 | client.delete_with((), &data, &vec![]).await.unwrap(); 40 | } 41 | 42 | #[tokio::test] 43 | async fn delete_capture() { 44 | let client = RestClient::new("https://httpbin.org").unwrap(); 45 | 46 | let data = HttpBinDelete { 47 | data: String::from("test data"), 48 | }; 49 | let resp = client.delete_capture::<_, _, HttpBinDeleteResp>((), &data).await.unwrap(); 50 | 51 | assert_eq!(resp.json.data, "test data"); 52 | assert_eq!(resp.url, "https://httpbin.org/delete"); 53 | } 54 | 55 | #[tokio::test] 56 | async fn delete_capture_query_params() { 57 | let client = RestClient::new("https://httpbin.org").unwrap(); 58 | 59 | let params = vec![("a", "2"), ("b", "abcd")]; 60 | let data = HttpBinDelete { 61 | data: String::from("test data"), 62 | }; 63 | let resp = 64 | client.delete_capture_with::<_, _, HttpBinDeleteResp>((), &data, ¶ms).await.unwrap(); 65 | 66 | assert_eq!(resp.json.data, "test data"); 67 | assert_eq!(resp.url, "https://httpbin.org/delete?a=2&b=abcd"); 68 | } -------------------------------------------------------------------------------- /tests/error.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::{Deserialize, Serialize}; 3 | use std::time::{Duration, Instant}; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | struct InvalidResource {} 7 | 8 | impl RestPath<()> for InvalidResource { 9 | fn get_path(_: ()) -> Result { 10 | Ok(String::from("not_found")) 11 | } 12 | } 13 | 14 | impl RestPath for InvalidResource { 15 | fn get_path(param: bool) -> Result { 16 | if param { 17 | return Ok(String::from("path")); 18 | } 19 | Err(Error::UrlError) 20 | } 21 | } 22 | 23 | #[derive(Serialize, Deserialize)] 24 | struct HttpBinStatus {} 25 | 26 | impl RestPath for HttpBinStatus { 27 | fn get_path(code: u16) -> Result { 28 | Ok(format!("status/{}", code)) 29 | } 30 | } 31 | 32 | #[derive(Serialize, Deserialize)] 33 | struct HttpBinDelay {} 34 | 35 | impl RestPath for HttpBinDelay { 36 | fn get_path(delay: u16) -> Result { 37 | Ok(format!("delay/{}", delay)) 38 | } 39 | } 40 | 41 | #[derive(Serialize, Deserialize)] 42 | struct HttpBinBase64 {} 43 | 44 | impl RestPath for HttpBinBase64 { 45 | fn get_path(data: String) -> Result { 46 | Ok(format!("base64/{}", data)) 47 | } 48 | } 49 | 50 | #[test] 51 | fn invalid_baseurl() { 52 | match RestClient::new("1234") { 53 | Err(Error::UrlError) => (), 54 | _ => panic!("Expected url error"), 55 | }; 56 | } 57 | 58 | #[test] 59 | fn invalid_get() { 60 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 61 | 62 | if client.get::<(), InvalidResource>(()).is_ok() { 63 | panic!("expected error"); 64 | } 65 | } 66 | 67 | #[test] 68 | fn invalid_post() { 69 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 70 | 71 | let data = InvalidResource {}; 72 | 73 | if client.post((), &data).is_ok() { 74 | panic!("expected error"); 75 | } 76 | } 77 | 78 | #[test] 79 | fn path_error() { 80 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 81 | 82 | if let Err(Error::UrlError) = client.get::(false) { 83 | } else { 84 | panic!("expected url error"); 85 | } 86 | } 87 | 88 | #[test] 89 | fn http_error() { 90 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 91 | 92 | match client.get::<_, HttpBinStatus>(418) { 93 | Err(Error::HttpError(s, body)) => { 94 | assert_eq!(s, 418); 95 | assert!(!body.is_empty()); 96 | } 97 | _ => panic!("Expected 418 error status with response body"), 98 | }; 99 | } 100 | 101 | #[test] 102 | fn request_timeout() { 103 | let mut client = RestClient::new_blocking("http://httpbin.org").unwrap(); 104 | 105 | client.set_timeout(Duration::from_secs(1)); 106 | 107 | let start = Instant::now(); 108 | if let Err(Error::TimeoutError) = client.get::(3) { 109 | assert!(start.elapsed().as_secs() == 1); 110 | } else { 111 | panic!("expected timeout error"); 112 | } 113 | } 114 | 115 | #[test] 116 | #[cfg(feature = "lib-serde-json")] 117 | fn deserialize_error() { 118 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 119 | 120 | // Service returns decoded base64 in body which should be string 'test'. 121 | // This fails JSON deserialization and is returned in the Error 122 | if let Err(Error::DeserializeParseError(_, data)) = 123 | client.get::("dGVzdA==".to_string()) 124 | { 125 | assert!(data == "test"); 126 | } else { 127 | panic!("expected serialized error"); 128 | } 129 | } 130 | 131 | #[test] 132 | #[cfg(feature = "lib-simd-json")] 133 | fn deserialize_error() { 134 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 135 | 136 | if let Err(Error::DeserializeParseSimdJsonError(_, data)) = 137 | client.get::("dGVzdA==".to_string()) 138 | { 139 | assert!(data == "test"); 140 | } else { 141 | panic!("expected serialized error"); 142 | } 143 | } -------------------------------------------------------------------------------- /tests/error_async.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::{Deserialize, Serialize}; 3 | use std::time::{Duration, Instant}; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | struct InvalidResource {} 7 | 8 | impl RestPath<()> for InvalidResource { 9 | fn get_path(_: ()) -> Result { 10 | Ok(String::from("not_found")) 11 | } 12 | } 13 | 14 | impl RestPath for InvalidResource { 15 | fn get_path(param: bool) -> Result { 16 | if param { 17 | return Ok(String::from("path")); 18 | } 19 | Err(Error::UrlError) 20 | } 21 | } 22 | 23 | #[derive(Serialize, Deserialize)] 24 | struct HttpBinStatus {} 25 | 26 | impl RestPath for HttpBinStatus { 27 | fn get_path(code: u16) -> Result { 28 | Ok(format!("status/{}", code)) 29 | } 30 | } 31 | 32 | #[derive(Serialize, Deserialize)] 33 | struct HttpBinDelay {} 34 | 35 | impl RestPath for HttpBinDelay { 36 | fn get_path(delay: u16) -> Result { 37 | Ok(format!("delay/{}", delay)) 38 | } 39 | } 40 | 41 | #[derive(Serialize, Deserialize)] 42 | struct HttpBinBase64 {} 43 | 44 | impl RestPath for HttpBinBase64 { 45 | fn get_path(data: String) -> Result { 46 | Ok(format!("base64/{}", data)) 47 | } 48 | } 49 | 50 | #[tokio::test] 51 | async fn invalid_get() { 52 | let client = RestClient::new("http://httpbin.org").unwrap(); 53 | 54 | if client.get::<(), InvalidResource>(()).await.is_ok() { 55 | panic!("expected error"); 56 | } 57 | } 58 | 59 | #[tokio::test] 60 | async fn invalid_post() { 61 | let client = RestClient::new("http://httpbin.org").unwrap(); 62 | 63 | let data = InvalidResource {}; 64 | 65 | if client.post((), &data).await.is_ok() { 66 | panic!("expected error"); 67 | } 68 | } 69 | 70 | #[tokio::test] 71 | async fn path_error() { 72 | let client = RestClient::new("http://httpbin.org").unwrap(); 73 | 74 | if let Err(Error::UrlError) = client.get::(false).await { 75 | } else { 76 | panic!("expected url error"); 77 | } 78 | } 79 | 80 | #[tokio::test] 81 | async fn http_error() { 82 | let client = RestClient::new("http://httpbin.org").unwrap(); 83 | 84 | match client.get::<_, HttpBinStatus>(418).await { 85 | Err(Error::HttpError(s, body)) => { 86 | assert_eq!(s, 418); 87 | assert!(!body.is_empty()); 88 | } 89 | _ => panic!("Expected 418 error status with response body"), 90 | }; 91 | } 92 | 93 | #[tokio::test] 94 | async fn request_timeout() { 95 | let mut client = RestClient::new("http://httpbin.org").unwrap(); 96 | 97 | client.set_timeout(Duration::from_secs(1)); 98 | 99 | let start = Instant::now(); 100 | if let Err(Error::TimeoutError) = client.get::(3).await { 101 | assert!(start.elapsed().as_secs() == 1); 102 | } else { 103 | panic!("expected timeout error"); 104 | } 105 | } 106 | 107 | #[tokio::test] 108 | #[cfg(feature = "lib-serde-json")] 109 | async fn deserialize_error() { 110 | let client = RestClient::new("http://httpbin.org").unwrap(); 111 | 112 | // Service returns decoded base64 in body which should be string 'test'. 113 | // This fails JSON deserialization and is returned in the Error 114 | if let Err(Error::DeserializeParseError(_, data)) = 115 | client.get::("dGVzdA==".to_string()).await 116 | { 117 | assert!(data == "test"); 118 | } else { 119 | panic!("expected serialized error"); 120 | } 121 | } 122 | 123 | #[tokio::test] 124 | #[cfg(feature = "lib-simd-json")] 125 | async fn deserialize_error() { 126 | let client = RestClient::new("http://httpbin.org").unwrap(); 127 | 128 | if let Err(Error::DeserializeParseSimdJsonError(_, data)) = 129 | client.get::("dGVzdA==".to_string()).await 130 | { 131 | assert!(data == "test"); 132 | } else { 133 | panic!("expected serialized error"); 134 | } 135 | } -------------------------------------------------------------------------------- /tests/get.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::Deserialize; 3 | use std::time::Duration; 4 | 5 | #[derive(Deserialize)] 6 | struct HttpBinAnything { 7 | url: String, 8 | args: HttpBinAnythingArgs, 9 | } 10 | 11 | #[derive(Deserialize)] 12 | struct HttpRelativePath { 13 | url: String, 14 | } 15 | 16 | #[derive(Deserialize)] 17 | struct HttpBinAnythingArgs { 18 | #[serde(default)] 19 | a: String, 20 | #[serde(default)] 21 | b: String, 22 | } 23 | 24 | impl RestPath<()> for HttpBinAnything { 25 | fn get_path(_: ()) -> Result { 26 | Ok(String::from("anything")) 27 | } 28 | } 29 | 30 | impl RestPath for HttpBinAnything { 31 | fn get_path(param: u32) -> Result { 32 | Ok(format!("anything/{}", param)) 33 | } 34 | } 35 | 36 | impl<'a> RestPath<(u32, &'a str)> for HttpBinAnything { 37 | fn get_path(param: (u32, &str)) -> Result { 38 | let (a, b) = param; 39 | Ok(format!("anything/{}/{}", a, b)) 40 | } 41 | } 42 | 43 | impl RestPath<()> for HttpRelativePath { 44 | fn get_path(_: ()) -> Result { 45 | Ok(String::from("test")) 46 | } 47 | } 48 | 49 | #[test] 50 | fn basic_get_builder() { 51 | let client = RestClient::builder() 52 | .timeout(Duration::from_secs(10)) 53 | .send_null_body(false) 54 | .blocking("https://httpbin.org") 55 | .unwrap(); 56 | 57 | let data = client.get::<_, HttpBinAnything>(()).unwrap(); 58 | assert_eq!(data.url, "https://httpbin.org/anything"); 59 | } 60 | 61 | #[test] 62 | fn basic_get_https() { 63 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 64 | 65 | let data = client.get::<_, HttpBinAnything>(()).unwrap(); 66 | assert_eq!(data.url, "https://httpbin.org/anything"); 67 | } 68 | 69 | #[test] 70 | fn get_path_param() { 71 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 72 | 73 | let data = client.get::<_, HttpBinAnything>(1234).unwrap(); 74 | assert_eq!(data.url, "https://httpbin.org/anything/1234"); 75 | } 76 | 77 | #[test] 78 | fn get_multi_path_param() { 79 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 80 | 81 | let data = client.get::<_, HttpBinAnything>((1234, "abcd")).unwrap(); 82 | assert_eq!(data.url, "https://httpbin.org/anything/1234/abcd"); 83 | } 84 | 85 | #[test] 86 | fn get_query_params() { 87 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 88 | 89 | let params = vec![("a", "2"), ("b", "abcd")]; 90 | let data = client.get_with::<_, HttpBinAnything>((), ¶ms).unwrap(); 91 | 92 | assert_eq!(data.url, "https://httpbin.org/anything?a=2&b=abcd"); 93 | assert_eq!(data.args.a, "2"); 94 | assert_eq!(data.args.b, "abcd"); 95 | } 96 | 97 | #[test] 98 | fn relative_path() { 99 | // When using relative paths, the base path should end with '/'. Otherwise 100 | // the Url crate join() will replace the last element instead of appending 101 | // the path returned from get_path(). 102 | let client = RestClient::new_blocking("https://httpbin.org/anything/api/").unwrap(); 103 | 104 | let data = client.get::<_, HttpRelativePath>(()).unwrap(); 105 | assert_eq!(data.url, "https://httpbin.org/anything/api/test"); 106 | } 107 | 108 | 109 | #[test] 110 | fn body_wash_fn() { 111 | let mut client = RestClient::new_blocking("https://httpbin.org").unwrap(); 112 | 113 | // Ignore the JSON returned by the server and return a static test 114 | // JSON from the body wash fn so it is easy to detect it was called. 115 | let body_wash_fn = |_body: String| -> String { 116 | String::from("{\"url\": \"from body wash fn\", \"args\": {}}") 117 | }; 118 | client.set_body_wash_fn(body_wash_fn); 119 | 120 | let data = client.get::<_, HttpBinAnything>(()).unwrap(); 121 | assert_eq!(data.url, "from body wash fn"); 122 | } 123 | -------------------------------------------------------------------------------- /tests/get_async.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::Deserialize; 3 | use std::time::Duration; 4 | 5 | #[derive(Deserialize)] 6 | struct HttpBinAnything { 7 | url: String, 8 | args: HttpBinAnythingArgs, 9 | } 10 | 11 | #[derive(Deserialize)] 12 | struct HttpRelativePath { 13 | url: String, 14 | } 15 | 16 | #[derive(Deserialize)] 17 | struct HttpBinAnythingArgs { 18 | #[serde(default)] 19 | a: String, 20 | #[serde(default)] 21 | b: String, 22 | } 23 | 24 | impl RestPath<()> for HttpBinAnything { 25 | fn get_path(_: ()) -> Result { 26 | Ok(String::from("anything")) 27 | } 28 | } 29 | 30 | impl RestPath for HttpBinAnything { 31 | fn get_path(param: u32) -> Result { 32 | Ok(format!("anything/{}", param)) 33 | } 34 | } 35 | 36 | impl<'a> RestPath<(u32, &'a str)> for HttpBinAnything { 37 | fn get_path(param: (u32, &str)) -> Result { 38 | let (a, b) = param; 39 | Ok(format!("anything/{}/{}", a, b)) 40 | } 41 | } 42 | 43 | impl RestPath<()> for HttpRelativePath { 44 | fn get_path(_: ()) -> Result { 45 | Ok(String::from("test")) 46 | } 47 | } 48 | 49 | #[tokio::test] 50 | async fn basic_get_builder() { 51 | let client = RestClient::builder() 52 | .timeout(Duration::from_secs(10)) 53 | .send_null_body(false) 54 | .build("https://httpbin.org") 55 | .unwrap(); 56 | 57 | let data = client.get::<_, HttpBinAnything>(()).await.unwrap(); 58 | assert_eq!(data.url, "https://httpbin.org/anything"); 59 | } 60 | 61 | #[tokio::test] 62 | async fn basic_get_https() { 63 | let client = RestClient::new("https://httpbin.org").unwrap(); 64 | 65 | let data = client.get::<_, HttpBinAnything>(()).await.unwrap(); 66 | assert_eq!(data.url, "https://httpbin.org/anything"); 67 | } 68 | 69 | #[tokio::test] 70 | async fn get_path_param() { 71 | let client = RestClient::new("https://httpbin.org").unwrap(); 72 | 73 | let data = client.get::<_, HttpBinAnything>(1234).await.unwrap(); 74 | assert_eq!(data.url, "https://httpbin.org/anything/1234"); 75 | } 76 | 77 | #[tokio::test] 78 | async fn get_concurrent() { 79 | let client = RestClient::new("https://httpbin.org").unwrap(); 80 | 81 | let (data1, data2, data3) = tokio::try_join!( 82 | client.get::<_, HttpBinAnything>(1), 83 | client.get::<_, HttpBinAnything>(2), 84 | client.get::<_, HttpBinAnything>(3) 85 | ).unwrap(); 86 | 87 | assert_eq!(data1.url, "https://httpbin.org/anything/1"); 88 | assert_eq!(data2.url, "https://httpbin.org/anything/2"); 89 | assert_eq!(data3.url, "https://httpbin.org/anything/3"); 90 | } 91 | 92 | #[tokio::test] 93 | async fn get_multi_path_param() { 94 | let client = RestClient::new("https://httpbin.org").unwrap(); 95 | 96 | let data = client.get::<_, HttpBinAnything>((1234, "abcd")).await.unwrap(); 97 | assert_eq!(data.url, "https://httpbin.org/anything/1234/abcd"); 98 | } 99 | 100 | #[tokio::test] 101 | async fn get_query_params() { 102 | let client = RestClient::new("https://httpbin.org").unwrap(); 103 | 104 | let params = vec![("a", "2"), ("b", "abcd")]; 105 | let data = client.get_with::<_, HttpBinAnything>((), ¶ms).await.unwrap(); 106 | 107 | assert_eq!(data.url, "https://httpbin.org/anything?a=2&b=abcd"); 108 | assert_eq!(data.args.a, "2"); 109 | assert_eq!(data.args.b, "abcd"); 110 | } 111 | 112 | #[tokio::test] 113 | async fn relative_path() { 114 | // When using relative paths, the base path should end with '/'. Otherwise 115 | // the Url crate join() will replace the last element instead of appending 116 | // the path returned from get_path(). 117 | let client = RestClient::new("https://httpbin.org/anything/api/").unwrap(); 118 | 119 | let data = client.get::<_, HttpRelativePath>(()).await.unwrap(); 120 | assert_eq!(data.url, "https://httpbin.org/anything/api/test"); 121 | } 122 | 123 | 124 | #[tokio::test] 125 | async fn body_wash_fn() { 126 | let mut client = RestClient::new("https://httpbin.org").unwrap(); 127 | 128 | // Ignore the JSON returned by the server and return a static test 129 | // JSON from the body wash fn so it is easy to detect it was called. 130 | let body_wash_fn = |_body: String| -> String { 131 | String::from("{\"url\": \"from body wash fn\", \"args\": {}}") 132 | }; 133 | client.set_body_wash_fn(body_wash_fn); 134 | 135 | let data = client.get::<_, HttpBinAnything>(()).await.unwrap(); 136 | assert_eq!(data.url, "from body wash fn"); 137 | } 138 | -------------------------------------------------------------------------------- /tests/headers.rs: -------------------------------------------------------------------------------- 1 | use hyper::header::*; 2 | use restson::{Error, RestClient, RestPath}; 3 | use serde_derive::Deserialize; 4 | 5 | #[derive(Deserialize)] 6 | struct HttpBinAnything { 7 | headers: TestHeaders, 8 | } 9 | 10 | #[derive(Deserialize)] 11 | struct TestHeaders { 12 | #[serde(default)] 13 | #[serde(rename = "User-Agent")] 14 | user_agent: String, 15 | 16 | #[serde(default)] 17 | #[serde(rename = "X-Test")] 18 | test: String, 19 | } 20 | 21 | impl RestPath<()> for HttpBinAnything { 22 | fn get_path(_: ()) -> Result { 23 | Ok(String::from("anything")) 24 | } 25 | } 26 | 27 | #[test] 28 | fn headers() { 29 | let mut client = RestClient::new_blocking("http://httpbin.org").unwrap(); 30 | 31 | client 32 | .set_header(USER_AGENT.as_str(), "restson-test") 33 | .unwrap(); 34 | 35 | let data = client.get::<_, HttpBinAnything>(()).unwrap().into_inner(); 36 | assert_eq!(data.headers.user_agent, "restson-test"); 37 | } 38 | 39 | #[test] 40 | fn headers_clear() { 41 | let mut client = RestClient::new_blocking("http://httpbin.org").unwrap(); 42 | 43 | client.set_header("X-Test", "12345").unwrap(); 44 | 45 | let data = client.get::<_, HttpBinAnything>(()).unwrap().into_inner(); 46 | assert_eq!(data.headers.test, "12345"); 47 | 48 | client.clear_headers(); 49 | 50 | let data = client.get::<_, HttpBinAnything>(()).unwrap().into_inner(); 51 | assert_eq!(data.headers.test, ""); 52 | } 53 | 54 | #[test] 55 | fn default_user_agent() { 56 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 57 | 58 | let data = client.get::<_, HttpBinAnything>(()).unwrap().into_inner(); 59 | assert_eq!( 60 | data.headers.user_agent, 61 | "restson/".to_owned() + env!("CARGO_PKG_VERSION") 62 | ); 63 | } 64 | 65 | #[test] 66 | fn response_headers() { 67 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 68 | 69 | let data = client.get::<_, HttpBinAnything>(()).unwrap(); 70 | assert_eq!(data.headers()["content-type"], "application/json"); 71 | } 72 | -------------------------------------------------------------------------------- /tests/headers_async.rs: -------------------------------------------------------------------------------- 1 | use hyper::header::*; 2 | use restson::{Error, RestClient, RestPath}; 3 | use serde_derive::Deserialize; 4 | 5 | #[derive(Deserialize)] 6 | struct HttpBinAnything { 7 | headers: TestHeaders, 8 | } 9 | 10 | #[derive(Deserialize)] 11 | struct TestHeaders { 12 | #[serde(default)] 13 | #[serde(rename = "User-Agent")] 14 | user_agent: String, 15 | 16 | #[serde(default)] 17 | #[serde(rename = "X-Test")] 18 | test: String, 19 | } 20 | 21 | impl RestPath<()> for HttpBinAnything { 22 | fn get_path(_: ()) -> Result { 23 | Ok(String::from("anything")) 24 | } 25 | } 26 | 27 | #[tokio::test] 28 | async fn headers() { 29 | let mut client = RestClient::new("http://httpbin.org").unwrap(); 30 | 31 | client 32 | .set_header(USER_AGENT.as_str(), "restson-test") 33 | .unwrap(); 34 | 35 | let data = client.get::<_, HttpBinAnything>(()).await.unwrap().into_inner(); 36 | assert_eq!(data.headers.user_agent, "restson-test"); 37 | } 38 | 39 | #[tokio::test] 40 | async fn headers_clear() { 41 | let mut client = RestClient::new("http://httpbin.org").unwrap(); 42 | 43 | client.set_header("X-Test", "12345").unwrap(); 44 | 45 | let data = client.get::<_, HttpBinAnything>(()).await.unwrap().into_inner(); 46 | assert_eq!(data.headers.test, "12345"); 47 | 48 | client.clear_headers(); 49 | 50 | let data = client.get::<_, HttpBinAnything>(()).await.unwrap().into_inner(); 51 | assert_eq!(data.headers.test, ""); 52 | } 53 | 54 | #[tokio::test] 55 | async fn default_user_agent() { 56 | let client = RestClient::new("http://httpbin.org").unwrap(); 57 | 58 | let data = client.get::<_, HttpBinAnything>(()).await.unwrap().into_inner(); 59 | assert_eq!( 60 | data.headers.user_agent, 61 | "restson/".to_owned() + env!("CARGO_PKG_VERSION") 62 | ); 63 | } 64 | 65 | #[tokio::test] 66 | async fn response_headers() { 67 | let client = RestClient::new("http://httpbin.org").unwrap(); 68 | 69 | let data = client.get::<_, HttpBinAnything>(()).await.unwrap(); 70 | assert_eq!(data.headers()["content-type"], "application/json"); 71 | } 72 | -------------------------------------------------------------------------------- /tests/patch.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | struct HttpBinPatch { 6 | data: String, 7 | } 8 | 9 | #[derive(Deserialize)] 10 | struct HttpBinPatchResp { 11 | json: HttpBinPatch, 12 | url: String, 13 | } 14 | 15 | impl RestPath<()> for HttpBinPatch { 16 | fn get_path(_: ()) -> Result { 17 | Ok(String::from("patch")) 18 | } 19 | } 20 | 21 | #[test] 22 | fn basic_patch() { 23 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 24 | 25 | let data = HttpBinPatch { 26 | data: String::from("test data"), 27 | }; 28 | client.patch((), &data).unwrap(); 29 | } 30 | 31 | #[test] 32 | fn patch_query_params() { 33 | let client = RestClient::new_blocking("http://httpbin.org").unwrap(); 34 | 35 | let params = vec![("a", "2"), ("b", "abcd")]; 36 | let data = HttpBinPatch { 37 | data: String::from("test data"), 38 | }; 39 | client.patch_with((), &data, ¶ms).unwrap(); 40 | } 41 | 42 | #[test] 43 | fn patch_capture() { 44 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 45 | 46 | let data = HttpBinPatch { 47 | data: String::from("test data"), 48 | }; 49 | let resp = client.patch_capture::<_, _, HttpBinPatchResp>((), &data).unwrap(); 50 | 51 | assert_eq!(resp.json.data, "test data"); 52 | assert_eq!(resp.url, "https://httpbin.org/patch"); 53 | } 54 | 55 | #[test] 56 | fn patch_capture_query_params() { 57 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 58 | 59 | let params = vec![("a", "2"), ("b", "abcd")]; 60 | let data = HttpBinPatch { 61 | data: String::from("test data"), 62 | }; 63 | let resp = client.patch_capture_with::<_, _, HttpBinPatchResp>((), &data, ¶ms).unwrap(); 64 | 65 | assert_eq!(resp.json.data, "test data"); 66 | assert_eq!(resp.url, "https://httpbin.org/patch?a=2&b=abcd"); 67 | } 68 | -------------------------------------------------------------------------------- /tests/patch_async.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | struct HttpBinPatch { 6 | data: String, 7 | } 8 | 9 | #[derive(Deserialize)] 10 | struct HttpBinPatchResp { 11 | json: HttpBinPatch, 12 | url: String, 13 | } 14 | 15 | impl RestPath<()> for HttpBinPatch { 16 | fn get_path(_: ()) -> Result { 17 | Ok(String::from("patch")) 18 | } 19 | } 20 | 21 | #[tokio::test] 22 | async fn basic_patch() { 23 | let client = RestClient::new("http://httpbin.org").unwrap(); 24 | 25 | let data = HttpBinPatch { 26 | data: String::from("test data"), 27 | }; 28 | client.patch((), &data).await.unwrap(); 29 | } 30 | 31 | #[tokio::test] 32 | async fn patch_query_params() { 33 | let client = RestClient::new("http://httpbin.org").unwrap(); 34 | 35 | let params = vec![("a", "2"), ("b", "abcd")]; 36 | let data = HttpBinPatch { 37 | data: String::from("test data"), 38 | }; 39 | client.patch_with((), &data, ¶ms).await.unwrap(); 40 | } 41 | 42 | #[tokio::test] 43 | async fn patch_capture() { 44 | let client = RestClient::new("https://httpbin.org").unwrap(); 45 | 46 | let data = HttpBinPatch { 47 | data: String::from("test data"), 48 | }; 49 | let resp = client.patch_capture::<_, _, HttpBinPatchResp>((), &data).await.unwrap(); 50 | 51 | assert_eq!(resp.json.data, "test data"); 52 | assert_eq!(resp.url, "https://httpbin.org/patch"); 53 | } 54 | 55 | #[tokio::test] 56 | async fn patch_capture_query_params() { 57 | let client = RestClient::new("https://httpbin.org").unwrap(); 58 | 59 | let params = vec![("a", "2"), ("b", "abcd")]; 60 | let data = HttpBinPatch { 61 | data: String::from("test data"), 62 | }; 63 | let resp = 64 | client.patch_capture_with::<_, _, HttpBinPatchResp>((), &data, ¶ms).await.unwrap(); 65 | 66 | assert_eq!(resp.json.data, "test data"); 67 | assert_eq!(resp.url, "https://httpbin.org/patch?a=2&b=abcd"); 68 | } 69 | -------------------------------------------------------------------------------- /tests/post.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | struct HttpBinPost { 6 | data: String, 7 | } 8 | 9 | #[derive(Deserialize)] 10 | struct HttpBinPostResp { 11 | json: HttpBinPost, 12 | url: String, 13 | } 14 | 15 | impl RestPath<()> for HttpBinPost { 16 | fn get_path(_: ()) -> Result { 17 | Ok(String::from("post")) 18 | } 19 | } 20 | 21 | #[test] 22 | fn basic_post() { 23 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 24 | 25 | let data = HttpBinPost { 26 | data: String::from("test data"), 27 | }; 28 | client.post((), &data).unwrap(); 29 | } 30 | 31 | #[test] 32 | fn post_query_params() { 33 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 34 | 35 | let params = vec![("a", "2"), ("b", "abcd")]; 36 | let data = HttpBinPost { 37 | data: String::from("test data"), 38 | }; 39 | client.post_with((), &data, ¶ms).unwrap(); 40 | } 41 | 42 | #[test] 43 | fn post_capture() { 44 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 45 | 46 | let data = HttpBinPost { 47 | data: String::from("test data"), 48 | }; 49 | let resp = client.post_capture::<_, _, HttpBinPostResp>((), &data).unwrap(); 50 | 51 | assert_eq!(resp.json.data, "test data"); 52 | assert_eq!(resp.url, "https://httpbin.org/post"); 53 | } 54 | 55 | #[test] 56 | fn post_capture_query_params() { 57 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 58 | 59 | let params = vec![("a", "2"), ("b", "abcd")]; 60 | let data = HttpBinPost { 61 | data: String::from("test data"), 62 | }; 63 | let resp = client.post_capture_with::<_, _, HttpBinPostResp>((), &data, ¶ms).unwrap(); 64 | 65 | assert_eq!(resp.json.data, "test data"); 66 | assert_eq!(resp.url, "https://httpbin.org/post?a=2&b=abcd"); 67 | } 68 | -------------------------------------------------------------------------------- /tests/post_async.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | struct HttpBinPost { 6 | data: String, 7 | } 8 | 9 | #[derive(Deserialize)] 10 | struct HttpBinPostResp { 11 | json: HttpBinPost, 12 | url: String, 13 | } 14 | 15 | impl RestPath<()> for HttpBinPost { 16 | fn get_path(_: ()) -> Result { 17 | Ok(String::from("post")) 18 | } 19 | } 20 | 21 | #[tokio::test] 22 | async fn basic_post() { 23 | let client = RestClient::new("https://httpbin.org").unwrap(); 24 | 25 | let data = HttpBinPost { 26 | data: String::from("test data"), 27 | }; 28 | client.post((), &data).await.unwrap(); 29 | } 30 | 31 | #[tokio::test] 32 | async fn post_query_params() { 33 | let client = RestClient::new("https://httpbin.org").unwrap(); 34 | 35 | let params = vec![("a", "2"), ("b", "abcd")]; 36 | let data = HttpBinPost { 37 | data: String::from("test data"), 38 | }; 39 | client.post_with((), &data, ¶ms).await.unwrap(); 40 | } 41 | 42 | #[tokio::test] 43 | async fn post_capture() { 44 | let client = RestClient::new("https://httpbin.org").unwrap(); 45 | 46 | let data = HttpBinPost { 47 | data: String::from("test data"), 48 | }; 49 | let resp = client.post_capture::<_, _, HttpBinPostResp>((), &data).await.unwrap(); 50 | 51 | assert_eq!(resp.json.data, "test data"); 52 | assert_eq!(resp.url, "https://httpbin.org/post"); 53 | } 54 | 55 | #[tokio::test] 56 | async fn post_capture_query_params() { 57 | let client = RestClient::new("https://httpbin.org").unwrap(); 58 | 59 | let params = vec![("a", "2"), ("b", "abcd")]; 60 | let data = HttpBinPost { 61 | data: String::from("test data"), 62 | }; 63 | let resp = client.post_capture_with::<_, _, HttpBinPostResp>((), &data, ¶ms).await.unwrap(); 64 | 65 | assert_eq!(resp.json.data, "test data"); 66 | assert_eq!(resp.url, "https://httpbin.org/post?a=2&b=abcd"); 67 | } 68 | -------------------------------------------------------------------------------- /tests/put.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | struct HttpBinPut { 6 | data: String, 7 | } 8 | 9 | #[derive(Deserialize)] 10 | struct HttpBinPutResp { 11 | json: HttpBinPut, 12 | url: String, 13 | } 14 | 15 | impl RestPath<()> for HttpBinPut { 16 | fn get_path(_: ()) -> Result { 17 | Ok(String::from("put")) 18 | } 19 | } 20 | 21 | #[test] 22 | fn basic_put() { 23 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 24 | 25 | let data = HttpBinPut { 26 | data: String::from("test data"), 27 | }; 28 | client.put((), &data).unwrap(); 29 | } 30 | 31 | #[test] 32 | fn put_query_params() { 33 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 34 | 35 | let params = vec![("a", "2"), ("b", "abcd")]; 36 | let data = HttpBinPut { 37 | data: String::from("test data"), 38 | }; 39 | client.put_with((), &data, ¶ms).unwrap(); 40 | } 41 | 42 | #[test] 43 | fn put_capture() { 44 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 45 | 46 | let data = HttpBinPut { 47 | data: String::from("test data"), 48 | }; 49 | let resp = client.put_capture::<_, _, HttpBinPutResp>((), &data).unwrap(); 50 | 51 | assert_eq!(resp.json.data, "test data"); 52 | assert_eq!(resp.url, "https://httpbin.org/put"); 53 | } 54 | 55 | #[test] 56 | fn put_capture_query_params() { 57 | let client = RestClient::new_blocking("https://httpbin.org").unwrap(); 58 | 59 | let params = vec![("a", "2"), ("b", "abcd")]; 60 | let data = HttpBinPut { 61 | data: String::from("test data"), 62 | }; 63 | let resp = client.put_capture_with::<_, _, HttpBinPutResp>((), &data, ¶ms).unwrap(); 64 | 65 | assert_eq!(resp.json.data, "test data"); 66 | assert_eq!(resp.url, "https://httpbin.org/put?a=2&b=abcd"); 67 | } 68 | -------------------------------------------------------------------------------- /tests/put_async.rs: -------------------------------------------------------------------------------- 1 | use restson::{Error, RestClient, RestPath}; 2 | use serde_derive::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | struct HttpBinPut { 6 | data: String, 7 | } 8 | 9 | #[derive(Deserialize)] 10 | struct HttpBinPutResp { 11 | json: HttpBinPut, 12 | url: String, 13 | } 14 | 15 | impl RestPath<()> for HttpBinPut { 16 | fn get_path(_: ()) -> Result { 17 | Ok(String::from("put")) 18 | } 19 | } 20 | 21 | #[tokio::test] 22 | async fn basic_put() { 23 | let client = RestClient::new("https://httpbin.org").unwrap(); 24 | 25 | let data = HttpBinPut { 26 | data: String::from("test data"), 27 | }; 28 | client.put((), &data).await.unwrap(); 29 | } 30 | 31 | #[tokio::test] 32 | async fn put_query_params() { 33 | let client = RestClient::new("https://httpbin.org").unwrap(); 34 | 35 | let params = vec![("a", "2"), ("b", "abcd")]; 36 | let data = HttpBinPut { 37 | data: String::from("test data"), 38 | }; 39 | client.put_with((), &data, ¶ms).await.unwrap(); 40 | } 41 | 42 | #[tokio::test] 43 | async fn put_capture() { 44 | let client = RestClient::new("https://httpbin.org").unwrap(); 45 | 46 | let data = HttpBinPut { 47 | data: String::from("test data"), 48 | }; 49 | let resp = client.put_capture::<_, _, HttpBinPutResp>((), &data).await.unwrap(); 50 | 51 | assert_eq!(resp.json.data, "test data"); 52 | assert_eq!(resp.url, "https://httpbin.org/put"); 53 | } 54 | 55 | #[tokio::test] 56 | async fn put_capture_query_params() { 57 | let client = RestClient::new("https://httpbin.org").unwrap(); 58 | 59 | let params = vec![("a", "2"), ("b", "abcd")]; 60 | let data = HttpBinPut { 61 | data: String::from("test data"), 62 | }; 63 | let resp = client.put_capture_with::<_, _, HttpBinPutResp>((), &data, ¶ms).await.unwrap(); 64 | 65 | assert_eq!(resp.json.data, "test data"); 66 | assert_eq!(resp.url, "https://httpbin.org/put?a=2&b=abcd"); 67 | } 68 | --------------------------------------------------------------------------------