1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
//! Rustify is a small crate which provides a way to easily scaffold code which
//! communicates with HTTP REST API endpoints. It covers simple cases such as basic
//! GET requests as well as more advanced cases such as sending serialized data
//! and deserializing the result. A derive macro is provided to keep code DRY.
//!
//! Rustify provides both a trait for implementing API endpoints as well as clients
//! for executing requests against the defined endpoints. Currently, only a client
//! using [reqwest::blocking][1] is provided.
//!
//! Presently, rustify only supports JSON serialization and generally assumes the
//! remote endpoint accepts and responds with JSON.
//!
//!
//! ## Architecture
//!
//! This crate consists of two primary traits:
//!
//! * The [Endpoint][crate::endpoint::Endpoint] trait which represents a remote
//! HTTP REST API endpoint
//! * The [Client][crate::client::Client] trait which is responsible for
//! executing the Endpoint
//!
//! This provides a loosely coupled interface that allows for multiple
//! implementations of the Client trait which may use different HTTP backends.
//! The Client trait in particular was kept intentionally easy to implement and
//! is only required to send a HTTP [request][crate::client::Request] consisting
//! of a URL, method, and body and then return the
//! [response][crate::client::Response] consisting of a URL, response code, and
//! response body. The crate currently only provides a blocking client based on
//! the [reqwest][2] crate.
//!
//! The Endpoint trait is what will be most implemented by end-users of this
//! crate. Since the implementation can be verbose and most functionality can be
//! defined with very little syntax, a macro is provided via `rustify_derive`
//! which should be used for generating implementations of this trait.
//!
//!
//! ## Usage
//!
//! The below example creates a `Test` endpoint that, when executed, will send a
//! GET request to `http://!api.com/test/path` and expect an empty response:
//!
//! ```
//! use rustify::clients::reqwest::ReqwestClient;
//! use rustify::endpoint::Endpoint;
//! use rustify_derive::Endpoint;
//! use serde::Serialize;
//!
//! #[derive(Debug, Endpoint, Serialize)]
//! #[endpoint(path = "test/path")]
//! struct Test {}
//!
//! let endpoint = Test {};
//! let client = ReqwestClient::default("http://!api.com");
//! let result = endpoint.exec(&client);
//! ```
//!
//! ## Advanced Usage
//!
//! This examples demonstrates the complexity available using the full suite of
//! options offered by the macro:
//!
//! ```rust
//! use derive_builder::Builder;
//! use rustify::clients::reqwest::ReqwestClient;
//! use rustify::{endpoint::{Endpoint, MiddleWare}, errors::ClientError};
//! use rustify_derive::Endpoint;
//! use serde::{Deserialize, Serialize};
//! use serde_json::Value;
//! use serde_with::skip_serializing_none;
//!
//! struct Middle {}
//! impl MiddleWare for Middle {
//! fn request<E: Endpoint>(
//! &self,
//! _: &E,
//! req: &mut rustify::client::Request,
//! ) -> Result<(), ClientError> {
//! req.headers
//! .push(("X-API-Token".to_string(), "mytoken".to_string()));
//! Ok(())
//! }
//! fn response<E: Endpoint>(
//! &self,
//! _: &E,
//! resp: &mut rustify::client::Response,
//! ) -> Result<(), ClientError> {
//! let err_body = resp.body.clone();
//! let wrapper: TestWrapper =
//! serde_json::from_slice(&resp.body).map_err(|e| ClientError::ResponseParseError {
//! source: Box::new(e),
//! content: String::from_utf8(err_body).ok(),
//! })?;
//! resp.body = wrapper.result.to_string().as_bytes().to_vec();
//! Ok(())
//! }
//! }
//!
//! #[derive(Deserialize)]
//! struct TestResponse {
//! age: u8,
//! }
//!
//! #[derive(Deserialize)]
//! struct TestWrapper {
//! result: Value,
//! }
//!
//! fn test_complex() {
//! #[skip_serializing_none]
//! #[derive(Builder, Debug, Default, Endpoint, Serialize)]
//! #[endpoint(
//! path = "test/path/{self.name}",
//! method = "POST",
//! result = "TestResponse",
//! builder = "true"
//! )]
//! #[builder(setter(into, strip_option), default)]
//! struct Test {
//! #[serde(skip)]
//! name: String,
//! kind: String,
//! special: Option<bool>,
//! optional: Option<String>,
//! }
//!
//! let client = ReqwestClient::default("http://!api.com");
//! let result = Test::builder().name("test").kind("test").exec_mut(&client, &Middle {});
//!
//! }
//! ```
//!
//! Breaking this down:
//!
//! ```ignore
//! #[endpoint(
//! path = "test/path/{self.name}",
//! method = "POST",
//! result = "TestResponse",
//! builder = "true"
//! )]
//!
//! ```
//!
//! * The `path` argument supports basic substitution using curly braces. In
//! this case the final url would be `http://!api.com/test/path/test`. Since
//! the `name` field is only used to build the endpoint URL, we add the
//! `#[serde(skip)]` attribute to inform `serde` to not serialize this field
//! when building the request.
//! * The `method` argument specifies the type of the HTTP request.
//! * The `result` argument specifies the type of response that the
//! [exec()][crate::endpoint::Endpoint::execute] method will return. This
//! type must derive [serde::Deserialize].
//! * The `builder` argument tells the macro to add some useful functions for
//! when the endpoint is using the `Builder` derive macro from
//! [derive_builder][3]. In particular, it adds a `builder()` static method to
//! the base struct and the `exec()` methods to the generated `TestBuilder`
//! struct which automatically calls `build()` on `TestBuilder` and then
//! executes the result. This allows for concise calls like this:
//! `Test::builder().name("test").kind("test").exec(&client);`
//!
//! Endpoints contain two methods for executing requests; in this example the
//! `execute_m()` variant is being used which allows passing an instance of an
//! object that implements `MiddleWare` which can be used to mutate the request
//! and response object respectively. Here the an arbitrary request header
//! containing a fictitious API token is being injected and the response has a
//! wrapper removed before final parsing.
//!
//! This example also demonstrates a common pattern of using
//! [skip_serializing_none][4] macro to force `serde` to not serialize fields of
//! type `Option::None`. When combined with the `default` parameter offered by
//! [derive_builder][3] the result is an endpoint which can have required and/or
//! optional fields as needed and which don't get serialized when not specified
//! when building. For example:
//!
//! ```ignore
//! // Errors, `kind` field is required
//! let result = Test::builder().name("test").exec(&client);
//!
//! // Produces POST http://!api.com/test/path/test {"kind": "test"}
//! let result = Test::builder().name("test").kind("test").exec(&client);
//!
//! // Produces POST http://!api.com/test/path/test {"kind": "test", "optional": "yes"}
//! let result = Test::builder().name("test").kind("test").optional("yes").exec&client);
//! ```
//!
//! ## Error Handling
//!
//! All errors generated by this crate are wrapped in the
//! [ClientError][crate::errors::ClientError] enum provided by the crate.
//!
//! [1]: https://docs.rs/reqwest/latest/reqwest/blocking/index.html
//! [2]: https://docs.rs/reqwest/latest/reqwest/index.html
//! [3]: https://docs.rs/derive_builder/latest/derive_builder/
//! [4]: https://docs.rs/serde_with/1.9.4/serde_with/attr.skip_serializing_none.html