wreq-rb 0.5.0 → 0.6.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 (150) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +1922 -397
  3. data/LICENSE +203 -0
  4. data/README.md +47 -15
  5. data/ext/wreq_rb/Cargo.toml +4 -6
  6. data/ext/wreq_rb/src/client.rs +216 -49
  7. data/lib/wreq-rb/version.rb +1 -1
  8. data/patches/0001-add-transfer-size-tracking.patch +76 -67
  9. data/vendor/wreq/Cargo.toml +119 -71
  10. data/vendor/wreq/README.md +25 -20
  11. data/vendor/wreq/bench/http1.rs +25 -0
  12. data/vendor/wreq/bench/http1_over_tls.rs +25 -0
  13. data/vendor/wreq/bench/http2.rs +25 -0
  14. data/vendor/wreq/bench/http2_over_tls.rs +25 -0
  15. data/vendor/wreq/bench/support/bench.rs +91 -0
  16. data/vendor/wreq/bench/support/client.rs +217 -0
  17. data/vendor/wreq/bench/support/server.rs +188 -0
  18. data/vendor/wreq/bench/support.rs +56 -0
  19. data/vendor/wreq/examples/cert_store.rs +4 -4
  20. data/vendor/wreq/examples/{emulation.rs → emulate.rs} +2 -2
  21. data/vendor/wreq/examples/http2_websocket.rs +2 -2
  22. data/vendor/wreq/examples/keylog.rs +3 -3
  23. data/vendor/wreq/examples/{request_with_emulation.rs → request_with_emulate.rs} +2 -2
  24. data/vendor/wreq/examples/rt.rs +23 -0
  25. data/vendor/wreq/src/client/body.rs +23 -61
  26. data/vendor/wreq/src/client/emulate.rs +119 -0
  27. data/vendor/wreq/src/client/{http/future.rs → future.rs} +11 -32
  28. data/vendor/wreq/src/client/{http → layer}/client/pool.rs +66 -61
  29. data/vendor/wreq/src/client/{http → layer}/client.rs +416 -270
  30. data/vendor/wreq/src/client/layer/config.rs +27 -6
  31. data/vendor/wreq/src/client/layer/decoder.rs +9 -4
  32. data/vendor/wreq/src/client/layer/redirect/future.rs +6 -3
  33. data/vendor/wreq/src/client/layer/redirect.rs +4 -5
  34. data/vendor/wreq/src/client/layer/retry.rs +8 -5
  35. data/vendor/wreq/src/client/layer/timeout/body.rs +15 -6
  36. data/vendor/wreq/src/client/layer/timeout/future.rs +23 -18
  37. data/vendor/wreq/src/client/layer/timeout.rs +24 -74
  38. data/vendor/wreq/src/client/layer.rs +1 -2
  39. data/vendor/wreq/src/client/multipart.rs +137 -154
  40. data/vendor/wreq/src/client/request.rs +202 -118
  41. data/vendor/wreq/src/client/response.rs +46 -45
  42. data/vendor/wreq/src/client/upgrade.rs +15 -0
  43. data/vendor/wreq/src/client/ws.rs +73 -25
  44. data/vendor/wreq/src/client.rs +1655 -17
  45. data/vendor/wreq/src/config.rs +11 -11
  46. data/vendor/wreq/src/{client/conn → conn}/connector.rs +139 -137
  47. data/vendor/wreq/src/conn/descriptor.rs +143 -0
  48. data/vendor/wreq/src/conn/http.rs +484 -0
  49. data/vendor/wreq/src/conn/net/io.rs +75 -0
  50. data/vendor/wreq/src/conn/net/tcp/compio.rs +71 -0
  51. data/vendor/wreq/src/conn/net/tcp/tokio.rs +57 -0
  52. data/vendor/wreq/src/conn/net/tcp.rs +561 -0
  53. data/vendor/wreq/src/conn/net/uds/compio.rs +60 -0
  54. data/vendor/wreq/src/{client/conn/uds.rs → conn/net/uds/tokio.rs} +18 -12
  55. data/vendor/wreq/src/conn/net/uds.rs +11 -0
  56. data/vendor/wreq/src/conn/net.rs +130 -0
  57. data/vendor/wreq/src/{client/conn → conn}/proxy/socks.rs +2 -9
  58. data/vendor/wreq/src/{client/conn → conn}/proxy/tunnel.rs +21 -56
  59. data/vendor/wreq/src/conn/tls_info.rs +47 -0
  60. data/vendor/wreq/src/{client/conn.rs → conn.rs} +202 -54
  61. data/vendor/wreq/src/cookie.rs +302 -142
  62. data/vendor/wreq/src/dns/gai/compio.rs +77 -0
  63. data/vendor/wreq/src/dns/gai/tokio.rs +90 -0
  64. data/vendor/wreq/src/dns/gai.rs +14 -164
  65. data/vendor/wreq/src/dns/hickory.rs +16 -23
  66. data/vendor/wreq/src/dns/resolve.rs +7 -41
  67. data/vendor/wreq/src/dns.rs +90 -7
  68. data/vendor/wreq/src/error.rs +57 -31
  69. data/vendor/wreq/src/ext.rs +25 -0
  70. data/vendor/wreq/src/group.rs +211 -0
  71. data/vendor/wreq/src/header.rs +100 -112
  72. data/vendor/wreq/src/lib.rs +124 -73
  73. data/vendor/wreq/src/proxy.rs +6 -20
  74. data/vendor/wreq/src/redirect.rs +1 -1
  75. data/vendor/wreq/src/rt.rs +208 -0
  76. data/vendor/wreq/src/sync.rs +97 -98
  77. data/vendor/wreq/src/tls/compress.rs +124 -0
  78. data/vendor/wreq/src/tls/conn/ext.rs +54 -45
  79. data/vendor/wreq/src/tls/conn/service.rs +14 -18
  80. data/vendor/wreq/src/tls/conn.rs +169 -241
  81. data/vendor/wreq/src/tls/keylog.rs +68 -5
  82. data/vendor/wreq/src/tls/session.rs +205 -0
  83. data/vendor/wreq/src/tls/{x509 → trust}/identity.rs +4 -21
  84. data/vendor/wreq/src/tls/{x509/parser.rs → trust/parse.rs} +1 -1
  85. data/vendor/wreq/src/tls/{x509 → trust}/store.rs +42 -81
  86. data/vendor/wreq/src/tls/{x509.rs → trust.rs} +8 -2
  87. data/vendor/wreq/src/tls.rs +489 -25
  88. data/vendor/wreq/src/trace.rs +0 -12
  89. data/vendor/wreq/src/util.rs +1 -1
  90. data/vendor/wreq/tests/badssl.rs +10 -10
  91. data/vendor/wreq/tests/client.rs +3 -9
  92. data/vendor/wreq/tests/cookie.rs +6 -8
  93. data/vendor/wreq/tests/{emulation.rs → emulate.rs} +130 -22
  94. data/vendor/wreq/tests/multipart.rs +43 -1
  95. data/vendor/wreq/tests/proxy.rs +1 -1
  96. data/vendor/wreq/tests/support/layer.rs +1 -0
  97. metadata +49 -71
  98. data/patches/0002-add-cancel-connections.patch +0 -181
  99. data/vendor/wreq/src/client/conn/conn.rs +0 -231
  100. data/vendor/wreq/src/client/conn/http.rs +0 -1023
  101. data/vendor/wreq/src/client/conn/tls_info.rs +0 -98
  102. data/vendor/wreq/src/client/core/body/incoming.rs +0 -485
  103. data/vendor/wreq/src/client/core/body/length.rs +0 -118
  104. data/vendor/wreq/src/client/core/body.rs +0 -34
  105. data/vendor/wreq/src/client/core/common/buf.rs +0 -149
  106. data/vendor/wreq/src/client/core/common/rewind.rs +0 -141
  107. data/vendor/wreq/src/client/core/common/watch.rs +0 -76
  108. data/vendor/wreq/src/client/core/common.rs +0 -3
  109. data/vendor/wreq/src/client/core/conn/http1.rs +0 -342
  110. data/vendor/wreq/src/client/core/conn/http2.rs +0 -307
  111. data/vendor/wreq/src/client/core/conn.rs +0 -11
  112. data/vendor/wreq/src/client/core/dispatch.rs +0 -299
  113. data/vendor/wreq/src/client/core/error.rs +0 -435
  114. data/vendor/wreq/src/client/core/ext.rs +0 -201
  115. data/vendor/wreq/src/client/core/http1.rs +0 -178
  116. data/vendor/wreq/src/client/core/http2.rs +0 -483
  117. data/vendor/wreq/src/client/core/proto/h1/conn.rs +0 -988
  118. data/vendor/wreq/src/client/core/proto/h1/decode.rs +0 -1170
  119. data/vendor/wreq/src/client/core/proto/h1/dispatch.rs +0 -684
  120. data/vendor/wreq/src/client/core/proto/h1/encode.rs +0 -580
  121. data/vendor/wreq/src/client/core/proto/h1/io.rs +0 -879
  122. data/vendor/wreq/src/client/core/proto/h1/role.rs +0 -694
  123. data/vendor/wreq/src/client/core/proto/h1.rs +0 -104
  124. data/vendor/wreq/src/client/core/proto/h2/client.rs +0 -650
  125. data/vendor/wreq/src/client/core/proto/h2/ping.rs +0 -539
  126. data/vendor/wreq/src/client/core/proto/h2.rs +0 -379
  127. data/vendor/wreq/src/client/core/proto/headers.rs +0 -138
  128. data/vendor/wreq/src/client/core/proto.rs +0 -58
  129. data/vendor/wreq/src/client/core/rt/bounds.rs +0 -57
  130. data/vendor/wreq/src/client/core/rt/timer.rs +0 -150
  131. data/vendor/wreq/src/client/core/rt/tokio.rs +0 -99
  132. data/vendor/wreq/src/client/core/rt.rs +0 -25
  133. data/vendor/wreq/src/client/core/upgrade.rs +0 -267
  134. data/vendor/wreq/src/client/core.rs +0 -16
  135. data/vendor/wreq/src/client/emulation.rs +0 -161
  136. data/vendor/wreq/src/client/http/client/error.rs +0 -142
  137. data/vendor/wreq/src/client/http/client/exec.rs +0 -29
  138. data/vendor/wreq/src/client/http/client/extra.rs +0 -77
  139. data/vendor/wreq/src/client/http/client/util.rs +0 -104
  140. data/vendor/wreq/src/client/http.rs +0 -1629
  141. data/vendor/wreq/src/client/layer/config/options.rs +0 -156
  142. data/vendor/wreq/src/client/layer/cookie.rs +0 -161
  143. data/vendor/wreq/src/hash.rs +0 -143
  144. data/vendor/wreq/src/tls/conn/cache.rs +0 -123
  145. data/vendor/wreq/src/tls/conn/cert_compression.rs +0 -125
  146. data/vendor/wreq/src/tls/keylog/handle.rs +0 -64
  147. data/vendor/wreq/src/tls/options.rs +0 -464
  148. /data/vendor/wreq/src/client/{http → layer}/client/lazy.rs +0 -0
  149. /data/vendor/wreq/src/{client/conn → conn}/proxy.rs +0 -0
  150. /data/vendor/wreq/src/{client/conn → conn}/verbose.rs +0 -0
@@ -2,6 +2,7 @@ use std::ffi::c_void;
2
2
  use std::panic::{self, AssertUnwindSafe};
3
3
  use std::ptr;
4
4
  use std::any::Any;
5
+ use std::sync::Arc;
5
6
  use std::time::Duration;
6
7
 
7
8
  use magnus::{
@@ -9,13 +10,15 @@ use magnus::{
9
10
  try_convert::TryConvert, Value,
10
11
  };
11
12
  use tokio::runtime::Runtime;
13
+ use tokio::sync::Semaphore;
14
+ use tokio::task::JoinSet;
12
15
  use tokio_util::sync::CancellationToken;
13
16
  use std::net::IpAddr;
14
17
  use wreq::header::{HeaderMap, HeaderName, HeaderValue, OrigHeaderMap};
15
18
  use wreq::tls::TlsVersion;
16
- use wreq_util::{Emulation as BrowserEmulation, EmulationOS, EmulationOption};
19
+ use wreq_util::{Emulation as BrowserEmulation, Platform as EmulationPlatform, Profile as BrowserProfile};
17
20
 
18
- use crate::error::{generic_error, to_magnus_error};
21
+ use crate::error::{generic_error, to_magnus_error, wreq_error};
19
22
  use crate::response::Response;
20
23
 
21
24
  // --------------------------------------------------------------------------
@@ -137,39 +140,95 @@ async fn execute_request(req: wreq::RequestBuilder) -> Result<ResponseData, wreq
137
140
  Ok(ResponseData { status, headers, body, url, version, content_length, transfer_size })
138
141
  }
139
142
 
143
+ // --------------------------------------------------------------------------
144
+ // Batch execution
145
+ // --------------------------------------------------------------------------
146
+
147
+ const DEFAULT_BATCH_CONCURRENCY: usize = 16;
148
+
149
+ /// Per-item batch result as pure Rust types (no Ruby objects). Errors are kept
150
+ /// as messages so one failure never discards the rest of the batch.
151
+ enum BatchItem {
152
+ Ok(ResponseData),
153
+ Err(String),
154
+ }
155
+
156
+ /// Outcome of a whole batch performed outside the GVL.
157
+ enum BatchOutcome {
158
+ Done(Vec<BatchItem>),
159
+ Interrupted,
160
+ }
161
+
162
+ /// Run every request concurrently with at most `concurrency` in flight,
163
+ /// returning results in input order.
164
+ async fn execute_batch(reqs: Vec<wreq::RequestBuilder>, concurrency: usize) -> Vec<BatchItem> {
165
+ let permits = Arc::new(Semaphore::new(concurrency));
166
+ let mut set: JoinSet<(usize, BatchItem)> = JoinSet::new();
167
+
168
+ for (idx, req) in reqs.into_iter().enumerate() {
169
+ let permits = Arc::clone(&permits);
170
+ set.spawn(async move {
171
+ let _permit = match permits.acquire_owned().await {
172
+ Ok(p) => p,
173
+ Err(_) => return (idx, BatchItem::Err("batch semaphore closed".to_owned())),
174
+ };
175
+ let item = match execute_request(req).await {
176
+ Ok(data) => BatchItem::Ok(data),
177
+ Err(e) => BatchItem::Err(e.to_string()),
178
+ };
179
+ (idx, item)
180
+ });
181
+ }
182
+
183
+ let mut slots: Vec<Option<BatchItem>> = Vec::new();
184
+ slots.resize_with(set.len(), || None);
185
+ while let Some(joined) = set.join_next().await {
186
+ // A JoinError means the task panicked or was aborted; its slot is left
187
+ // empty and filled with a generic error below.
188
+ if let Ok((idx, item)) = joined {
189
+ slots[idx] = Some(item);
190
+ }
191
+ }
192
+
193
+ slots
194
+ .into_iter()
195
+ .map(|slot| slot.unwrap_or_else(|| BatchItem::Err("request task failed".to_owned())))
196
+ .collect()
197
+ }
198
+
140
199
  // --------------------------------------------------------------------------
141
200
  // Emulation helpers
142
201
  // --------------------------------------------------------------------------
143
202
 
144
- /// The default emulation to apply when none is specified.
145
- const DEFAULT_EMULATION: BrowserEmulation = BrowserEmulation::Chrome145;
203
+ /// The default browser profile to apply when none is specified.
204
+ const DEFAULT_EMULATION: BrowserProfile = BrowserProfile::Chrome148;
146
205
 
147
- /// Parse a Ruby string like "chrome_143" into a BrowserEmulation variant.
148
- fn parse_emulation(name: &str) -> Result<BrowserEmulation, magnus::Error> {
206
+ /// Parse a Ruby string like "chrome_143" into a BrowserProfile variant.
207
+ fn parse_emulation(name: &str) -> Result<BrowserProfile, magnus::Error> {
149
208
  let json_val = serde_json::Value::String(name.to_string());
150
- serde_json::from_value::<BrowserEmulation>(json_val)
151
- .map_err(|_| generic_error(format!("unknown emulation: '{}'. Use names like 'chrome_145', 'firefox_147', 'safari_18.5', etc.", name)))
209
+ serde_json::from_value::<BrowserProfile>(json_val)
210
+ .map_err(|_| generic_error(format!("unknown emulation: '{}'. Use names like 'chrome_148', 'firefox_151', 'safari_18.5', etc.", name)))
152
211
  }
153
212
 
154
- /// Parse a Ruby string like "windows" into an EmulationOS variant.
155
- fn parse_emulation_os(name: &str) -> Result<EmulationOS, magnus::Error> {
213
+ /// Parse a Ruby string like "windows" into an EmulationPlatform variant.
214
+ fn parse_emulation_os(name: &str) -> Result<EmulationPlatform, magnus::Error> {
156
215
  let json_val = serde_json::Value::String(name.to_string());
157
- serde_json::from_value::<EmulationOS>(json_val)
216
+ serde_json::from_value::<EmulationPlatform>(json_val)
158
217
  .map_err(|_| generic_error("unknown emulation_os. Use: 'windows', 'macos', 'linux', 'android', 'ios'"))
159
218
  }
160
219
 
161
- /// Build an EmulationOption from an Emulation and an optional OS from the opts hash.
220
+ /// Build a BrowserEmulation from a profile and an optional platform from the opts hash.
162
221
  fn build_emulation_option(
163
- emu: BrowserEmulation,
222
+ profile: BrowserProfile,
164
223
  opts: &RHash,
165
- ) -> Result<EmulationOption, magnus::Error> {
166
- let os = match hash_get_string(opts, "emulation_os")? {
167
- Some(os_name) => parse_emulation_os(&os_name)?,
168
- None => EmulationOS::default(),
224
+ ) -> Result<BrowserEmulation, magnus::Error> {
225
+ let platform = match hash_get_string(opts, "emulation_os")? {
226
+ Some(platform_name) => parse_emulation_os(&platform_name)?,
227
+ None => EmulationPlatform::default(),
169
228
  };
170
- Ok(EmulationOption::builder()
171
- .emulation(emu)
172
- .emulation_os(os)
229
+ Ok(BrowserEmulation::builder()
230
+ .profile(profile)
231
+ .platform(platform)
173
232
  .build())
174
233
  }
175
234
 
@@ -192,7 +251,8 @@ impl Client {
192
251
  Some(RHash::try_convert(args[0])?)
193
252
  };
194
253
 
195
- let mut builder = wreq::Client::builder();
254
+ let mut builder = wreq::Client::builder()
255
+ .retry(wreq::retry::Policy::never());
196
256
 
197
257
  if let Some(opts) = opts {
198
258
  // Apply header_order BEFORE emulation so the user's ordering takes precedence
@@ -276,11 +336,11 @@ impl Client {
276
336
  }
277
337
 
278
338
  if let Some(v) = hash_get_bool(&opts, "verify_host")? {
279
- builder = builder.verify_hostname(v);
339
+ builder = builder.tls_verify_hostname(v);
280
340
  }
281
341
 
282
342
  if let Some(v) = hash_get_bool(&opts, "verify_cert")? {
283
- builder = builder.cert_verification(v);
343
+ builder = builder.tls_cert_verification(v);
284
344
  }
285
345
 
286
346
  if let Some(true) = hash_get_bool(&opts, "http1_only")? {
@@ -311,7 +371,7 @@ impl Client {
311
371
  builder = builder.pool_max_idle_per_host(n);
312
372
  }
313
373
 
314
- if let Some(n) = hash_get_u32(&opts, "pool_max_size")? {
374
+ if let Some(n) = hash_get_usize(&opts, "pool_max_size")? {
315
375
  builder = builder.pool_max_size(n);
316
376
  }
317
377
 
@@ -334,11 +394,11 @@ impl Client {
334
394
  }
335
395
 
336
396
  if let Some(s) = hash_get_string(&opts, "min_tls_version")? {
337
- builder = builder.min_tls_version(parse_tls_version(&s)?);
397
+ builder = builder.tls_min_version(parse_tls_version(&s)?);
338
398
  }
339
399
 
340
400
  if let Some(s) = hash_get_string(&opts, "max_tls_version")? {
341
- builder = builder.max_tls_version(parse_tls_version(&s)?);
401
+ builder = builder.tls_max_version(parse_tls_version(&s)?);
342
402
  }
343
403
  } else {
344
404
  builder = builder.emulation(DEFAULT_EMULATION);
@@ -387,7 +447,6 @@ impl Client {
387
447
  old
388
448
  };
389
449
  old_token.cancel();
390
- self.inner.cancel_connections();
391
450
  }
392
451
 
393
452
  fn execute_method(&self, method_str: &str, args: &[Value]) -> Result<Response, magnus::Error> {
@@ -439,12 +498,141 @@ impl Client {
439
498
  };
440
499
  Ok(Response::new(data.status, data.headers, data.body, data.url, data.version, data.content_length, data.transfer_size))
441
500
  }
501
+
502
+ /// Wreq::Client#request_batch(specs) or #request_batch(specs, options)
503
+ fn request_batch(&self, args: &[Value]) -> Result<RArray, magnus::Error> {
504
+ if args.is_empty() {
505
+ return Err(generic_error("an array of requests is required"));
506
+ }
507
+ let specs = RArray::try_convert(args[0])?;
508
+
509
+ let opts: Option<RHash> = if args.len() > 1 {
510
+ Some(RHash::try_convert(args[1])?)
511
+ } else {
512
+ None
513
+ };
514
+
515
+ let concurrency = match opts.as_ref() {
516
+ Some(o) => hash_get_usize(o, "concurrency")?.unwrap_or(DEFAULT_BATCH_CONCURRENCY),
517
+ None => DEFAULT_BATCH_CONCURRENCY,
518
+ };
519
+ if concurrency == 0 {
520
+ return Err(generic_error("concurrency must be >= 1"));
521
+ }
522
+
523
+ // All Ruby -> Rust conversion happens here, while we still hold the GVL.
524
+ let mut reqs: Vec<wreq::RequestBuilder> = Vec::with_capacity(specs.len());
525
+ for spec in specs.into_iter() {
526
+ reqs.push(self.build_request(spec, opts.as_ref())?);
527
+ }
528
+
529
+ let ruby = unsafe { Ruby::get_unchecked() };
530
+ if reqs.is_empty() {
531
+ return Ok(ruby.ary_new());
532
+ }
533
+
534
+ let client_token = self.cancel_token.lock().unwrap_or_else(|e| e.into_inner()).clone();
535
+
536
+ // One GVL release covering the whole batch.
537
+ let outcome: BatchOutcome = unsafe {
538
+ without_gvl(|thread_token| {
539
+ runtime().block_on(async {
540
+ tokio::select! {
541
+ biased;
542
+ _ = thread_token.cancelled() => BatchOutcome::Interrupted,
543
+ _ = client_token.cancelled() => BatchOutcome::Interrupted,
544
+ items = execute_batch(reqs, concurrency) => BatchOutcome::Done(items),
545
+ }
546
+ })
547
+ })
548
+ };
549
+
550
+ let items = match outcome {
551
+ BatchOutcome::Done(items) => items,
552
+ BatchOutcome::Interrupted => return Err(generic_error("request interrupted")),
553
+ };
554
+
555
+ // Back under the GVL: Ruby objects may be created again.
556
+ let results = ruby.ary_new_capa(items.len());
557
+ for item in items {
558
+ match item {
559
+ BatchItem::Ok(d) => {
560
+ let resp = Response::new(
561
+ d.status, d.headers, d.body, d.url, d.version, d.content_length,
562
+ d.transfer_size,
563
+ );
564
+ results.push(ruby.obj_wrap(resp))?;
565
+ }
566
+ BatchItem::Err(msg) => {
567
+ let err: Value = wreq_error().funcall("new", (msg,))?;
568
+ results.push(err)?;
569
+ }
570
+ }
571
+ }
572
+ Ok(results)
573
+ }
574
+
575
+ /// Convert a single batch spec into a RequestBuilder. Accepted forms:
576
+ /// "https://example.com" -> GET
577
+ /// { method:, url:, **opts }
578
+ fn build_request(
579
+ &self,
580
+ spec: Value,
581
+ shared: Option<&RHash>,
582
+ ) -> Result<wreq::RequestBuilder, magnus::Error> {
583
+ let mut method = wreq::Method::GET;
584
+ let url: String;
585
+ let mut item_opts: Option<RHash> = None;
586
+
587
+ if let Some(hash) = RHash::from_value(spec) {
588
+ url = hash_get_string(&hash, "url")?
589
+ .ok_or_else(|| generic_error("each request hash requires a :url"))?;
590
+ if let Some(val) = hash_get_value(&hash, "method")? {
591
+ method = value_to_method(val)?;
592
+ }
593
+ item_opts = Some(hash);
594
+ } else {
595
+ url = TryConvert::try_convert(spec)?;
596
+ }
597
+
598
+ let mut req = self.inner.request(method, &url);
599
+ if let Some(shared) = shared {
600
+ req = apply_request_options(req, shared)?;
601
+ }
602
+ if let Some(item_opts) = item_opts {
603
+ req = apply_request_options(req, &item_opts)?;
604
+ }
605
+ Ok(req)
606
+ }
607
+ }
608
+
609
+ /// Parse a String or Symbol like "post" / :post into an HTTP method.
610
+ fn value_to_method(val: Value) -> Result<wreq::Method, magnus::Error> {
611
+ let name: String = val.funcall("to_s", ())?;
612
+ name.to_uppercase()
613
+ .parse()
614
+ .map_err(|_| generic_error(format!("invalid HTTP method: {}", name)))
442
615
  }
443
616
 
444
617
  fn apply_request_options(
445
618
  mut req: wreq::RequestBuilder,
446
619
  opts: &RHash,
447
620
  ) -> Result<wreq::RequestBuilder, magnus::Error> {
621
+ if let Some(val) = hash_get_value(opts, "emulation")? {
622
+ let ruby = unsafe { Ruby::get_unchecked() };
623
+ if val.is_kind_of(ruby.class_false_class()) {
624
+ // emulation: false — no per-request emulation override
625
+ } else if val.is_kind_of(ruby.class_true_class()) {
626
+ let opt = build_emulation_option(DEFAULT_EMULATION, opts)?;
627
+ req = req.emulation(opt);
628
+ } else {
629
+ let name: String = TryConvert::try_convert(val)?;
630
+ let emu = parse_emulation(&name)?;
631
+ let opt = build_emulation_option(emu, opts)?;
632
+ req = req.emulation(opt);
633
+ }
634
+ }
635
+
448
636
  if let Some(hdr_hash) = hash_get_hash(opts, "headers")? {
449
637
  let hmap = hash_to_header_map(&hdr_hash)?;
450
638
  req = req.headers(hmap);
@@ -499,21 +687,6 @@ fn apply_request_options(
499
687
  req = req.proxy(proxy);
500
688
  }
501
689
 
502
- if let Some(val) = hash_get_value(opts, "emulation")? {
503
- let ruby = unsafe { Ruby::get_unchecked() };
504
- if val.is_kind_of(ruby.class_false_class()) {
505
- // emulation: false — no per-request emulation override
506
- } else if val.is_kind_of(ruby.class_true_class()) {
507
- let opt = build_emulation_option(DEFAULT_EMULATION, opts)?;
508
- req = req.emulation(opt);
509
- } else {
510
- let name: String = TryConvert::try_convert(val)?;
511
- let emu = parse_emulation(&name)?;
512
- let opt = build_emulation_option(emu, opts)?;
513
- req = req.emulation(opt);
514
- }
515
- }
516
-
517
690
  Ok(req)
518
691
  }
519
692
 
@@ -599,13 +772,6 @@ fn hash_get_usize(hash: &RHash, key: &str) -> Result<Option<usize>, magnus::Erro
599
772
  }
600
773
  }
601
774
 
602
- fn hash_get_u32(hash: &RHash, key: &str) -> Result<Option<u32>, magnus::Error> {
603
- match hash_get_value(hash, key)? {
604
- Some(v) => Ok(Some(TryConvert::try_convert(v)?)),
605
- None => Ok(None),
606
- }
607
- }
608
-
609
775
  fn parse_tls_version(s: &str) -> Result<TlsVersion, magnus::Error> {
610
776
  match s {
611
777
  "tls1.0" | "tls_1_0" | "1.0" => Ok(TlsVersion::TLS_1_0),
@@ -699,6 +865,7 @@ pub fn init(_ruby: &magnus::Ruby, module: &magnus::RModule) -> Result<(), magnus
699
865
  client_class.define_method("delete", method!(Client::delete, -1))?;
700
866
  client_class.define_method("head", method!(Client::head, -1))?;
701
867
  client_class.define_method("options", method!(Client::options, -1))?;
868
+ client_class.define_method("request_batch", method!(Client::request_batch, -1))?;
702
869
  client_class.define_method("cancel", method!(Client::cancel, 0))?;
703
870
 
704
871
  module.define_module_function("get", function!(wreq_get, -1))?;
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wreq
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -1,60 +1,51 @@
1
- diff --git a/src/client/http.rs b/src/client/http.rs
2
- index 10be0c83..462c77d0 100644
3
- --- a/src/client/http.rs
4
- +++ b/src/client/http.rs
5
- @@ -33,6 +33,7 @@ use self::future::Pending;
6
- feature = "deflate",
7
- ))]
8
- use super::layer::decoder::{AcceptEncoding, DecompressionLayer};
9
- +use super::layer::transfer_size::{CountingBody, TransferSizeLayer, TransferSizeService};
10
- #[cfg(feature = "ws")]
11
- use super::ws::WebSocketRequestBuilder;
12
- use super::{
13
- @@ -104,7 +105,7 @@ type Decompression<T> = super::layer::decoder::Decompression<T>;
14
- feature = "brotli",
15
- feature = "deflate"
16
- ))]
17
- -type ResponseBody = TimeoutBody<tower_http::decompression::DecompressionBody<Incoming>>;
18
- +type ResponseBody = TimeoutBody<tower_http::decompression::DecompressionBody<CountingBody<Incoming>>>;
1
+ diff --git a/src/client.rs b/src/client.rs
2
+ index 4abe25d8..76e709a8 100644
3
+ --- a/src/client.rs
4
+ +++ b/src/client.rs
5
+ @@ -49,6 +49,7 @@ use self::{
6
+ redirect::{FollowRedirect, FollowRedirectLayer},
7
+ retry::RetryPolicy,
8
+ timeout::{Timeout, TimeoutLayer, TimeoutOptions, body::TimeoutBody},
9
+ + transfer_size::{CountingBody, TransferSizeLayer, TransferSizeService},
10
+ },
11
+ request::{Request, RequestBuilder},
12
+ response::Response,
13
+ @@ -115,25 +116,30 @@ type MaybeDecompressionBody<T> = tower_http::decompression::DecompressionBody<T>
14
+ type ClientService = Timeout<
15
+ ConfigService<
16
+ MaybeDecompression<
17
+ - Retry<RetryPolicy, FollowRedirect<HttpClient<Connector, Body>, FollowRedirectPolicy>>,
18
+ + TransferSizeService<
19
+ + Retry<
20
+ + RetryPolicy,
21
+ + FollowRedirect<HttpClient<Connector, Body>, FollowRedirectPolicy>,
22
+ + >,
23
+ + >,
24
+ >,
25
+ >,
26
+ >;
19
27
 
20
- /// Response body type with timeout only (no compression features).
21
- #[cfg(not(any(
22
- @@ -113,23 +114,25 @@ type ResponseBody = TimeoutBody<tower_http::decompression::DecompressionBody<Inc
23
- feature = "brotli",
24
- feature = "deflate"
25
- )))]
26
- -type ResponseBody = TimeoutBody<Incoming>;
27
- +type ResponseBody = TimeoutBody<CountingBody<Incoming>>;
28
+ type BoxedClientService = BoxCloneSyncService<
29
+ http::Request<Body>,
30
+ - http::Response<TimeoutBody<MaybeDecompressionBody<Incoming>>>,
31
+ + http::Response<TimeoutBody<MaybeDecompressionBody<CountingBody<Incoming>>>>,
32
+ BoxError,
33
+ >;
28
34
 
29
- /// The complete HTTP client service stack with all middleware layers.
30
- type ClientService = Timeout<
31
- ResponseBodyTimeout<
32
- ConfigService<
33
- Decompression<
34
- - Retry<
35
- - RetryPolicy,
36
- - FollowRedirect<
37
- - CookieService<
38
- - MapErr<
39
- - HttpClient<Connector, Body>,
40
- - fn(client::error::Error) -> BoxError,
41
- + TransferSizeService<
42
- + Retry<
43
- + RetryPolicy,
44
- + FollowRedirect<
45
- + CookieService<
46
- + MapErr<
47
- + HttpClient<Connector, Body>,
48
- + fn(client::error::Error) -> BoxError,
49
- + >,
50
- >,
51
- + FollowRedirectPolicy,
52
- >,
53
- - FollowRedirectPolicy,
54
- >,
55
- >,
56
- >,
57
- @@ -582,6 +585,10 @@ impl ClientBuilder {
35
+ type BoxedClientServiceLayer = BoxCloneSyncServiceLayer<
36
+ BoxCloneSyncService<
37
+ http::Request<Body>,
38
+ - http::Response<MaybeDecompressionBody<Incoming>>,
39
+ + http::Response<MaybeDecompressionBody<CountingBody<Incoming>>>,
40
+ BoxError,
41
+ >,
42
+ http::Request<Body>,
43
+ - http::Response<MaybeDecompressionBody<Incoming>>,
44
+ + http::Response<MaybeDecompressionBody<CountingBody<Incoming>>>,
45
+ BoxError,
46
+ >;
47
+
48
+ @@ -593,6 +599,10 @@ impl ClientBuilder {
58
49
  })
59
50
  .service(service);
60
51
 
@@ -65,18 +56,36 @@ index 10be0c83..462c77d0 100644
65
56
  #[cfg(any(
66
57
  feature = "gzip",
67
58
  feature = "zstd",
59
+ @@ -1585,7 +1595,7 @@ impl ClientBuilder {
60
+ L: Layer<
61
+ BoxCloneSyncService<
62
+ http::Request<Body>,
63
+ - http::Response<MaybeDecompressionBody<Incoming>>,
64
+ + http::Response<MaybeDecompressionBody<CountingBody<Incoming>>>,
65
+ BoxError,
66
+ >,
67
+ > + Clone
68
+ @@ -1594,7 +1604,7 @@ impl ClientBuilder {
69
+ + 'static,
70
+ L::Service: Service<
71
+ http::Request<Body>,
72
+ - Response = http::Response<MaybeDecompressionBody<Incoming>>,
73
+ + Response = http::Response<MaybeDecompressionBody<CountingBody<Incoming>>>,
74
+ Error = BoxError,
75
+ > + Clone
76
+ + Send
68
77
  diff --git a/src/client/layer.rs b/src/client/layer.rs
69
- index 05bb533f..cb0b0866 100644
78
+ index 5c03ef4d..fe37cff0 100644
70
79
  --- a/src/client/layer.rs
71
80
  +++ b/src/client/layer.rs
72
- @@ -13,3 +13,4 @@ pub mod decoder;
81
+ @@ -12,3 +12,4 @@ pub mod decoder;
73
82
  pub mod redirect;
74
83
  pub mod retry;
75
84
  pub mod timeout;
76
85
  +pub mod transfer_size;
77
86
  diff --git a/src/client/layer/transfer_size.rs b/src/client/layer/transfer_size.rs
78
87
  new file mode 100644
79
- index 00000000..7e8a4390
88
+ index 00000000..f155ec8d
80
89
  --- /dev/null
81
90
  +++ b/src/client/layer/transfer_size.rs
82
91
  @@ -0,0 +1,176 @@
@@ -91,8 +100,8 @@ index 00000000..7e8a4390
91
100
  +use std::{
92
101
  + pin::Pin,
93
102
  + sync::{
94
- + atomic::{AtomicU64, Ordering},
95
103
  + Arc,
104
+ + atomic::{AtomicU64, Ordering},
96
105
  + },
97
106
  + task::{Context, Poll},
98
107
  +};
@@ -257,18 +266,18 @@ index 00000000..7e8a4390
257
266
  + }
258
267
  +}
259
268
  diff --git a/src/client/response.rs b/src/client/response.rs
260
- index d98859b9..16ed35c3 100644
269
+ index 3d3532af..ed478804 100644
261
270
  --- a/src/client/response.rs
262
271
  +++ b/src/client/response.rs
263
- @@ -20,6 +20,7 @@ use serde::de::DeserializeOwned;
264
- use super::{
265
- conn::HttpInfo,
266
- core::{ext::ReasonPhrase, upgrade},
267
- + layer::transfer_size::TransferSizeHandle,
268
- };
272
+ @@ -18,6 +18,7 @@ use mime::Mime;
273
+ use serde::de::DeserializeOwned;
274
+ use wreq_proto::ext::ReasonPhrase;
275
+
276
+ +use super::layer::transfer_size::TransferSizeHandle;
269
277
  #[cfg(feature = "cookies")]
270
278
  use crate::cookie;
271
- @@ -91,6 +92,21 @@ impl Response {
279
+ use crate::{
280
+ @@ -97,6 +98,21 @@ impl Response {
272
281
  HttpBody::size_hint(self.res.body()).exact()
273
282
  }
274
283