wreq-rb 0.3.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 (167) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +2688 -0
  3. data/Cargo.toml +6 -0
  4. data/README.md +179 -0
  5. data/ext/wreq_rb/Cargo.toml +39 -0
  6. data/ext/wreq_rb/extconf.rb +22 -0
  7. data/ext/wreq_rb/src/client.rs +565 -0
  8. data/ext/wreq_rb/src/error.rs +25 -0
  9. data/ext/wreq_rb/src/lib.rs +20 -0
  10. data/ext/wreq_rb/src/response.rs +132 -0
  11. data/lib/wreq-rb/version.rb +5 -0
  12. data/lib/wreq-rb.rb +17 -0
  13. data/patches/0001-add-transfer-size-tracking.patch +292 -0
  14. data/vendor/wreq/Cargo.toml +306 -0
  15. data/vendor/wreq/LICENSE +202 -0
  16. data/vendor/wreq/README.md +122 -0
  17. data/vendor/wreq/examples/cert_store.rs +77 -0
  18. data/vendor/wreq/examples/connect_via_lower_priority_tokio_runtime.rs +258 -0
  19. data/vendor/wreq/examples/emulation.rs +118 -0
  20. data/vendor/wreq/examples/form.rs +14 -0
  21. data/vendor/wreq/examples/http1_websocket.rs +37 -0
  22. data/vendor/wreq/examples/http2_websocket.rs +45 -0
  23. data/vendor/wreq/examples/json_dynamic.rs +41 -0
  24. data/vendor/wreq/examples/json_typed.rs +47 -0
  25. data/vendor/wreq/examples/keylog.rs +16 -0
  26. data/vendor/wreq/examples/request_with_emulation.rs +115 -0
  27. data/vendor/wreq/examples/request_with_interface.rs +37 -0
  28. data/vendor/wreq/examples/request_with_local_address.rs +16 -0
  29. data/vendor/wreq/examples/request_with_proxy.rs +13 -0
  30. data/vendor/wreq/examples/request_with_redirect.rs +22 -0
  31. data/vendor/wreq/examples/request_with_version.rs +15 -0
  32. data/vendor/wreq/examples/tor_socks.rs +24 -0
  33. data/vendor/wreq/examples/unix_socket.rs +33 -0
  34. data/vendor/wreq/src/client/body.rs +304 -0
  35. data/vendor/wreq/src/client/conn/conn.rs +231 -0
  36. data/vendor/wreq/src/client/conn/connector.rs +549 -0
  37. data/vendor/wreq/src/client/conn/http.rs +1023 -0
  38. data/vendor/wreq/src/client/conn/proxy/socks.rs +233 -0
  39. data/vendor/wreq/src/client/conn/proxy/tunnel.rs +260 -0
  40. data/vendor/wreq/src/client/conn/proxy.rs +39 -0
  41. data/vendor/wreq/src/client/conn/tls_info.rs +98 -0
  42. data/vendor/wreq/src/client/conn/uds.rs +44 -0
  43. data/vendor/wreq/src/client/conn/verbose.rs +149 -0
  44. data/vendor/wreq/src/client/conn.rs +323 -0
  45. data/vendor/wreq/src/client/core/body/incoming.rs +485 -0
  46. data/vendor/wreq/src/client/core/body/length.rs +118 -0
  47. data/vendor/wreq/src/client/core/body.rs +34 -0
  48. data/vendor/wreq/src/client/core/common/buf.rs +149 -0
  49. data/vendor/wreq/src/client/core/common/rewind.rs +141 -0
  50. data/vendor/wreq/src/client/core/common/watch.rs +76 -0
  51. data/vendor/wreq/src/client/core/common.rs +3 -0
  52. data/vendor/wreq/src/client/core/conn/http1.rs +342 -0
  53. data/vendor/wreq/src/client/core/conn/http2.rs +307 -0
  54. data/vendor/wreq/src/client/core/conn.rs +11 -0
  55. data/vendor/wreq/src/client/core/dispatch.rs +299 -0
  56. data/vendor/wreq/src/client/core/error.rs +435 -0
  57. data/vendor/wreq/src/client/core/ext.rs +201 -0
  58. data/vendor/wreq/src/client/core/http1.rs +178 -0
  59. data/vendor/wreq/src/client/core/http2.rs +483 -0
  60. data/vendor/wreq/src/client/core/proto/h1/conn.rs +988 -0
  61. data/vendor/wreq/src/client/core/proto/h1/decode.rs +1170 -0
  62. data/vendor/wreq/src/client/core/proto/h1/dispatch.rs +684 -0
  63. data/vendor/wreq/src/client/core/proto/h1/encode.rs +580 -0
  64. data/vendor/wreq/src/client/core/proto/h1/io.rs +879 -0
  65. data/vendor/wreq/src/client/core/proto/h1/role.rs +694 -0
  66. data/vendor/wreq/src/client/core/proto/h1.rs +104 -0
  67. data/vendor/wreq/src/client/core/proto/h2/client.rs +650 -0
  68. data/vendor/wreq/src/client/core/proto/h2/ping.rs +539 -0
  69. data/vendor/wreq/src/client/core/proto/h2.rs +379 -0
  70. data/vendor/wreq/src/client/core/proto/headers.rs +138 -0
  71. data/vendor/wreq/src/client/core/proto.rs +58 -0
  72. data/vendor/wreq/src/client/core/rt/bounds.rs +57 -0
  73. data/vendor/wreq/src/client/core/rt/timer.rs +150 -0
  74. data/vendor/wreq/src/client/core/rt/tokio.rs +99 -0
  75. data/vendor/wreq/src/client/core/rt.rs +25 -0
  76. data/vendor/wreq/src/client/core/upgrade.rs +267 -0
  77. data/vendor/wreq/src/client/core.rs +16 -0
  78. data/vendor/wreq/src/client/emulation.rs +161 -0
  79. data/vendor/wreq/src/client/http/client/error.rs +142 -0
  80. data/vendor/wreq/src/client/http/client/exec.rs +29 -0
  81. data/vendor/wreq/src/client/http/client/extra.rs +77 -0
  82. data/vendor/wreq/src/client/http/client/lazy.rs +79 -0
  83. data/vendor/wreq/src/client/http/client/pool.rs +1105 -0
  84. data/vendor/wreq/src/client/http/client/util.rs +104 -0
  85. data/vendor/wreq/src/client/http/client.rs +1003 -0
  86. data/vendor/wreq/src/client/http/future.rs +99 -0
  87. data/vendor/wreq/src/client/http.rs +1629 -0
  88. data/vendor/wreq/src/client/layer/config/options.rs +156 -0
  89. data/vendor/wreq/src/client/layer/config.rs +116 -0
  90. data/vendor/wreq/src/client/layer/cookie.rs +161 -0
  91. data/vendor/wreq/src/client/layer/decoder.rs +139 -0
  92. data/vendor/wreq/src/client/layer/redirect/future.rs +270 -0
  93. data/vendor/wreq/src/client/layer/redirect/policy.rs +63 -0
  94. data/vendor/wreq/src/client/layer/redirect.rs +145 -0
  95. data/vendor/wreq/src/client/layer/retry/classify.rs +105 -0
  96. data/vendor/wreq/src/client/layer/retry/scope.rs +51 -0
  97. data/vendor/wreq/src/client/layer/retry.rs +151 -0
  98. data/vendor/wreq/src/client/layer/timeout/body.rs +233 -0
  99. data/vendor/wreq/src/client/layer/timeout/future.rs +90 -0
  100. data/vendor/wreq/src/client/layer/timeout.rs +177 -0
  101. data/vendor/wreq/src/client/layer.rs +15 -0
  102. data/vendor/wreq/src/client/multipart.rs +717 -0
  103. data/vendor/wreq/src/client/request.rs +818 -0
  104. data/vendor/wreq/src/client/response.rs +534 -0
  105. data/vendor/wreq/src/client/ws/json.rs +99 -0
  106. data/vendor/wreq/src/client/ws/message.rs +453 -0
  107. data/vendor/wreq/src/client/ws.rs +714 -0
  108. data/vendor/wreq/src/client.rs +27 -0
  109. data/vendor/wreq/src/config.rs +140 -0
  110. data/vendor/wreq/src/cookie.rs +579 -0
  111. data/vendor/wreq/src/dns/gai.rs +249 -0
  112. data/vendor/wreq/src/dns/hickory.rs +78 -0
  113. data/vendor/wreq/src/dns/resolve.rs +180 -0
  114. data/vendor/wreq/src/dns.rs +69 -0
  115. data/vendor/wreq/src/error.rs +502 -0
  116. data/vendor/wreq/src/ext.rs +398 -0
  117. data/vendor/wreq/src/hash.rs +143 -0
  118. data/vendor/wreq/src/header.rs +506 -0
  119. data/vendor/wreq/src/into_uri.rs +187 -0
  120. data/vendor/wreq/src/lib.rs +586 -0
  121. data/vendor/wreq/src/proxy/mac.rs +82 -0
  122. data/vendor/wreq/src/proxy/matcher.rs +806 -0
  123. data/vendor/wreq/src/proxy/uds.rs +66 -0
  124. data/vendor/wreq/src/proxy/win.rs +31 -0
  125. data/vendor/wreq/src/proxy.rs +569 -0
  126. data/vendor/wreq/src/redirect.rs +575 -0
  127. data/vendor/wreq/src/retry.rs +198 -0
  128. data/vendor/wreq/src/sync.rs +129 -0
  129. data/vendor/wreq/src/tls/conn/cache.rs +123 -0
  130. data/vendor/wreq/src/tls/conn/cert_compression.rs +125 -0
  131. data/vendor/wreq/src/tls/conn/ext.rs +82 -0
  132. data/vendor/wreq/src/tls/conn/macros.rs +34 -0
  133. data/vendor/wreq/src/tls/conn/service.rs +138 -0
  134. data/vendor/wreq/src/tls/conn.rs +681 -0
  135. data/vendor/wreq/src/tls/keylog/handle.rs +64 -0
  136. data/vendor/wreq/src/tls/keylog.rs +99 -0
  137. data/vendor/wreq/src/tls/options.rs +464 -0
  138. data/vendor/wreq/src/tls/x509/identity.rs +122 -0
  139. data/vendor/wreq/src/tls/x509/parser.rs +71 -0
  140. data/vendor/wreq/src/tls/x509/store.rs +228 -0
  141. data/vendor/wreq/src/tls/x509.rs +68 -0
  142. data/vendor/wreq/src/tls.rs +154 -0
  143. data/vendor/wreq/src/trace.rs +55 -0
  144. data/vendor/wreq/src/util.rs +122 -0
  145. data/vendor/wreq/tests/badssl.rs +228 -0
  146. data/vendor/wreq/tests/brotli.rs +350 -0
  147. data/vendor/wreq/tests/client.rs +1098 -0
  148. data/vendor/wreq/tests/connector_layers.rs +227 -0
  149. data/vendor/wreq/tests/cookie.rs +306 -0
  150. data/vendor/wreq/tests/deflate.rs +347 -0
  151. data/vendor/wreq/tests/emulation.rs +260 -0
  152. data/vendor/wreq/tests/gzip.rs +347 -0
  153. data/vendor/wreq/tests/layers.rs +261 -0
  154. data/vendor/wreq/tests/multipart.rs +165 -0
  155. data/vendor/wreq/tests/proxy.rs +438 -0
  156. data/vendor/wreq/tests/redirect.rs +629 -0
  157. data/vendor/wreq/tests/retry.rs +135 -0
  158. data/vendor/wreq/tests/support/delay_server.rs +117 -0
  159. data/vendor/wreq/tests/support/error.rs +16 -0
  160. data/vendor/wreq/tests/support/layer.rs +183 -0
  161. data/vendor/wreq/tests/support/mod.rs +9 -0
  162. data/vendor/wreq/tests/support/server.rs +232 -0
  163. data/vendor/wreq/tests/timeouts.rs +281 -0
  164. data/vendor/wreq/tests/unix_socket.rs +135 -0
  165. data/vendor/wreq/tests/upgrade.rs +98 -0
  166. data/vendor/wreq/tests/zstd.rs +559 -0
  167. metadata +225 -0
@@ -0,0 +1,565 @@
1
+ use std::ffi::c_void;
2
+ use std::panic::{self, AssertUnwindSafe};
3
+ use std::ptr;
4
+ use std::any::Any;
5
+ use std::time::Duration;
6
+
7
+ use magnus::{
8
+ function, method, prelude::*, Module, RArray, RHash, Ruby,
9
+ try_convert::TryConvert, Value,
10
+ };
11
+ use tokio::runtime::Runtime;
12
+ use tokio_util::sync::CancellationToken;
13
+ use wreq::header::{HeaderMap, HeaderName, HeaderValue};
14
+ use wreq_util::Emulation as BrowserEmulation;
15
+
16
+ use crate::error::{generic_error, to_magnus_error};
17
+ use crate::response::Response;
18
+
19
+ // --------------------------------------------------------------------------
20
+ // Shared Tokio runtime
21
+ // --------------------------------------------------------------------------
22
+
23
+ fn runtime() -> &'static Runtime {
24
+ use std::sync::OnceLock;
25
+ static RT: OnceLock<Runtime> = OnceLock::new();
26
+ RT.get_or_init(|| {
27
+ tokio::runtime::Builder::new_multi_thread()
28
+ .enable_all()
29
+ .build()
30
+ .expect("failed to build tokio runtime")
31
+ })
32
+ }
33
+
34
+ // --------------------------------------------------------------------------
35
+ // GVL release helper
36
+ // --------------------------------------------------------------------------
37
+
38
+ /// Run a closure without the Ruby GVL, allowing other Ruby threads to execute.
39
+ /// The closure receives a `CancellationToken` that is cancelled if Ruby
40
+ /// interrupts the thread (e.g. `Thread.kill`, signal, timeout).
41
+ ///
42
+ /// # Safety
43
+ /// The closure must NOT access any Ruby objects or call any Ruby C API.
44
+ /// Extract all data from Ruby before calling this, convert results after.
45
+ unsafe fn without_gvl<F, R>(f: F) -> R
46
+ where
47
+ F: FnOnce(CancellationToken) -> R,
48
+ {
49
+ struct CallData<F, R> {
50
+ func: Option<F>,
51
+ result: Option<R>,
52
+ token: CancellationToken,
53
+ panic_payload: Option<Box<dyn Any + Send>>,
54
+ }
55
+
56
+ unsafe extern "C" fn call<F, R>(data: *mut c_void) -> *mut c_void
57
+ where
58
+ F: FnOnce(CancellationToken) -> R,
59
+ {
60
+ let d = data as *mut CallData<F, R>;
61
+ let f = (*d).func.take().unwrap();
62
+ let token = (*d).token.clone();
63
+ // catch_unwind prevents a panic from unwinding through C frames (UB).
64
+ match panic::catch_unwind(AssertUnwindSafe(|| f(token))) {
65
+ Ok(val) => ptr::write(&mut (*d).result, Some(val)),
66
+ Err(payload) => (*d).panic_payload = Some(payload),
67
+ }
68
+ ptr::null_mut()
69
+ }
70
+
71
+ /// Unblock function called by Ruby when it wants to interrupt this thread.
72
+ /// Cancels the token so the in-flight async work can abort promptly.
73
+ unsafe extern "C" fn ubf<F, R>(data: *mut c_void) {
74
+ let d = data as *const CallData<F, R>;
75
+ (*d).token.cancel();
76
+ }
77
+
78
+ let mut data = CallData {
79
+ func: Some(f),
80
+ result: None,
81
+ token: CancellationToken::new(),
82
+ panic_payload: None,
83
+ };
84
+ let data_ptr = &mut data as *mut CallData<F, R> as *mut c_void;
85
+
86
+ unsafe {
87
+ rb_sys::rb_thread_call_without_gvl(
88
+ Some(call::<F, R>),
89
+ data_ptr,
90
+ Some(ubf::<F, R>),
91
+ data_ptr,
92
+ );
93
+ }
94
+
95
+ if let Some(payload) = data.panic_payload {
96
+ panic::resume_unwind(payload);
97
+ }
98
+
99
+ data.result.unwrap()
100
+ }
101
+
102
+ /// Collected response data as pure Rust types (no Ruby objects).
103
+ struct ResponseData {
104
+ status: u16,
105
+ headers: Vec<(String, String)>,
106
+ body: Vec<u8>,
107
+ url: String,
108
+ version: String,
109
+ content_length: Option<u64>,
110
+ transfer_size: Option<u64>,
111
+ }
112
+
113
+ /// Outcome of the network call performed outside the GVL.
114
+ enum RequestOutcome {
115
+ Ok(ResponseData),
116
+ Err(wreq::Error),
117
+ Interrupted,
118
+ }
119
+
120
+ /// Execute a request and collect the full response as pure Rust types.
121
+ async fn execute_request(req: wreq::RequestBuilder) -> Result<ResponseData, wreq::Error> {
122
+ let resp = req.send().await?;
123
+ let status = resp.status().as_u16();
124
+ let url = resp.uri().to_string();
125
+ let version = format!("{:?}", resp.version());
126
+ let content_length = resp.content_length();
127
+ let headers: Vec<(String, String)> = resp
128
+ .headers()
129
+ .iter()
130
+ .map(|(k, v)| (k.as_str().to_owned(), v.to_str().unwrap_or("").to_owned()))
131
+ .collect();
132
+ let transfer_size_handle = resp.transfer_size_handle().cloned();
133
+ let body = resp.bytes().await?.to_vec();
134
+ let transfer_size = transfer_size_handle.map(|h| h.get());
135
+ Ok(ResponseData { status, headers, body, url, version, content_length, transfer_size })
136
+ }
137
+
138
+ // --------------------------------------------------------------------------
139
+ // Emulation helpers
140
+ // --------------------------------------------------------------------------
141
+
142
+ /// The default emulation to apply when none is specified.
143
+ const DEFAULT_EMULATION: BrowserEmulation = BrowserEmulation::Chrome143;
144
+
145
+ /// Parse a Ruby string like "chrome_143" into a BrowserEmulation variant.
146
+ fn parse_emulation(name: &str) -> Result<BrowserEmulation, magnus::Error> {
147
+ let json_val = serde_json::Value::String(name.to_string());
148
+ serde_json::from_value::<BrowserEmulation>(json_val)
149
+ .map_err(|_| generic_error(format!("unknown emulation: '{}'. Use names like 'chrome_143', 'firefox_146', 'safari_18_5', etc.", name)))
150
+ }
151
+
152
+ // --------------------------------------------------------------------------
153
+ // Ruby Client
154
+ // --------------------------------------------------------------------------
155
+
156
+ #[magnus::wrap(class = "Wreq::Client", free_immediately)]
157
+ struct Client {
158
+ inner: wreq::Client,
159
+ }
160
+
161
+ impl Client {
162
+ /// Wreq::Client.new or Wreq::Client.new(options_hash)
163
+ fn rb_new(args: &[Value]) -> Result<Self, magnus::Error> {
164
+ let opts: Option<RHash> = if args.is_empty() {
165
+ None
166
+ } else {
167
+ Some(RHash::try_convert(args[0])?)
168
+ };
169
+
170
+ let mut builder = wreq::Client::builder();
171
+
172
+ if let Some(opts) = opts {
173
+ if let Some(val) = hash_get_value(&opts, "emulation")? {
174
+ let ruby = unsafe { Ruby::get_unchecked() };
175
+ if val.is_kind_of(ruby.class_false_class()) {
176
+ // emulation: false — skip emulation entirely
177
+ } else if val.is_kind_of(ruby.class_true_class()) {
178
+ builder = builder.emulation(DEFAULT_EMULATION);
179
+ } else {
180
+ let name: String = TryConvert::try_convert(val)?;
181
+ let emu = parse_emulation(&name)?;
182
+ builder = builder.emulation(emu);
183
+ }
184
+ } else {
185
+ builder = builder.emulation(DEFAULT_EMULATION);
186
+ }
187
+
188
+ if let Some(ua) = hash_get_string(&opts, "user_agent")? {
189
+ builder = builder.user_agent(ua);
190
+ }
191
+
192
+ if let Some(hdr_hash) = hash_get_hash(&opts, "headers")? {
193
+ let hmap = hash_to_header_map(&hdr_hash)?;
194
+ builder = builder.default_headers(hmap);
195
+ }
196
+
197
+ if let Some(t) = hash_get_float(&opts, "timeout")? {
198
+ builder = builder.timeout(Duration::from_secs_f64(t));
199
+ }
200
+
201
+ if let Some(t) = hash_get_float(&opts, "connect_timeout")? {
202
+ builder = builder.connect_timeout(Duration::from_secs_f64(t));
203
+ }
204
+
205
+ if let Some(t) = hash_get_float(&opts, "read_timeout")? {
206
+ builder = builder.read_timeout(Duration::from_secs_f64(t));
207
+ }
208
+
209
+ if let Some(val) = hash_get_value(&opts, "redirect")? {
210
+ let ruby = unsafe { Ruby::get_unchecked() };
211
+ if val.is_kind_of(ruby.class_false_class()) {
212
+ builder = builder.redirect(wreq::redirect::Policy::none());
213
+ } else if val.is_kind_of(ruby.class_true_class()) {
214
+ builder = builder.redirect(wreq::redirect::Policy::limited(10));
215
+ } else {
216
+ let n: usize = TryConvert::try_convert(val)?;
217
+ builder = builder.redirect(wreq::redirect::Policy::limited(n));
218
+ }
219
+ }
220
+
221
+ if let Some(enabled) = hash_get_bool(&opts, "cookie_store")? {
222
+ builder = builder.cookie_store(enabled);
223
+ }
224
+
225
+ if let Some(proxy_url) = hash_get_string(&opts, "proxy")? {
226
+ let mut proxy = wreq::Proxy::all(&proxy_url).map_err(to_magnus_error)?;
227
+ if let (Some(user), Some(pass)) = (
228
+ hash_get_string(&opts, "proxy_user")?,
229
+ hash_get_string(&opts, "proxy_pass")?,
230
+ ) {
231
+ proxy = proxy.basic_auth(&user, &pass);
232
+ }
233
+ builder = builder.proxy(proxy);
234
+ }
235
+
236
+ if let Some(true) = hash_get_bool(&opts, "no_proxy")? {
237
+ builder = builder.no_proxy();
238
+ }
239
+
240
+ if let Some(enabled) = hash_get_bool(&opts, "https_only")? {
241
+ builder = builder.https_only(enabled);
242
+ }
243
+
244
+ if let Some(v) = hash_get_bool(&opts, "verify_host")? {
245
+ builder = builder.verify_hostname(v);
246
+ }
247
+
248
+ if let Some(v) = hash_get_bool(&opts, "verify_cert")? {
249
+ builder = builder.cert_verification(v);
250
+ }
251
+
252
+ if let Some(true) = hash_get_bool(&opts, "http1_only")? {
253
+ builder = builder.http1_only();
254
+ }
255
+ if let Some(true) = hash_get_bool(&opts, "http2_only")? {
256
+ builder = builder.http2_only();
257
+ }
258
+
259
+ if let Some(v) = hash_get_bool(&opts, "gzip")? {
260
+ builder = builder.gzip(v);
261
+ }
262
+ if let Some(v) = hash_get_bool(&opts, "brotli")? {
263
+ builder = builder.brotli(v);
264
+ }
265
+ if let Some(v) = hash_get_bool(&opts, "deflate")? {
266
+ builder = builder.deflate(v);
267
+ }
268
+ if let Some(v) = hash_get_bool(&opts, "zstd")? {
269
+ builder = builder.zstd(v);
270
+ }
271
+ } else {
272
+ builder = builder.emulation(DEFAULT_EMULATION);
273
+ }
274
+
275
+ let client = builder.build().map_err(to_magnus_error)?;
276
+ Ok(Client { inner: client })
277
+ }
278
+
279
+ /// client.get(url) or client.get(url, opts)
280
+ fn get(&self, args: &[Value]) -> Result<Response, magnus::Error> {
281
+ self.execute_method("GET", args)
282
+ }
283
+
284
+ fn post(&self, args: &[Value]) -> Result<Response, magnus::Error> {
285
+ self.execute_method("POST", args)
286
+ }
287
+
288
+ fn put(&self, args: &[Value]) -> Result<Response, magnus::Error> {
289
+ self.execute_method("PUT", args)
290
+ }
291
+
292
+ fn patch(&self, args: &[Value]) -> Result<Response, magnus::Error> {
293
+ self.execute_method("PATCH", args)
294
+ }
295
+
296
+ fn delete(&self, args: &[Value]) -> Result<Response, magnus::Error> {
297
+ self.execute_method("DELETE", args)
298
+ }
299
+
300
+ fn head(&self, args: &[Value]) -> Result<Response, magnus::Error> {
301
+ self.execute_method("HEAD", args)
302
+ }
303
+
304
+ fn options(&self, args: &[Value]) -> Result<Response, magnus::Error> {
305
+ self.execute_method("OPTIONS", args)
306
+ }
307
+
308
+ fn execute_method(&self, method_str: &str, args: &[Value]) -> Result<Response, magnus::Error> {
309
+ let url: String = if args.is_empty() {
310
+ return Err(generic_error("url is required"));
311
+ } else {
312
+ TryConvert::try_convert(args[0])?
313
+ };
314
+
315
+ let opts: Option<RHash> = if args.len() > 1 {
316
+ Some(RHash::try_convert(args[1])?)
317
+ } else {
318
+ None
319
+ };
320
+
321
+ let method: wreq::Method = method_str
322
+ .parse()
323
+ .map_err(|_| generic_error(format!("invalid HTTP method: {}", method_str)))?;
324
+
325
+ let mut req = self.inner.request(method, &url);
326
+
327
+ if let Some(opts) = opts {
328
+ req = apply_request_options(req, &opts)?;
329
+ }
330
+
331
+ // Release the GVL so other Ruby threads can run during I/O.
332
+ let outcome: RequestOutcome = unsafe {
333
+ without_gvl(|cancel| {
334
+ runtime().block_on(async {
335
+ tokio::select! {
336
+ biased;
337
+ _ = cancel.cancelled() => RequestOutcome::Interrupted,
338
+ res = execute_request(req) => match res {
339
+ Ok(data) => RequestOutcome::Ok(data),
340
+ Err(e) => RequestOutcome::Err(e),
341
+ },
342
+ }
343
+ })
344
+ })
345
+ };
346
+
347
+ let data = match outcome {
348
+ RequestOutcome::Ok(d) => d,
349
+ RequestOutcome::Err(e) => return Err(to_magnus_error(e)),
350
+ RequestOutcome::Interrupted => return Err(generic_error("request interrupted")),
351
+ };
352
+ Ok(Response::new(data.status, data.headers, data.body, data.url, data.version, data.content_length, data.transfer_size))
353
+ }
354
+ }
355
+
356
+ fn apply_request_options(
357
+ mut req: wreq::RequestBuilder,
358
+ opts: &RHash,
359
+ ) -> Result<wreq::RequestBuilder, magnus::Error> {
360
+ if let Some(hdr_hash) = hash_get_hash(opts, "headers")? {
361
+ let hmap = hash_to_header_map(&hdr_hash)?;
362
+ req = req.headers(hmap);
363
+ }
364
+
365
+ if let Some(body_str) = hash_get_string(opts, "body")? {
366
+ req = req.body(body_str);
367
+ }
368
+
369
+ if let Some(json_val) = hash_get_value(opts, "json")? {
370
+ let ruby = unsafe { Ruby::get_unchecked() };
371
+ let json_module: Value = ruby.class_object().const_get("JSON")?;
372
+ let json_str: String = json_module.funcall("generate", (json_val,))?;
373
+ req = req
374
+ .header("content-type", "application/json")
375
+ .body(json_str);
376
+ }
377
+
378
+ if let Some(form_hash) = hash_get_hash(opts, "form")? {
379
+ let pairs = hash_to_pairs(&form_hash)?;
380
+ req = req.form(&pairs);
381
+ }
382
+
383
+ if let Some(query_hash) = hash_get_hash(opts, "query")? {
384
+ let pairs = hash_to_pairs(&query_hash)?;
385
+ req = req.query(&pairs);
386
+ }
387
+
388
+ if let Some(t) = hash_get_float(opts, "timeout")? {
389
+ req = req.timeout(Duration::from_secs_f64(t));
390
+ }
391
+
392
+ if let Some(token) = hash_get_string(opts, "auth")? {
393
+ req = req.auth(token);
394
+ }
395
+
396
+ if let Some(token) = hash_get_string(opts, "bearer")? {
397
+ req = req.bearer_auth(token);
398
+ }
399
+
400
+ if let Some(basic_val) = hash_get_value(opts, "basic")? {
401
+ let ary = RArray::try_convert(basic_val)?;
402
+ if ary.len() >= 2 {
403
+ let user: String = TryConvert::try_convert(ary.entry::<Value>(0)?)?;
404
+ let pass: String = TryConvert::try_convert(ary.entry::<Value>(1)?)?;
405
+ req = req.basic_auth(user, Some(pass));
406
+ }
407
+ }
408
+
409
+ if let Some(proxy_url) = hash_get_string(opts, "proxy")? {
410
+ let proxy = wreq::Proxy::all(&proxy_url).map_err(to_magnus_error)?;
411
+ req = req.proxy(proxy);
412
+ }
413
+
414
+ if let Some(val) = hash_get_value(opts, "emulation")? {
415
+ let ruby = unsafe { Ruby::get_unchecked() };
416
+ if val.is_kind_of(ruby.class_false_class()) {
417
+ // emulation: false — no per-request emulation override
418
+ } else if val.is_kind_of(ruby.class_true_class()) {
419
+ req = req.emulation(DEFAULT_EMULATION);
420
+ } else {
421
+ let name: String = TryConvert::try_convert(val)?;
422
+ let emu = parse_emulation(&name)?;
423
+ req = req.emulation(emu);
424
+ }
425
+ }
426
+
427
+ Ok(req)
428
+ }
429
+
430
+ // --------------------------------------------------------------------------
431
+ // Module-level convenience methods
432
+ // --------------------------------------------------------------------------
433
+
434
+ fn wreq_get(args: &[Value]) -> Result<Response, magnus::Error> {
435
+ let client = Client::rb_new(&[])?;
436
+ client.execute_method("GET", args)
437
+ }
438
+
439
+ fn wreq_post(args: &[Value]) -> Result<Response, magnus::Error> {
440
+ let client = Client::rb_new(&[])?;
441
+ client.execute_method("POST", args)
442
+ }
443
+
444
+ fn wreq_put(args: &[Value]) -> Result<Response, magnus::Error> {
445
+ let client = Client::rb_new(&[])?;
446
+ client.execute_method("PUT", args)
447
+ }
448
+
449
+ fn wreq_patch(args: &[Value]) -> Result<Response, magnus::Error> {
450
+ let client = Client::rb_new(&[])?;
451
+ client.execute_method("PATCH", args)
452
+ }
453
+
454
+ fn wreq_delete(args: &[Value]) -> Result<Response, magnus::Error> {
455
+ let client = Client::rb_new(&[])?;
456
+ client.execute_method("DELETE", args)
457
+ }
458
+
459
+ fn wreq_head(args: &[Value]) -> Result<Response, magnus::Error> {
460
+ let client = Client::rb_new(&[])?;
461
+ client.execute_method("HEAD", args)
462
+ }
463
+
464
+ // --------------------------------------------------------------------------
465
+ // Hash helpers
466
+ // --------------------------------------------------------------------------
467
+
468
+ fn hash_get_value(hash: &RHash, key: &str) -> Result<Option<Value>, magnus::Error> {
469
+ // Try string key
470
+ let val: Value = hash.aref(key)?;
471
+ if !val.is_nil() {
472
+ return Ok(Some(val));
473
+ }
474
+ // Try symbol key
475
+ let ruby = unsafe { Ruby::get_unchecked() };
476
+ let sym = ruby.to_symbol(key);
477
+ let val: Value = hash.aref(sym)?;
478
+ if !val.is_nil() {
479
+ return Ok(Some(val));
480
+ }
481
+ Ok(None)
482
+ }
483
+
484
+ fn hash_get_string(hash: &RHash, key: &str) -> Result<Option<String>, magnus::Error> {
485
+ match hash_get_value(hash, key)? {
486
+ Some(v) => Ok(Some(TryConvert::try_convert(v)?)),
487
+ None => Ok(None),
488
+ }
489
+ }
490
+
491
+ fn hash_get_float(hash: &RHash, key: &str) -> Result<Option<f64>, magnus::Error> {
492
+ match hash_get_value(hash, key)? {
493
+ Some(v) => Ok(Some(TryConvert::try_convert(v)?)),
494
+ None => Ok(None),
495
+ }
496
+ }
497
+
498
+ fn hash_get_bool(hash: &RHash, key: &str) -> Result<Option<bool>, magnus::Error> {
499
+ match hash_get_value(hash, key)? {
500
+ Some(v) => Ok(Some(TryConvert::try_convert(v)?)),
501
+ None => Ok(None),
502
+ }
503
+ }
504
+
505
+ fn hash_get_hash(hash: &RHash, key: &str) -> Result<Option<RHash>, magnus::Error> {
506
+ match hash_get_value(hash, key)? {
507
+ Some(v) => Ok(Some(RHash::try_convert(v)?)),
508
+ None => Ok(None),
509
+ }
510
+ }
511
+
512
+ fn hash_to_header_map(hash: &RHash) -> Result<HeaderMap, magnus::Error> {
513
+ let mut hmap = HeaderMap::new();
514
+ hash.foreach(|k: String, v: String| {
515
+ let name =
516
+ HeaderName::from_bytes(k.as_bytes()).map_err(|e| generic_error(e))?;
517
+ let value = HeaderValue::from_str(&v).map_err(|e| generic_error(e))?;
518
+ hmap.insert(name, value);
519
+ Ok(magnus::r_hash::ForEach::Continue)
520
+ })?;
521
+ Ok(hmap)
522
+ }
523
+
524
+ fn hash_to_pairs(hash: &RHash) -> Result<Vec<(String, String)>, magnus::Error> {
525
+ let mut pairs: Vec<(String, String)> = Vec::new();
526
+ hash.foreach(|k: Value, v: Value| {
527
+ let ruby = unsafe { Ruby::get_unchecked() };
528
+ let ks: String = if k.is_kind_of(ruby.class_symbol()) {
529
+ let s: String = k.funcall("to_s", ())?;
530
+ s
531
+ } else {
532
+ TryConvert::try_convert(k)?
533
+ };
534
+ let vs: String = v.funcall("to_s", ())?;
535
+ pairs.push((ks, vs));
536
+ Ok(magnus::r_hash::ForEach::Continue)
537
+ })?;
538
+ Ok(pairs)
539
+ }
540
+
541
+ // --------------------------------------------------------------------------
542
+ // Init
543
+ // --------------------------------------------------------------------------
544
+
545
+ pub fn init(_ruby: &magnus::Ruby, module: &magnus::RModule) -> Result<(), magnus::Error> {
546
+ let ruby = unsafe { Ruby::get_unchecked() };
547
+ let client_class = module.define_class("Client", ruby.class_object())?;
548
+ client_class.define_singleton_method("new", function!(Client::rb_new, -1))?;
549
+ client_class.define_method("get", method!(Client::get, -1))?;
550
+ client_class.define_method("post", method!(Client::post, -1))?;
551
+ client_class.define_method("put", method!(Client::put, -1))?;
552
+ client_class.define_method("patch", method!(Client::patch, -1))?;
553
+ client_class.define_method("delete", method!(Client::delete, -1))?;
554
+ client_class.define_method("head", method!(Client::head, -1))?;
555
+ client_class.define_method("options", method!(Client::options, -1))?;
556
+
557
+ module.define_module_function("get", function!(wreq_get, -1))?;
558
+ module.define_module_function("post", function!(wreq_post, -1))?;
559
+ module.define_module_function("put", function!(wreq_put, -1))?;
560
+ module.define_module_function("patch", function!(wreq_patch, -1))?;
561
+ module.define_module_function("delete", function!(wreq_delete, -1))?;
562
+ module.define_module_function("head", function!(wreq_head, -1))?;
563
+
564
+ Ok(())
565
+ }
@@ -0,0 +1,25 @@
1
+ use magnus::{ExceptionClass, Module};
2
+
3
+ static mut WREQ_ERROR: Option<ExceptionClass> = None;
4
+
5
+ pub fn wreq_error() -> ExceptionClass {
6
+ unsafe { WREQ_ERROR.unwrap() }
7
+ }
8
+
9
+ pub fn init(ruby: &magnus::Ruby, module: &magnus::RModule) -> Result<(), magnus::Error> {
10
+ let error_class = module.define_error("Error", ruby.exception_standard_error())?;
11
+ unsafe {
12
+ WREQ_ERROR = Some(error_class);
13
+ }
14
+ Ok(())
15
+ }
16
+
17
+ /// Convert a wreq::Error into a magnus::Error
18
+ pub fn to_magnus_error(err: wreq::Error) -> magnus::Error {
19
+ magnus::Error::new(wreq_error(), err.to_string())
20
+ }
21
+
22
+ /// Convert any Display error into a magnus::Error
23
+ pub fn generic_error(msg: impl std::fmt::Display) -> magnus::Error {
24
+ magnus::Error::new(wreq_error(), msg.to_string())
25
+ }
@@ -0,0 +1,20 @@
1
+ #![allow(unused_imports)]
2
+
3
+ mod client;
4
+ mod error;
5
+ mod response;
6
+
7
+ use magnus::prelude::*;
8
+
9
+ /// Initialize the native extension.
10
+ /// This is called when `require "wreq_rb/wreq_rb"` is invoked.
11
+ #[magnus::init]
12
+ fn init(ruby: &magnus::Ruby) -> Result<(), magnus::Error> {
13
+ let module = ruby.define_module("Wreq")?;
14
+
15
+ error::init(ruby, &module)?;
16
+ response::init(ruby, &module)?;
17
+ client::init(ruby, &module)?;
18
+
19
+ Ok(())
20
+ }