wreq 1.2.5 → 1.2.6
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.
- checksums.yaml +4 -4
- data/Cargo.lock +20 -13
- data/Cargo.toml +10 -2
- data/crates/wreq-util/src/emulate/macros.rs +12 -10
- data/crates/wreq-util/src/emulate/profile/chrome/header.rs +18 -3
- data/crates/wreq-util/src/emulate/profile/firefox/header.rs +16 -0
- data/crates/wreq-util/src/emulate/profile/opera/header.rs +6 -7
- data/crates/wreq-util/tests/client.rs +103 -1
- data/lib/wreq.rb +70 -45
- data/lib/wreq_ruby/body.rb +29 -11
- data/lib/wreq_ruby/client.rb +88 -55
- data/lib/wreq_ruby/cookie.rb +79 -19
- data/lib/wreq_ruby/emulate.rb +74 -6
- data/lib/wreq_ruby/error.rb +5 -7
- data/lib/wreq_ruby/http.rb +74 -0
- data/lib/wreq_ruby/response.rb +3 -0
- data/script/build_windows_gnu.ps1 +6 -0
- data/src/arch.rs +22 -0
- data/src/client/body/form.rs +2 -0
- data/src/client/body/json.rs +47 -14
- data/src/client/body/stream.rs +147 -43
- data/src/client/body.rs +11 -15
- data/src/client/param.rs +7 -7
- data/src/client/req.rs +87 -47
- data/src/client/resp.rs +23 -24
- data/src/client.rs +310 -230
- data/src/cookie.rs +280 -87
- data/src/emulate.rs +85 -50
- data/src/error.rs +198 -41
- data/src/extractor.rs +16 -76
- data/src/header.rs +146 -23
- data/src/http.rs +62 -33
- data/src/lib.rs +26 -20
- data/src/macros.rs +71 -46
- data/src/options.rs +284 -0
- data/src/rt.rs +22 -11
- data/src/serde/de/array_deserializer.rs +48 -0
- data/src/serde/de/array_enumerator.rs +59 -0
- data/src/serde/de/deserializer.rs +307 -0
- data/src/serde/de/enum_deserializer.rs +47 -0
- data/src/serde/de/hash_deserializer.rs +89 -0
- data/src/serde/de/number_deserializer.rs +51 -0
- data/src/serde/de/variant_deserializer.rs +101 -0
- data/src/serde/de.rs +33 -0
- data/src/serde/error.rs +146 -0
- data/src/serde/ser/enums.rs +14 -0
- data/src/serde/ser/map_serializer.rs +56 -0
- data/src/serde/ser/seq_serializer.rs +71 -0
- data/src/serde/ser/serializer.rs +194 -0
- data/src/serde/ser/struct_serializer.rs +106 -0
- data/src/serde/ser/struct_variant_serializer.rs +44 -0
- data/src/serde/ser/tuple_variant_serializer.rs +41 -0
- data/src/serde/ser.rs +18 -0
- data/src/serde/tests.rs +237 -0
- data/src/serde.rs +202 -0
- data/test/body_sender_test.rb +246 -0
- data/test/cookie_test.rb +211 -10
- data/test/json_precision_test.rb +209 -0
- data/test/option_validation_test.rb +311 -0
- data/test/value_semantics_test.rb +238 -0
- metadata +25 -2
- data/src/header/helper.rs +0 -112
data/lib/wreq_ruby/emulate.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Profile and platform constants mirror the native enum variant names.
|
|
4
|
+
# standard:disable Naming/ConstantName
|
|
5
|
+
|
|
3
6
|
module Wreq
|
|
4
7
|
# Browser and client fingerprint profile enumeration backed by Rust.
|
|
5
8
|
#
|
|
@@ -164,6 +167,29 @@ module Wreq
|
|
|
164
167
|
def to_s
|
|
165
168
|
end
|
|
166
169
|
end
|
|
170
|
+
|
|
171
|
+
unless method_defined?(:==)
|
|
172
|
+
# Value-based equality.
|
|
173
|
+
# @param other [Object]
|
|
174
|
+
# @return [Boolean]
|
|
175
|
+
def ==(other)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
unless method_defined?(:eql?)
|
|
180
|
+
# Strict equality for Hash key and Set member semantics.
|
|
181
|
+
# @param other [Object]
|
|
182
|
+
# @return [Boolean]
|
|
183
|
+
def eql?(other)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
unless method_defined?(:hash)
|
|
188
|
+
# Hash value consistent with {#eql?} for use as Hash keys.
|
|
189
|
+
# @return [Integer]
|
|
190
|
+
def hash
|
|
191
|
+
end
|
|
192
|
+
end
|
|
167
193
|
end
|
|
168
194
|
|
|
169
195
|
# Operating system platform enumeration backed by Rust.
|
|
@@ -195,6 +221,36 @@ module Wreq
|
|
|
195
221
|
def to_s
|
|
196
222
|
end
|
|
197
223
|
end
|
|
224
|
+
|
|
225
|
+
unless method_defined?(:to_sym)
|
|
226
|
+
# Returns the platform as a lowercase symbol (e.g. :windows, :linux).
|
|
227
|
+
# @return [Symbol]
|
|
228
|
+
def to_sym
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
unless method_defined?(:==)
|
|
233
|
+
# Value-based equality.
|
|
234
|
+
# @param other [Object]
|
|
235
|
+
# @return [Boolean]
|
|
236
|
+
def ==(other)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
unless method_defined?(:eql?)
|
|
241
|
+
# Strict equality for Hash key and Set member semantics.
|
|
242
|
+
# @param other [Object]
|
|
243
|
+
# @return [Boolean]
|
|
244
|
+
def eql?(other)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
unless method_defined?(:hash)
|
|
249
|
+
# Hash value consistent with {#eql?} for use as Hash keys.
|
|
250
|
+
# @return [Integer]
|
|
251
|
+
def hash
|
|
252
|
+
end
|
|
253
|
+
end
|
|
198
254
|
end
|
|
199
255
|
|
|
200
256
|
# Emulation option wrapper.
|
|
@@ -216,19 +272,31 @@ module Wreq
|
|
|
216
272
|
#
|
|
217
273
|
# @param profile [Wreq::Profile, nil] Fingerprint profile to emulate
|
|
218
274
|
# @param platform [Wreq::Platform, nil] Operating system platform to emulate
|
|
219
|
-
# @param http2 [Boolean] Whether HTTP/2
|
|
220
|
-
#
|
|
275
|
+
# @param http2 [Boolean, nil] Whether HTTP/2 emulation is enabled; defaults
|
|
276
|
+
# to true when omitted or nil
|
|
277
|
+
# @param headers [Boolean, nil] Whether default emulation headers are enabled;
|
|
278
|
+
# defaults to true when omitted or nil
|
|
279
|
+
# @return [Wreq::Emulation] Configured emulation settings
|
|
280
|
+
# @raise [ArgumentError] if an option is unknown or extra arguments are given
|
|
281
|
+
# @raise [TypeError] if the option argument is not a Hash or a value has the
|
|
282
|
+
# wrong Ruby type
|
|
221
283
|
class Emulation
|
|
222
284
|
# Native fields and methods are set by the extension.
|
|
223
285
|
# This stub is for documentation only.
|
|
224
|
-
unless
|
|
286
|
+
unless singleton_methods(false).include?(:new)
|
|
225
287
|
# @param profile [Wreq::Profile, nil] Fingerprint profile to emulate
|
|
226
288
|
# @param platform [Wreq::Platform, nil] Operating system platform to emulate
|
|
227
|
-
# @param http2 [Boolean] Whether HTTP/2
|
|
228
|
-
#
|
|
289
|
+
# @param http2 [Boolean, nil] Whether HTTP/2 emulation is enabled; defaults
|
|
290
|
+
# to true when omitted or nil
|
|
291
|
+
# @param headers [Boolean, nil] Whether default emulation headers are enabled;
|
|
292
|
+
# defaults to true when omitted or nil
|
|
229
293
|
# @return [Wreq::Emulation] Configured emulation settings
|
|
230
|
-
|
|
294
|
+
# @raise [ArgumentError] if an option is unknown or extra arguments are given
|
|
295
|
+
# @raise [TypeError] if an option or value has the wrong Ruby type
|
|
296
|
+
def self.new(**options)
|
|
231
297
|
end
|
|
232
298
|
end
|
|
233
299
|
end
|
|
234
300
|
end
|
|
301
|
+
|
|
302
|
+
# standard:enable Naming/ConstantName
|
data/lib/wreq_ruby/error.rb
CHANGED
|
@@ -88,7 +88,7 @@ unless defined?(Wreq)
|
|
|
88
88
|
#
|
|
89
89
|
# @example
|
|
90
90
|
# begin
|
|
91
|
-
# client = Wreq::Client.new(max_redirects: 3)
|
|
91
|
+
# client = Wreq::Client.new(allow_redirects: true, max_redirects: 3)
|
|
92
92
|
# client.get("https://httpbin.io/redirect/10")
|
|
93
93
|
# rescue Wreq::RedirectError => e
|
|
94
94
|
# puts "Too many redirects: #{e.message}"
|
|
@@ -139,16 +139,14 @@ unless defined?(Wreq)
|
|
|
139
139
|
|
|
140
140
|
# Configuration and builder errors
|
|
141
141
|
|
|
142
|
-
#
|
|
142
|
+
# A native client or request configuration could not be built.
|
|
143
143
|
#
|
|
144
|
-
# Raised when
|
|
144
|
+
# Raised when validated Ruby options cannot be represented by the native
|
|
145
|
+
# builder or request body.
|
|
145
146
|
#
|
|
146
147
|
# @example
|
|
147
148
|
# begin
|
|
148
|
-
# client = Wreq::Client.new(
|
|
149
|
-
# proxy: "invalid://proxy",
|
|
150
|
-
# timeout: -1
|
|
151
|
-
# )
|
|
149
|
+
# client = Wreq::Client.new(proxy: "invalid://")
|
|
152
150
|
# rescue Wreq::BuilderError => e
|
|
153
151
|
# puts "Invalid configuration: #{e.message}"
|
|
154
152
|
# end
|
data/lib/wreq_ruby/http.rb
CHANGED
|
@@ -25,6 +25,43 @@ module Wreq
|
|
|
25
25
|
TRACE = nil # @return [Wreq::Method] HTTP TRACE method
|
|
26
26
|
PATCH = nil # @return [Wreq::Method] HTTP PATCH method
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
# Returns the HTTP method token (e.g. "GET", "POST").
|
|
30
|
+
# @return [String]
|
|
31
|
+
unless method_defined?(:to_s)
|
|
32
|
+
def to_s
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the HTTP method as a lowercase symbol (e.g. :get, :post).
|
|
37
|
+
# @return [Symbol]
|
|
38
|
+
unless method_defined?(:to_sym)
|
|
39
|
+
def to_sym
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Value-based equality. Returns true when both represent the same HTTP method.
|
|
44
|
+
# @param other [Object]
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
unless method_defined?(:==)
|
|
47
|
+
def ==(other)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Strict equality for Hash key and Set member semantics.
|
|
52
|
+
# @param other [Object]
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
unless method_defined?(:eql?)
|
|
55
|
+
def eql?(other)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Hash value consistent with {#eql?} for use as Hash keys.
|
|
60
|
+
# @return [Integer]
|
|
61
|
+
unless method_defined?(:hash)
|
|
62
|
+
def hash
|
|
63
|
+
end
|
|
64
|
+
end
|
|
28
65
|
end
|
|
29
66
|
|
|
30
67
|
# HTTP version enumeration backed by Rust.
|
|
@@ -63,6 +100,21 @@ module Wreq
|
|
|
63
100
|
def ==(other)
|
|
64
101
|
end
|
|
65
102
|
end
|
|
103
|
+
|
|
104
|
+
# Strict equality for Hash key and Set member semantics.
|
|
105
|
+
# @param other [Object]
|
|
106
|
+
# @return [Boolean]
|
|
107
|
+
unless method_defined?(:eql?)
|
|
108
|
+
def eql?(other)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Hash value consistent with {#eql?} for use as Hash keys.
|
|
113
|
+
# @return [Integer]
|
|
114
|
+
unless method_defined?(:hash)
|
|
115
|
+
def hash
|
|
116
|
+
end
|
|
117
|
+
end
|
|
66
118
|
end
|
|
67
119
|
|
|
68
120
|
# HTTP status code wrapper.
|
|
@@ -141,6 +193,28 @@ module Wreq
|
|
|
141
193
|
# @return [String] Status code as string
|
|
142
194
|
def to_s
|
|
143
195
|
end
|
|
196
|
+
|
|
197
|
+
# Returns the status code as an integer.
|
|
198
|
+
# @return [Integer] the numeric HTTP status code
|
|
199
|
+
def to_i
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Value-based equality. Only compares with other StatusCode instances.
|
|
203
|
+
# @param other [Object]
|
|
204
|
+
# @return [Boolean]
|
|
205
|
+
def ==(other)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Strict equality for Hash key and Set member semantics.
|
|
209
|
+
# @param other [Object]
|
|
210
|
+
# @return [Boolean]
|
|
211
|
+
def eql?(other)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Hash value consistent with {#eql?} for use as Hash keys.
|
|
215
|
+
# @return [Integer]
|
|
216
|
+
def hash
|
|
217
|
+
end
|
|
144
218
|
end
|
|
145
219
|
end
|
|
146
220
|
end
|
data/lib/wreq_ruby/response.rb
CHANGED
|
@@ -127,6 +127,9 @@ unless defined?(Wreq)
|
|
|
127
127
|
|
|
128
128
|
# Parse the response body as JSON.
|
|
129
129
|
#
|
|
130
|
+
# Integral JSON numbers are returned as arbitrary-precision Ruby Integer
|
|
131
|
+
# values. Fractional and exponent-form numbers are returned as Float values.
|
|
132
|
+
#
|
|
130
133
|
# @return [Object] Parsed JSON (Hash, Array, String, Integer, Float, Boolean, nil)
|
|
131
134
|
# @raise [Wreq::DecodingError] if body is not valid JSON
|
|
132
135
|
# @example
|
|
@@ -32,7 +32,12 @@ function Invoke-Step {
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
Write-Host "==> $Name"
|
|
35
|
+
# Native command failures do not honor ErrorActionPreference in Windows PowerShell.
|
|
36
|
+
$global:LASTEXITCODE = 0
|
|
35
37
|
& $Command
|
|
38
|
+
if ($LASTEXITCODE -ne 0) {
|
|
39
|
+
throw "$Name failed with exit code $LASTEXITCODE."
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
function Get-MissingUcrtTools {
|
|
@@ -122,6 +127,7 @@ Invoke-Step "Check MSYS2 UCRT build tools" {
|
|
|
122
127
|
$stillMissing = @(Get-MissingUcrtTools)
|
|
123
128
|
if ($stillMissing.Count -eq 0) {
|
|
124
129
|
Write-Warning "pacman returned exit code $LASTEXITCODE, but all required tools are present; continuing."
|
|
130
|
+
$global:LASTEXITCODE = 0
|
|
125
131
|
return
|
|
126
132
|
}
|
|
127
133
|
|
data/src/arch.rs
CHANGED
|
@@ -4,6 +4,28 @@
|
|
|
4
4
|
//! ABI boundaries, or OS APIs used by the Rust extension. Normal HTTP client
|
|
5
5
|
//! behavior should stay in the client/runtime modules so platform workarounds
|
|
6
6
|
//! do not leak into the rest of the binding.
|
|
7
|
+
#![allow(unsafe_code)]
|
|
8
|
+
|
|
9
|
+
/// Whether the native client exposes TCP user-timeout configuration.
|
|
10
|
+
pub(crate) const SUPPORTS_TCP_USER_TIMEOUT: bool = cfg!(any(
|
|
11
|
+
target_os = "android",
|
|
12
|
+
target_os = "fuchsia",
|
|
13
|
+
target_os = "linux"
|
|
14
|
+
));
|
|
15
|
+
|
|
16
|
+
/// Whether the native client exposes network-interface binding.
|
|
17
|
+
pub(crate) const SUPPORTS_INTERFACE: bool = cfg!(any(
|
|
18
|
+
target_os = "android",
|
|
19
|
+
target_os = "fuchsia",
|
|
20
|
+
target_os = "illumos",
|
|
21
|
+
target_os = "ios",
|
|
22
|
+
target_os = "linux",
|
|
23
|
+
target_os = "macos",
|
|
24
|
+
target_os = "solaris",
|
|
25
|
+
target_os = "tvos",
|
|
26
|
+
target_os = "visionos",
|
|
27
|
+
target_os = "watchos",
|
|
28
|
+
));
|
|
7
29
|
|
|
8
30
|
#[cfg(all(target_os = "windows", target_env = "gnu"))]
|
|
9
31
|
mod windows_gnu {
|
data/src/client/body/form.rs
CHANGED
data/src/client/body/json.rs
CHANGED
|
@@ -1,16 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
//! JSON conversion at the Ruby request and response boundary.
|
|
2
|
+
//!
|
|
3
|
+
//! Request values are converted from Ruby into an owned [`Json`] tree before
|
|
4
|
+
//! network I/O. Response bodies are deserialized into the same tree by wreq and
|
|
5
|
+
//! then converted back into Ruby values.
|
|
6
|
+
//!
|
|
7
|
+
//! The underlying `serde_json` configuration preserves object insertion order
|
|
8
|
+
//! and arbitrary-size integer tokens in both directions.
|
|
3
9
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
use ::serde::{Deserialize, Serialize};
|
|
11
|
+
use magnus::{Error, Ruby, TryConvert, Value};
|
|
12
|
+
|
|
13
|
+
use crate::{error::json_serialization_error, serde::deserialize_json};
|
|
14
|
+
|
|
15
|
+
/// An owned JSON tree shared by request and response conversion.
|
|
16
|
+
///
|
|
17
|
+
/// This wrapper keeps the configured `serde_json::Value` representation private
|
|
18
|
+
/// so callers use the same precision and ordering behavior in both directions.
|
|
19
|
+
#[derive(Deserialize, Serialize)]
|
|
20
|
+
#[serde(transparent)]
|
|
21
|
+
pub struct Json(serde_json::Value);
|
|
22
|
+
|
|
23
|
+
/// Convert supported Ruby request values into an owned JSON tree.
|
|
24
|
+
///
|
|
25
|
+
/// Supported values are `Hash`, `Array`, `String`, `Symbol`, `Integer`, finite
|
|
26
|
+
/// `Float`, booleans, and `nil`. Hash keys must be strings or symbols. Arrays
|
|
27
|
+
/// and hashes are limited to 100 nesting levels, which also bounds cyclic input.
|
|
28
|
+
/// Unsupported values are reported as `Wreq::BuilderError` before network I/O.
|
|
29
|
+
impl TryConvert for Json {
|
|
30
|
+
fn try_convert(value: Value) -> Result<Self, Error> {
|
|
31
|
+
let ruby = Ruby::get_with(value);
|
|
32
|
+
deserialize_json(&ruby, value)
|
|
33
|
+
.map(Self)
|
|
34
|
+
.map_err(|error| json_serialization_error(&ruby, error))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[cfg(test)]
|
|
39
|
+
mod tests {
|
|
40
|
+
use super::Json;
|
|
41
|
+
|
|
42
|
+
#[test]
|
|
43
|
+
fn preserves_number_precision_and_object_order() {
|
|
44
|
+
let source = br#"{"second":115792089237316195423570985008687907853269984665640564039457584007913129639936,"first":1}"#;
|
|
45
|
+
let json: Json = serde_json::from_slice(source).unwrap();
|
|
46
|
+
|
|
47
|
+
assert_eq!(source, serde_json::to_vec(&json).unwrap().as_slice());
|
|
48
|
+
}
|
|
16
49
|
}
|
data/src/client/body/stream.rs
CHANGED
|
@@ -1,34 +1,58 @@
|
|
|
1
|
+
//! Streaming request and response body support.
|
|
2
|
+
|
|
1
3
|
use std::{
|
|
4
|
+
cell::{Ref, RefCell, RefMut},
|
|
5
|
+
panic::catch_unwind,
|
|
2
6
|
pin::Pin,
|
|
3
|
-
sync::RwLock,
|
|
4
7
|
task::{Context, Poll},
|
|
5
8
|
};
|
|
6
9
|
|
|
7
10
|
use bytes::Bytes;
|
|
8
11
|
use futures_util::{Stream, StreamExt};
|
|
9
|
-
use magnus::{Error, RString,
|
|
10
|
-
use tokio::sync::{
|
|
11
|
-
Mutex,
|
|
12
|
-
mpsc::{self},
|
|
13
|
-
};
|
|
12
|
+
use magnus::{Error, Integer, RString, Ruby, Value, scan_args::scan_args};
|
|
13
|
+
use tokio::sync::{Mutex, Semaphore, mpsc};
|
|
14
14
|
|
|
15
15
|
use crate::{
|
|
16
|
-
error::{
|
|
16
|
+
error::{
|
|
17
|
+
argument_error, body_sender_borrow_error, body_sender_borrow_mut_error,
|
|
18
|
+
body_sender_send_error, closed_body_sender_error, memory_error, type_error, wreq_error,
|
|
19
|
+
},
|
|
17
20
|
rt,
|
|
18
21
|
};
|
|
19
22
|
|
|
23
|
+
/// Number of chunks buffered when Ruby omits the channel capacity.
|
|
24
|
+
const DEFAULT_CHANNEL_CAPACITY: usize = 8;
|
|
25
|
+
|
|
20
26
|
/// A receiver for streaming HTTP response bodies.
|
|
21
27
|
pub struct BodyReceiver(Mutex<Pin<Box<dyn Stream<Item = wreq::Result<Bytes>> + Send>>>);
|
|
22
28
|
|
|
23
|
-
/// A
|
|
29
|
+
/// A bounded producer for a single streaming HTTP request body.
|
|
30
|
+
///
|
|
31
|
+
/// The receiving side may be attached to one request. The producer remains
|
|
32
|
+
/// writable until [`BodySender::close`] is called or the request drops its
|
|
33
|
+
/// receiver. Ruby's GVL protects state access; no [`RefCell`] borrow is kept
|
|
34
|
+
/// while request backpressure waits without the GVL.
|
|
24
35
|
#[magnus::wrap(class = "Wreq::BodySender", free_immediately, size)]
|
|
25
|
-
pub struct BodySender(
|
|
36
|
+
pub struct BodySender(RefCell<InnerBodySender>);
|
|
26
37
|
|
|
38
|
+
/// Mutable ownership state for both halves of the body channel.
|
|
27
39
|
struct InnerBodySender {
|
|
40
|
+
/// Producing side, removed by [`BodySender::close`].
|
|
28
41
|
tx: Option<mpsc::Sender<Bytes>>,
|
|
42
|
+
/// Receiving side, removed when the sender is attached to a request.
|
|
29
43
|
rx: Option<mpsc::Receiver<Bytes>>,
|
|
30
44
|
}
|
|
31
45
|
|
|
46
|
+
impl InnerBodySender {
|
|
47
|
+
/// Return whether the channel can no longer accept body chunks.
|
|
48
|
+
fn is_closed(&self) -> bool {
|
|
49
|
+
match &self.tx {
|
|
50
|
+
Some(tx) => tx.is_closed(),
|
|
51
|
+
None => true,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
32
56
|
// ===== impl BodyReceiver =====
|
|
33
57
|
|
|
34
58
|
impl BodyReceiver {
|
|
@@ -39,8 +63,9 @@ impl BodyReceiver {
|
|
|
39
63
|
}
|
|
40
64
|
|
|
41
65
|
/// Read the next body chunk, converting stream errors into Ruby errors.
|
|
42
|
-
pub fn next(&self) -> Result<Option<Bytes>, Error> {
|
|
66
|
+
pub fn next(&self, ruby: &Ruby) -> Result<Option<Bytes>, Error> {
|
|
43
67
|
rt::try_block_on(
|
|
68
|
+
ruby,
|
|
44
69
|
async {
|
|
45
70
|
match self.0.lock().await.as_mut().next().await {
|
|
46
71
|
Some(Ok(data)) => Ok(Some(data)),
|
|
@@ -48,7 +73,7 @@ impl BodyReceiver {
|
|
|
48
73
|
None => Ok(None),
|
|
49
74
|
}
|
|
50
75
|
},
|
|
51
|
-
|
|
76
|
+
wreq_error,
|
|
52
77
|
)
|
|
53
78
|
}
|
|
54
79
|
}
|
|
@@ -56,54 +81,133 @@ impl BodyReceiver {
|
|
|
56
81
|
// ===== impl BodySender =====
|
|
57
82
|
|
|
58
83
|
impl BodySender {
|
|
59
|
-
///
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
/// Create a bounded request-body channel.
|
|
85
|
+
///
|
|
86
|
+
/// Ruby: `Wreq::BodySender.new(capacity = 8)`. Capacity must be greater
|
|
87
|
+
/// than zero and no larger than [`Semaphore::MAX_PERMITS`].
|
|
88
|
+
///
|
|
89
|
+
/// # Errors
|
|
90
|
+
///
|
|
91
|
+
/// Returns `TypeError` for a non-Integer capacity and `ArgumentError` for
|
|
92
|
+
/// an invalid range or argument count.
|
|
93
|
+
pub fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
|
|
94
|
+
let capacity = parse_capacity(ruby, args)?;
|
|
95
|
+
|
|
96
|
+
// Create the Tokio channel without allowing an unwind to cross the Ruby FFI boundary.
|
|
97
|
+
//
|
|
98
|
+
// Known panic conditions are rejected by [`parse_capacity`]. The unwind guard
|
|
99
|
+
// remains as a defensive fallback if Tokio adds another channel invariant.
|
|
100
|
+
let (tx, rx) =
|
|
101
|
+
catch_unwind(|| mpsc::channel(capacity)).map_err(|_| invalid_capacity_error(ruby))?;
|
|
102
|
+
|
|
103
|
+
Ok(BodySender(RefCell::new(InnerBodySender {
|
|
69
104
|
tx: Some(tx),
|
|
70
105
|
rx: Some(rx),
|
|
71
|
-
}))
|
|
106
|
+
})))
|
|
72
107
|
}
|
|
73
108
|
|
|
74
|
-
///
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
109
|
+
/// Push a binary chunk, waiting for capacity when the channel is full.
|
|
110
|
+
///
|
|
111
|
+
/// Ruby: `push(data)` where `data` is a String.
|
|
112
|
+
///
|
|
113
|
+
/// # Errors
|
|
114
|
+
///
|
|
115
|
+
/// Returns `IOError` after either channel side has closed. An interrupted
|
|
116
|
+
/// wait raises Ruby's standard `Interrupt` exception.
|
|
117
|
+
pub fn push(ruby: &Ruby, rb_self: &Self, data: RString) -> Result<(), Error> {
|
|
118
|
+
// Clone during the shared borrow, then release it before waiting
|
|
119
|
+
// for capacity. Request attachment needs a mutable borrow.
|
|
120
|
+
let tx = match &rb_self.read_inner(ruby)?.tx {
|
|
121
|
+
Some(tx) if !tx.is_closed() => tx.clone(),
|
|
122
|
+
_ => return Err(closed_body_sender_error(ruby)),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
rt::try_block_on(ruby, tx.send(data.to_bytes()), body_sender_send_error)
|
|
82
126
|
}
|
|
83
127
|
|
|
84
|
-
///
|
|
85
|
-
|
|
86
|
-
|
|
128
|
+
/// Close the producing side while retaining the receiver and queued chunks.
|
|
129
|
+
///
|
|
130
|
+
/// Calling this method more than once has no additional effect.
|
|
131
|
+
///
|
|
132
|
+
/// # Errors
|
|
133
|
+
///
|
|
134
|
+
/// Returns `Wreq::BodyError` if the internal state is already borrowed.
|
|
135
|
+
pub fn close(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
|
|
136
|
+
let mut inner = rb_self.write_inner(ruby)?;
|
|
87
137
|
inner.tx.take();
|
|
88
|
-
|
|
138
|
+
Ok(())
|
|
89
139
|
}
|
|
90
|
-
}
|
|
91
140
|
|
|
92
|
-
|
|
93
|
-
|
|
141
|
+
/// Return whether this sender can no longer accept body chunks.
|
|
142
|
+
///
|
|
143
|
+
/// # Errors
|
|
144
|
+
///
|
|
145
|
+
/// Returns `Wreq::BodyError` if the internal state is already borrowed.
|
|
146
|
+
pub fn is_closed(ruby: &Ruby, rb_self: &Self) -> Result<bool, Error> {
|
|
147
|
+
rb_self.read_inner(ruby).map(|r| r.is_closed())
|
|
148
|
+
}
|
|
94
149
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.
|
|
99
|
-
.
|
|
150
|
+
/// Borrow the channel state without panicking on accidental re-entry.
|
|
151
|
+
fn read_inner(&self, ruby: &Ruby) -> Result<Ref<'_, InnerBodySender>, Error> {
|
|
152
|
+
self.0
|
|
153
|
+
.try_borrow()
|
|
154
|
+
.map_err(|err| body_sender_borrow_error(ruby, err))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Mutably borrow the channel state without panicking on accidental re-entry.
|
|
158
|
+
fn write_inner(&self, ruby: &Ruby) -> Result<RefMut<'_, InnerBodySender>, Error> {
|
|
159
|
+
self.0
|
|
160
|
+
.try_borrow_mut()
|
|
161
|
+
.map_err(|err| body_sender_borrow_mut_error(ruby, err))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Move the receiving side into one request body.
|
|
165
|
+
///
|
|
166
|
+
/// # Errors
|
|
167
|
+
///
|
|
168
|
+
/// Returns `Wreq::MemoryError` if the receiver was already consumed, or
|
|
169
|
+
/// `Wreq::BodyError` if Ruby re-enters while the state is borrowed.
|
|
170
|
+
pub(super) fn take_receiver(&self, ruby: &Ruby) -> Result<ReceiverStream<Bytes>, Error> {
|
|
171
|
+
self.write_inner(ruby)?
|
|
100
172
|
.rx
|
|
101
173
|
.take()
|
|
102
174
|
.map(ReceiverStream::new)
|
|
103
|
-
.ok_or_else(memory_error)
|
|
175
|
+
.ok_or_else(|| memory_error(ruby))
|
|
104
176
|
}
|
|
105
177
|
}
|
|
106
178
|
|
|
179
|
+
/// Parse and validate the optional Ruby channel capacity.
|
|
180
|
+
///
|
|
181
|
+
/// [`mpsc::channel`] panics for zero or values above
|
|
182
|
+
/// [`Semaphore::MAX_PERMITS`], so validation must finish before channel creation.
|
|
183
|
+
fn parse_capacity(ruby: &Ruby, args: &[Value]) -> Result<usize, Error> {
|
|
184
|
+
let Some(value) = scan_args::<(), (Option<Value>,), (), (), (), ()>(args)?
|
|
185
|
+
.optional
|
|
186
|
+
.0
|
|
187
|
+
else {
|
|
188
|
+
return Ok(DEFAULT_CHANNEL_CAPACITY);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
let integer = Integer::from_value(value)
|
|
192
|
+
.ok_or_else(|| type_error(ruby, "capacity must be an Integer"))?;
|
|
193
|
+
let capacity = integer
|
|
194
|
+
.to_i64()
|
|
195
|
+
.ok()
|
|
196
|
+
.and_then(|capacity| usize::try_from(capacity).ok())
|
|
197
|
+
.filter(|capacity| (1..=Semaphore::MAX_PERMITS).contains(capacity))
|
|
198
|
+
.ok_or_else(|| invalid_capacity_error(ruby))?;
|
|
199
|
+
|
|
200
|
+
Ok(capacity)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// Build the synchronous Ruby error used for an invalid channel capacity.
|
|
204
|
+
fn invalid_capacity_error(ruby: &Ruby) -> Error {
|
|
205
|
+
argument_error(
|
|
206
|
+
ruby,
|
|
207
|
+
format!("capacity must be between 1 and {}", Semaphore::MAX_PERMITS),
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
107
211
|
/// A wrapper around [`tokio::sync::mpsc::Receiver`] that implements [`Stream`].
|
|
108
212
|
pub struct ReceiverStream<T> {
|
|
109
213
|
inner: mpsc::Receiver<T>,
|
data/src/client/body.rs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
mod form;
|
|
2
|
-
mod json;
|
|
3
|
-
mod stream;
|
|
1
|
+
pub mod form;
|
|
2
|
+
pub mod json;
|
|
3
|
+
pub mod stream;
|
|
4
4
|
|
|
5
5
|
use bytes::Bytes;
|
|
6
6
|
use futures_util::StreamExt;
|
|
@@ -9,29 +9,24 @@ use magnus::{
|
|
|
9
9
|
typed_data::Obj,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
pub use self::{
|
|
13
|
-
form::Form,
|
|
14
|
-
json::Json,
|
|
15
|
-
stream::{BodyReceiver, BodySender, ReceiverStream},
|
|
16
|
-
};
|
|
17
|
-
|
|
18
12
|
/// Represents the body of an HTTP request.
|
|
19
13
|
/// Supports text, bytes, and streaming bodies (Proc/Enumerator).
|
|
20
14
|
pub enum Body {
|
|
21
15
|
/// Static bytes body
|
|
22
16
|
Bytes(Bytes),
|
|
23
17
|
/// Streaming body
|
|
24
|
-
Stream(ReceiverStream<Bytes>),
|
|
18
|
+
Stream(stream::ReceiverStream<Bytes>),
|
|
25
19
|
}
|
|
26
20
|
|
|
27
21
|
impl TryConvert for Body {
|
|
28
22
|
fn try_convert(val: Value) -> Result<Self, Error> {
|
|
23
|
+
let ruby = Ruby::get_with(val);
|
|
29
24
|
if let Ok(s) = RString::try_convert(val) {
|
|
30
25
|
return Ok(Body::Bytes(s.to_bytes()));
|
|
31
26
|
}
|
|
32
27
|
|
|
33
|
-
let obj = Obj::<BodySender>::try_convert(val)?;
|
|
34
|
-
let stream =
|
|
28
|
+
let obj = Obj::<stream::BodySender>::try_convert(val)?;
|
|
29
|
+
let stream = obj.take_receiver(&ruby)?;
|
|
35
30
|
Ok(Body::Stream(stream))
|
|
36
31
|
}
|
|
37
32
|
}
|
|
@@ -50,8 +45,9 @@ impl From<Body> for wreq::Body {
|
|
|
50
45
|
|
|
51
46
|
pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
|
|
52
47
|
let sender_class = gem_module.define_class("BodySender", ruby.class_object())?;
|
|
53
|
-
sender_class.define_singleton_method("new", function!(BodySender::new, -1))?;
|
|
54
|
-
sender_class.define_method("push", method!(BodySender::push, 1))?;
|
|
55
|
-
sender_class.define_method("close", magnus::method!(BodySender::close, 0))?;
|
|
48
|
+
sender_class.define_singleton_method("new", function!(stream::BodySender::new, -1))?;
|
|
49
|
+
sender_class.define_method("push", method!(stream::BodySender::push, 1))?;
|
|
50
|
+
sender_class.define_method("close", magnus::method!(stream::BodySender::close, 0))?;
|
|
51
|
+
sender_class.define_method("closed?", magnus::method!(stream::BodySender::is_closed, 0))?;
|
|
56
52
|
Ok(())
|
|
57
53
|
}
|