wreq 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.toml +54 -0
  3. data/Gemfile +17 -0
  4. data/LICENSE +201 -0
  5. data/README.md +150 -0
  6. data/Rakefile +90 -0
  7. data/build.rs +9 -0
  8. data/examples/body.rb +42 -0
  9. data/examples/client.rb +33 -0
  10. data/examples/emulation_request.rb +37 -0
  11. data/examples/headers.rb +27 -0
  12. data/examples/proxy.rb +113 -0
  13. data/examples/send_stream.rb +85 -0
  14. data/examples/stream.rb +14 -0
  15. data/examples/thread_interrupt.rb +83 -0
  16. data/extconf.rb +7 -0
  17. data/lib/wreq.rb +313 -0
  18. data/lib/wreq_ruby/body.rb +36 -0
  19. data/lib/wreq_ruby/client.rb +516 -0
  20. data/lib/wreq_ruby/cookie.rb +144 -0
  21. data/lib/wreq_ruby/emulation.rb +186 -0
  22. data/lib/wreq_ruby/error.rb +159 -0
  23. data/lib/wreq_ruby/header.rb +197 -0
  24. data/lib/wreq_ruby/http.rb +132 -0
  25. data/lib/wreq_ruby/response.rb +208 -0
  26. data/script/build_platform_gem.rb +34 -0
  27. data/src/client/body/form.rs +2 -0
  28. data/src/client/body/json.rs +16 -0
  29. data/src/client/body/stream.rs +148 -0
  30. data/src/client/body.rs +57 -0
  31. data/src/client/param.rs +19 -0
  32. data/src/client/query.rs +2 -0
  33. data/src/client/req.rs +251 -0
  34. data/src/client/resp.rs +250 -0
  35. data/src/client.rs +392 -0
  36. data/src/cookie.rs +277 -0
  37. data/src/emulation.rs +317 -0
  38. data/src/error.rs +147 -0
  39. data/src/extractor.rs +199 -0
  40. data/src/gvl.rs +154 -0
  41. data/src/header.rs +177 -0
  42. data/src/http.rs +127 -0
  43. data/src/lib.rs +97 -0
  44. data/src/macros.rs +118 -0
  45. data/src/rt.rs +47 -0
  46. data/test/client_cookie_test.rb +46 -0
  47. data/test/client_test.rb +136 -0
  48. data/test/cookie_test.rb +166 -0
  49. data/test/emulation_test.rb +21 -0
  50. data/test/error_handling_test.rb +89 -0
  51. data/test/header_test.rb +290 -0
  52. data/test/module_methods_test.rb +75 -0
  53. data/test/request_parameters_test.rb +175 -0
  54. data/test/request_test.rb +234 -0
  55. data/test/response_test.rb +69 -0
  56. data/test/stream_test.rb +81 -0
  57. data/test/test_helper.rb +9 -0
  58. data/wreq.gemspec +68 -0
  59. metadata +112 -0
data/src/client/req.rs ADDED
@@ -0,0 +1,251 @@
1
+ use std::{net::IpAddr, time::Duration};
2
+
3
+ use http::{HeaderValue, header};
4
+ use magnus::{RHash, TryConvert, typed_data::Obj, value::ReprValue};
5
+ use serde::Deserialize;
6
+ use wreq::{
7
+ Client, Proxy, Version,
8
+ header::{HeaderMap, OrigHeaderMap},
9
+ };
10
+
11
+ use super::body::{Body, Form, Json};
12
+ use crate::{
13
+ client::{query::Query, resp::Response},
14
+ emulation::Emulation,
15
+ error::wreq_error_to_magnus,
16
+ extractor::Extractor,
17
+ http::Method,
18
+ rt,
19
+ };
20
+
21
+ /// The parameters for a request.
22
+ #[derive(Default, Deserialize)]
23
+ #[non_exhaustive]
24
+ pub struct Request {
25
+ /// The emulation option for the request.
26
+ #[serde(skip)]
27
+ emulation: Option<Emulation>,
28
+
29
+ /// The proxy to use for the request.
30
+ #[serde(skip)]
31
+ proxy: Option<Proxy>,
32
+
33
+ /// Bind to a local IP Address.
34
+ local_address: Option<IpAddr>,
35
+
36
+ /// Bind to an interface by `SO_BINDTODEVICE`.
37
+ interface: Option<String>,
38
+
39
+ /// The timeout to use for the request.
40
+ timeout: Option<u64>,
41
+
42
+ /// The read timeout to use for the request.
43
+ read_timeout: Option<u64>,
44
+
45
+ /// The HTTP version to use for the request.
46
+ #[serde(skip)]
47
+ version: Option<Version>,
48
+
49
+ /// The headers to use for the request.
50
+ #[serde(skip)]
51
+ headers: Option<HeaderMap>,
52
+
53
+ /// The original headers to use for the request.
54
+ #[serde(skip)]
55
+ orig_headers: Option<OrigHeaderMap>,
56
+
57
+ /// The option enables default headers.
58
+ default_headers: Option<bool>,
59
+
60
+ /// The cookies to use for the request.
61
+ #[serde(skip)]
62
+ cookies: Option<Vec<HeaderValue>>,
63
+
64
+ /// Whether to allow redirects.
65
+ allow_redirects: Option<bool>,
66
+
67
+ /// The maximum number of redirects to follow.
68
+ max_redirects: Option<usize>,
69
+
70
+ /// Sets gzip as an accepted encoding.
71
+ gzip: Option<bool>,
72
+
73
+ /// Sets brotli as an accepted encoding.
74
+ brotli: Option<bool>,
75
+
76
+ /// Sets deflate as an accepted encoding.
77
+ deflate: Option<bool>,
78
+
79
+ /// Sets zstd as an accepted encoding.
80
+ zstd: Option<bool>,
81
+
82
+ /// The authentication to use for the request.
83
+ auth: Option<String>,
84
+
85
+ /// The bearer authentication to use for the request.
86
+ bearer_auth: Option<String>,
87
+
88
+ /// The basic authentication to use for the request.
89
+ basic_auth: Option<(String, Option<String>)>,
90
+
91
+ /// The query parameters to use for the request.
92
+ query: Option<Query>,
93
+
94
+ /// The form parameters to use for the request.
95
+ form: Option<Form>,
96
+
97
+ /// The JSON body to use for the request.
98
+ json: Option<Json>,
99
+
100
+ /// The body to use for the request.
101
+ #[serde(skip)]
102
+ body: Option<Body>,
103
+ }
104
+
105
+ impl Request {
106
+ /// Create a new [`Request`] from Ruby keyword arguments.
107
+ pub fn new(ruby: &magnus::Ruby, hash: RHash) -> Result<Self, magnus::Error> {
108
+ let kwargs = hash.as_value();
109
+ let mut builder: Self = serde_magnus::deserialize(ruby, kwargs)?;
110
+
111
+ // extra emulation handling
112
+ if let Some(v) = hash.get(ruby.to_symbol("emulation")) {
113
+ let emulation_obj = Obj::<Emulation>::try_convert(v)?;
114
+ builder.emulation = Some((*emulation_obj).clone());
115
+ }
116
+
117
+ // extra version handling
118
+ builder.version = Extractor::<Version>::try_convert(kwargs)?.into_inner();
119
+
120
+ // extra headers handling
121
+ builder.headers = Extractor::<HeaderMap>::try_convert(kwargs)?.into_inner();
122
+
123
+ // extra original headers handling
124
+ builder.orig_headers = Extractor::<OrigHeaderMap>::try_convert(kwargs)?.into_inner();
125
+
126
+ // extra cookies handling
127
+ builder.cookies = Extractor::<Vec<HeaderValue>>::try_convert(kwargs)?.into_inner();
128
+
129
+ // extra proxy handling
130
+ builder.proxy = Extractor::<Proxy>::try_convert(kwargs)?.into_inner();
131
+
132
+ // extra body handling
133
+ if let Some(body) = hash.get(ruby.to_symbol("body")) {
134
+ builder.body = Some(Body::try_convert(body)?);
135
+ }
136
+
137
+ Ok(builder)
138
+ }
139
+ }
140
+
141
+ pub fn execute_request<U: AsRef<str>>(
142
+ client: Client,
143
+ method: Method,
144
+ url: U,
145
+ mut request: Request,
146
+ ) -> Result<Response, magnus::Error> {
147
+ rt::try_block_on(async move {
148
+ let mut builder = client.request(method.into_ffi(), url.as_ref());
149
+
150
+ // Emulation options.
151
+ apply_option!(set_if_some_inner, builder, request.emulation, emulation);
152
+
153
+ // Version options.
154
+ apply_option!(set_if_some, builder, request.version, version);
155
+
156
+ // Timeout options.
157
+ apply_option!(
158
+ set_if_some_map,
159
+ builder,
160
+ request.timeout,
161
+ timeout,
162
+ Duration::from_secs
163
+ );
164
+ apply_option!(
165
+ set_if_some_map,
166
+ builder,
167
+ request.read_timeout,
168
+ read_timeout,
169
+ Duration::from_secs
170
+ );
171
+
172
+ // Network options.
173
+ apply_option!(set_if_some, builder, request.proxy, proxy);
174
+ apply_option!(set_if_some, builder, request.local_address, local_address);
175
+ apply_option!(set_if_some, builder, request.interface, interface);
176
+
177
+ // Headers options.
178
+ apply_option!(set_if_some, builder, request.headers, headers);
179
+ apply_option!(set_if_some, builder, request.orig_headers, orig_headers);
180
+ apply_option!(
181
+ set_if_some,
182
+ builder,
183
+ request.default_headers,
184
+ default_headers
185
+ );
186
+
187
+ // Authentication options.
188
+ apply_option!(
189
+ set_if_some_map_ref,
190
+ builder,
191
+ request.auth,
192
+ auth,
193
+ AsRef::<str>::as_ref
194
+ );
195
+ apply_option!(set_if_some, builder, request.bearer_auth, bearer_auth);
196
+ if let Some(basic_auth) = request.basic_auth.take() {
197
+ builder = builder.basic_auth(basic_auth.0, basic_auth.1);
198
+ }
199
+
200
+ // Cookies options.
201
+ if let Some(cookies) = request.cookies.take() {
202
+ for cookie in cookies {
203
+ builder = builder.header(header::COOKIE, cookie);
204
+ }
205
+ }
206
+
207
+ // Allow redirects options.
208
+ match request.allow_redirects {
209
+ Some(false) => {
210
+ builder = builder.redirect(wreq::redirect::Policy::none());
211
+ }
212
+ Some(true) => {
213
+ builder = builder.redirect(
214
+ request
215
+ .max_redirects
216
+ .take()
217
+ .map(wreq::redirect::Policy::limited)
218
+ .unwrap_or_default(),
219
+ );
220
+ }
221
+ None => {}
222
+ };
223
+
224
+ // Compression options.
225
+ apply_option!(set_if_some, builder, request.gzip, gzip);
226
+ apply_option!(set_if_some, builder, request.brotli, brotli);
227
+ apply_option!(set_if_some, builder, request.deflate, deflate);
228
+ apply_option!(set_if_some, builder, request.zstd, zstd);
229
+
230
+ // Query options.
231
+ apply_option!(set_if_some_ref, builder, request.query, query);
232
+
233
+ // Form options.
234
+ apply_option!(set_if_some_ref, builder, request.form, form);
235
+
236
+ // JSON options.
237
+ apply_option!(set_if_some_ref, builder, request.json, json);
238
+
239
+ // Body options.
240
+ if let Some(body) = request.body.take() {
241
+ builder = builder.body(wreq::Body::from(body));
242
+ }
243
+
244
+ // Send request.
245
+ builder
246
+ .send()
247
+ .await
248
+ .map(Response::new)
249
+ .map_err(wreq_error_to_magnus)
250
+ })
251
+ }
@@ -0,0 +1,250 @@
1
+ use std::{net::SocketAddr, sync::Arc};
2
+
3
+ use arc_swap::ArcSwapOption;
4
+ use bytes::Bytes;
5
+ use futures_util::TryFutureExt;
6
+ use http::{Extensions, HeaderMap, response::Response as HttpResponse};
7
+ use http_body_util::BodyExt;
8
+ use magnus::{Error, Module, RArray, RModule, Ruby, Value, block::Yield};
9
+ use wreq::Uri;
10
+
11
+ use crate::{
12
+ client::body::{BodyReceiver, Json},
13
+ cookie::Cookie,
14
+ error::{memory_error, wreq_error_to_magnus},
15
+ gvl,
16
+ header::Headers,
17
+ http::{StatusCode, Version},
18
+ rt,
19
+ };
20
+
21
+ /// A response from a request.
22
+ #[magnus::wrap(class = "Wreq::Response", free_immediately, size)]
23
+ pub struct Response {
24
+ uri: Uri,
25
+ version: Version,
26
+ status: StatusCode,
27
+ content_length: Option<u64>,
28
+ headers: HeaderMap,
29
+ local_addr: Option<SocketAddr>,
30
+ remote_addr: Option<SocketAddr>,
31
+ body: ArcSwapOption<Body>,
32
+ extensions: Extensions,
33
+ }
34
+
35
+ /// Represents the state of the HTTP response body.
36
+ enum Body {
37
+ /// The body can be streamed once (not yet buffered).
38
+ Streamable(wreq::Body),
39
+ /// The body has been fully read into memory and can be reused.
40
+ Reusable(Bytes),
41
+ }
42
+
43
+ impl Response {
44
+ /// Create a new [`Response`] instance.
45
+ pub fn new(response: wreq::Response) -> Self {
46
+ let uri = response.uri().clone();
47
+ let content_length = response.content_length();
48
+ let local_addr = response.local_addr();
49
+ let remote_addr = response.remote_addr();
50
+ let response = HttpResponse::from(response);
51
+ let (parts, body) = response.into_parts();
52
+
53
+ Response {
54
+ uri,
55
+ local_addr,
56
+ remote_addr,
57
+ content_length,
58
+ extensions: parts.extensions,
59
+ version: Version::from_ffi(parts.version),
60
+ status: StatusCode::from(parts.status),
61
+ headers: parts.headers,
62
+ body: ArcSwapOption::from_pointee(Body::Streamable(body)),
63
+ }
64
+ }
65
+
66
+ /// Internal method to get the wreq::Response, optionally streaming the body.
67
+ fn response(&self, stream: bool) -> Result<wreq::Response, Error> {
68
+ let build_response = |body: wreq::Body| -> wreq::Response {
69
+ let mut response = HttpResponse::new(body);
70
+ *response.version_mut() = self.version.into_ffi();
71
+ *response.status_mut() = self.status.0;
72
+ *response.headers_mut() = self.headers.clone();
73
+ *response.extensions_mut() = self.extensions.clone();
74
+ wreq::Response::from(response)
75
+ };
76
+
77
+ if let Some(arc) = self.body.swap(None) {
78
+ match Arc::try_unwrap(arc) {
79
+ Ok(Body::Streamable(body)) => {
80
+ return if stream {
81
+ Ok(build_response(body))
82
+ } else {
83
+ let bytes = rt::try_block_on(
84
+ BodyExt::collect(body)
85
+ .map_ok(|buf| buf.to_bytes())
86
+ .map_err(wreq_error_to_magnus),
87
+ )?;
88
+
89
+ self.body
90
+ .store(Some(Arc::new(Body::Reusable(bytes.clone()))));
91
+
92
+ Ok(build_response(wreq::Body::from(bytes)))
93
+ };
94
+ }
95
+ Ok(Body::Reusable(bytes)) => {
96
+ self.body
97
+ .store(Some(Arc::new(Body::Reusable(bytes.clone()))));
98
+
99
+ if !stream {
100
+ return Ok(build_response(wreq::Body::from(bytes)));
101
+ }
102
+ }
103
+ _ => {}
104
+ };
105
+ }
106
+
107
+ Err(memory_error())
108
+ }
109
+ }
110
+
111
+ impl Response {
112
+ /// Get the response status code as a u16.
113
+ #[inline]
114
+ pub fn code(&self) -> u16 {
115
+ self.status.0.as_u16()
116
+ }
117
+
118
+ /// Get the response status code.
119
+ #[inline]
120
+ pub fn status(&self) -> StatusCode {
121
+ self.status
122
+ }
123
+
124
+ /// Get the response HTTP version.
125
+ #[inline]
126
+ pub fn version(&self) -> Version {
127
+ self.version
128
+ }
129
+
130
+ /// Get the response URL.
131
+ #[inline]
132
+ pub fn url(&self) -> String {
133
+ self.uri.to_string()
134
+ }
135
+
136
+ /// Get the content length of the response, if known.
137
+ #[inline]
138
+ pub fn content_length(&self) -> Option<u64> {
139
+ self.content_length
140
+ }
141
+
142
+ /// Get the response cookies.
143
+ pub fn cookies(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
144
+ let cookies = Cookie::extract_headers_cookies(&rb_self.headers);
145
+ let ary = ruby.ary_new_capa(cookies.len());
146
+ for cookie in cookies {
147
+ ary.push(cookie)?;
148
+ }
149
+ Ok(ary)
150
+ }
151
+
152
+ /// Get the response headers.
153
+ #[inline]
154
+ pub fn headers(&self) -> Headers {
155
+ Headers::from(self.headers.clone())
156
+ }
157
+
158
+ /// Get the local socket address, if available.
159
+ #[inline]
160
+ pub fn local_addr(&self) -> Option<String> {
161
+ self.local_addr.map(|addr| addr.to_string())
162
+ }
163
+
164
+ /// Get the remote socket address, if available.
165
+ #[inline]
166
+ pub fn remote_addr(&self) -> Option<String> {
167
+ self.remote_addr.map(|addr| addr.to_string())
168
+ }
169
+
170
+ /// Get the response body as bytes.
171
+ pub fn bytes(&self) -> Result<Bytes, Error> {
172
+ let response = self.response(false)?;
173
+ rt::try_block_on(response.bytes().map_err(wreq_error_to_magnus))
174
+ }
175
+
176
+ /// Get the response body as a UTF-8 string.
177
+ pub fn text(&self) -> Result<String, Error> {
178
+ let response = self.response(false)?;
179
+ rt::try_block_on(response.text().map_err(wreq_error_to_magnus))
180
+ }
181
+
182
+ /// Get the full response text given a specific encoding.
183
+ pub fn text_with_charset(&self, default_encoding: String) -> Result<String, Error> {
184
+ let response = self.response(false)?;
185
+ rt::try_block_on(
186
+ response
187
+ .text_with_charset(default_encoding)
188
+ .map_err(wreq_error_to_magnus),
189
+ )
190
+ }
191
+
192
+ /// Get the response body as JSON.
193
+ pub fn json(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
194
+ let response = rb_self.response(false)?;
195
+ rt::try_block_on(async move {
196
+ let json = response
197
+ .json::<Json>()
198
+ .await
199
+ .map_err(wreq_error_to_magnus)?;
200
+ serde_magnus::serialize(ruby, &json)
201
+ })
202
+ }
203
+
204
+ /// Get a chunk iterator for the response body.
205
+ pub fn chunks(&self) -> Result<Yield<BodyReceiver>, Error> {
206
+ self.response(true)
207
+ .map(wreq::Response::bytes_stream)
208
+ .map(BodyReceiver::new)
209
+ .map(Yield::Iter)
210
+ }
211
+
212
+ /// Close the response body, dropping any resources.
213
+ #[inline]
214
+ pub fn close(&self) {
215
+ gvl::nogvl(|| self.body.swap(None));
216
+ }
217
+ }
218
+
219
+ impl Drop for Response {
220
+ fn drop(&mut self) {
221
+ // Ensure body is dropped in GVL
222
+ self.body.swap(None);
223
+ }
224
+ }
225
+
226
+ pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
227
+ let response_class = gem_module.define_class("Response", ruby.class_object())?;
228
+ response_class.define_method("code", magnus::method!(Response::code, 0))?;
229
+ response_class.define_method("status", magnus::method!(Response::status, 0))?;
230
+ response_class.define_method("version", magnus::method!(Response::version, 0))?;
231
+ response_class.define_method("url", magnus::method!(Response::url, 0))?;
232
+ response_class.define_method(
233
+ "content_length",
234
+ magnus::method!(Response::content_length, 0),
235
+ )?;
236
+ response_class.define_method("cookies", magnus::method!(Response::cookies, 0))?;
237
+ response_class.define_method("headers", magnus::method!(Response::headers, 0))?;
238
+ response_class.define_method("local_addr", magnus::method!(Response::local_addr, 0))?;
239
+ response_class.define_method("remote_addr", magnus::method!(Response::remote_addr, 0))?;
240
+ response_class.define_method("bytes", magnus::method!(Response::bytes, 0))?;
241
+ response_class.define_method("text", magnus::method!(Response::text, 0))?;
242
+ response_class.define_method(
243
+ "text_with_charset",
244
+ magnus::method!(Response::text_with_charset, 1),
245
+ )?;
246
+ response_class.define_method("json", magnus::method!(Response::json, 0))?;
247
+ response_class.define_method("chunks", magnus::method!(Response::chunks, 0))?;
248
+ response_class.define_method("close", magnus::method!(Response::close, 0))?;
249
+ Ok(())
250
+ }