wreq 1.2.3-x64-mingw-ucrt
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 +7 -0
- data/Cargo.lock +1687 -0
- data/Cargo.toml +54 -0
- data/Gemfile +18 -0
- data/LICENSE +201 -0
- data/README.md +153 -0
- data/Rakefile +90 -0
- data/build.rs +16 -0
- data/docs/windows-gnu-tokio-crash.md +70 -0
- data/examples/body.rb +42 -0
- data/examples/client.rb +33 -0
- data/examples/cookie.rb +24 -0
- data/examples/emulate_request.rb +37 -0
- data/examples/headers.rb +27 -0
- data/examples/proxy.rb +113 -0
- data/examples/send_stream.rb +85 -0
- data/examples/stream.rb +14 -0
- data/examples/thread_interrupt.rb +83 -0
- data/extconf.rb +7 -0
- data/lib/wreq.rb +303 -0
- data/lib/wreq_ruby/3.3/wreq_ruby.so +0 -0
- data/lib/wreq_ruby/3.4/wreq_ruby.so +0 -0
- data/lib/wreq_ruby/4.0/wreq_ruby.so +0 -0
- data/lib/wreq_ruby/body.rb +36 -0
- data/lib/wreq_ruby/client.rb +526 -0
- data/lib/wreq_ruby/cookie.rb +156 -0
- data/lib/wreq_ruby/emulate.rb +232 -0
- data/lib/wreq_ruby/error.rb +157 -0
- data/lib/wreq_ruby/header.rb +205 -0
- data/lib/wreq_ruby/http.rb +160 -0
- data/lib/wreq_ruby/response.rb +209 -0
- data/script/build_platform_gem.rb +34 -0
- data/script/build_windows_gnu.ps1 +257 -0
- data/src/arch.rs +33 -0
- data/src/client/body/form.rs +2 -0
- data/src/client/body/json.rs +16 -0
- data/src/client/body/stream.rs +146 -0
- data/src/client/body.rs +57 -0
- data/src/client/param.rs +19 -0
- data/src/client/query.rs +2 -0
- data/src/client/req.rs +274 -0
- data/src/client/resp.rs +248 -0
- data/src/client.rs +413 -0
- data/src/cookie.rs +312 -0
- data/src/emulate.rs +376 -0
- data/src/error.rs +163 -0
- data/src/extractor.rs +117 -0
- data/src/gvl.rs +154 -0
- data/src/header.rs +245 -0
- data/src/http.rs +142 -0
- data/src/lib.rs +98 -0
- data/src/macros.rs +123 -0
- data/src/rt.rs +46 -0
- data/test/client_cookie_test.rb +46 -0
- data/test/client_test.rb +136 -0
- data/test/cookie_test.rb +182 -0
- data/test/emulation_test.rb +21 -0
- data/test/error_handling_test.rb +92 -0
- data/test/header_test.rb +290 -0
- data/test/inspect_test.rb +125 -0
- data/test/module_methods_test.rb +75 -0
- data/test/orig_header_test.rb +115 -0
- data/test/request_parameters_test.rb +175 -0
- data/test/request_test.rb +244 -0
- data/test/response_test.rb +69 -0
- data/test/stream_test.rb +397 -0
- data/test/test_helper.rb +52 -0
- data/wreq.gemspec +68 -0
- metadata +123 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[switch]$SkipBundleInstall,
|
|
3
|
+
[switch]$SkipToolInstall,
|
|
4
|
+
[switch]$BuildGem,
|
|
5
|
+
[switch]$TestGem,
|
|
6
|
+
[switch]$RunTests,
|
|
7
|
+
[string]$SmokeUrl = $env:WREQ_SMOKE_URL
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
$ErrorActionPreference = "Stop"
|
|
11
|
+
|
|
12
|
+
$target = "x86_64-pc-windows-gnu"
|
|
13
|
+
$platform = "x64-mingw-ucrt"
|
|
14
|
+
$gemPattern = "pkg\wreq-*-$platform.gem"
|
|
15
|
+
if ([string]::IsNullOrWhiteSpace($SmokeUrl)) {
|
|
16
|
+
$SmokeUrl = "https://httpbin.io/get"
|
|
17
|
+
}
|
|
18
|
+
$rubyRoot = Split-Path (Split-Path (Get-Command ruby).Source -Parent) -Parent
|
|
19
|
+
$ucrt = Join-Path $rubyRoot "msys64\ucrt64"
|
|
20
|
+
$ucrtBin = Join-Path $ucrt "bin"
|
|
21
|
+
$msysPackages = @(
|
|
22
|
+
"mingw-w64-ucrt-x86_64-gcc",
|
|
23
|
+
"mingw-w64-ucrt-x86_64-clang",
|
|
24
|
+
"mingw-w64-ucrt-x86_64-cmake",
|
|
25
|
+
"mingw-w64-ucrt-x86_64-pkgconf"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
function Invoke-Step {
|
|
29
|
+
param(
|
|
30
|
+
[string]$Name,
|
|
31
|
+
[scriptblock]$Command
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
Write-Host "==> $Name"
|
|
35
|
+
& $Command
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function Get-MissingUcrtTools {
|
|
39
|
+
$missing = @()
|
|
40
|
+
|
|
41
|
+
if (-not (Test-Path (Join-Path $ucrtBin "gcc.exe")) -or -not (Test-Path (Join-Path $ucrtBin "g++.exe"))) {
|
|
42
|
+
$missing += "gcc/g++"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (-not (Test-Path (Join-Path $ucrtBin "clang.exe")) -or -not (Test-Path (Join-Path $ucrtBin "libclang.dll"))) {
|
|
46
|
+
$missing += "clang/libclang"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (-not (Test-Path (Join-Path $ucrtBin "cmake.exe"))) {
|
|
50
|
+
$missing += "cmake"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (-not (Test-Path (Join-Path $ucrtBin "pkgconf.exe")) -and -not (Test-Path (Join-Path $ucrtBin "pkg-config.exe"))) {
|
|
54
|
+
$missing += "pkgconf"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
$missing
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Get-LatestPlatformGem {
|
|
61
|
+
$gem = Get-ChildItem -Path $gemPattern -ErrorAction SilentlyContinue |
|
|
62
|
+
Sort-Object LastWriteTime -Descending |
|
|
63
|
+
Select-Object -First 1
|
|
64
|
+
|
|
65
|
+
if (-not $gem) {
|
|
66
|
+
throw "No Windows GNU platform gem found at '$gemPattern'. Re-run with -BuildGem first."
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
$gem
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function Invoke-RubySmoke {
|
|
73
|
+
param(
|
|
74
|
+
[string]$Name,
|
|
75
|
+
[string]$Code
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
Write-Host "Smoke: $Name"
|
|
79
|
+
ruby -e $Code
|
|
80
|
+
if ($LASTEXITCODE -ne 0) {
|
|
81
|
+
throw "$Name failed with exit code $LASTEXITCODE."
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Invoke-Step "Check Ruby platform" {
|
|
86
|
+
$rubyArch = & ruby -rrbconfig -e "print RbConfig::CONFIG['arch']"
|
|
87
|
+
if ($rubyArch -ne $platform) {
|
|
88
|
+
throw "Expected Ruby platform '$platform', got '$rubyArch'. Use RubyInstaller UCRT for this build."
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
ruby -v
|
|
92
|
+
ruby -rrbconfig -rrubygems -e "puts RbConfig::CONFIG.values_at('arch', 'host', 'CC').join(%q{ }); puts Gem::Platform.local"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Invoke-Step "Install Rust target" {
|
|
96
|
+
$installedTargets = @(rustup target list --installed)
|
|
97
|
+
if ($installedTargets -contains $target) {
|
|
98
|
+
Write-Host "Rust target $target already installed; skipping rustup."
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
rustup target add $target
|
|
103
|
+
if ($LASTEXITCODE -ne 0) {
|
|
104
|
+
throw "Failed to install Rust target $target."
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Invoke-Step "Check MSYS2 UCRT build tools" {
|
|
109
|
+
$missing = @(Get-MissingUcrtTools)
|
|
110
|
+
if ($missing.Count -eq 0) {
|
|
111
|
+
Write-Host "MSYS2 UCRT build tools already present; skipping pacman."
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if ($SkipToolInstall) {
|
|
116
|
+
throw "Missing MSYS2 UCRT build tools: $($missing -join ', '). Re-run without -SkipToolInstall or install them with ridk."
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Write-Host "Missing MSYS2 UCRT build tools: $($missing -join ', ')"
|
|
120
|
+
ridk exec pacman -S --needed --noconfirm @msysPackages
|
|
121
|
+
if ($LASTEXITCODE -ne 0) {
|
|
122
|
+
$stillMissing = @(Get-MissingUcrtTools)
|
|
123
|
+
if ($stillMissing.Count -eq 0) {
|
|
124
|
+
Write-Warning "pacman returned exit code $LASTEXITCODE, but all required tools are present; continuing."
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw "Failed to install MSYS2 UCRT build tools. Missing: $($stillMissing -join ', '). If pacman cannot lock its database, close other pacman/ridk shells, run PowerShell as Administrator, or install RubyInstaller in a user-writable directory."
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Invoke-Step "Configure Windows GNU toolchain" {
|
|
133
|
+
$env:CARGO_BUILD_TARGET = $target
|
|
134
|
+
$env:LIBCLANG_PATH = $ucrtBin
|
|
135
|
+
$env:CC_x86_64_pc_windows_gnu = Join-Path $ucrtBin "gcc.exe"
|
|
136
|
+
$env:CXX_x86_64_pc_windows_gnu = Join-Path $ucrtBin "g++.exe"
|
|
137
|
+
$env:CMAKE = Join-Path $ucrtBin "cmake.exe"
|
|
138
|
+
Remove-Item Env:\CMAKE_GENERATOR -ErrorAction SilentlyContinue
|
|
139
|
+
|
|
140
|
+
rustc -Vv
|
|
141
|
+
Write-Host "LIBCLANG_PATH=$env:LIBCLANG_PATH"
|
|
142
|
+
Write-Host "CC_x86_64_pc_windows_gnu=$env:CC_x86_64_pc_windows_gnu"
|
|
143
|
+
Write-Host "CXX_x86_64_pc_windows_gnu=$env:CXX_x86_64_pc_windows_gnu"
|
|
144
|
+
Write-Host "CMAKE=$env:CMAKE"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (-not $SkipBundleInstall) {
|
|
148
|
+
Invoke-Step "Install Ruby dependencies" {
|
|
149
|
+
bundle install
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Invoke-Step "Compile native extension" {
|
|
154
|
+
ridk exec ruby -S bundle exec rake compile
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
Invoke-Step "Verify extension loads" {
|
|
158
|
+
ruby -Ilib -rwreq -e "puts Wreq::VERSION; puts Wreq::Client.new.class"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if ($RunTests) {
|
|
162
|
+
Invoke-Step "Run tests" {
|
|
163
|
+
ridk exec ruby -S bundle exec rake test
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if ($BuildGem) {
|
|
168
|
+
Invoke-Step "Build local platform gem" {
|
|
169
|
+
$rubyMinor = & ruby -e "print RUBY_VERSION[/\A\d+\.\d+/]"
|
|
170
|
+
$dest = "lib\wreq_ruby\$rubyMinor"
|
|
171
|
+
New-Item -ItemType Directory -Force $dest | Out-Null
|
|
172
|
+
Copy-Item "lib\wreq_ruby\wreq_ruby.so" (Join-Path $dest "wreq_ruby.so") -Force
|
|
173
|
+
|
|
174
|
+
ruby script/build_platform_gem.rb $platform
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if ($TestGem) {
|
|
179
|
+
Invoke-Step "Install and smoke test platform gem" {
|
|
180
|
+
$gem = Get-LatestPlatformGem
|
|
181
|
+
$gemHome = Join-Path ([System.IO.Path]::GetTempPath()) "wreq-gem-test-$PID"
|
|
182
|
+
$gemBin = Join-Path $gemHome "bin"
|
|
183
|
+
$testDir = Join-Path ([System.IO.Path]::GetTempPath()) "wreq-gem-test-cwd-$PID"
|
|
184
|
+
|
|
185
|
+
Remove-Item -Recurse -Force $gemHome, $testDir -ErrorAction SilentlyContinue
|
|
186
|
+
New-Item -ItemType Directory -Force $gemHome, $gemBin, $testDir | Out-Null
|
|
187
|
+
|
|
188
|
+
$oldGemHome = $env:GEM_HOME
|
|
189
|
+
$oldGemPath = $env:GEM_PATH
|
|
190
|
+
$oldGemrc = $env:GEMRC
|
|
191
|
+
$oldRubyopt = $env:RUBYOPT
|
|
192
|
+
$oldRubylib = $env:RUBYLIB
|
|
193
|
+
$oldBundleGemfile = $env:BUNDLE_GEMFILE
|
|
194
|
+
$oldBundleBinPath = $env:BUNDLE_BIN_PATH
|
|
195
|
+
$oldBundlerVersion = $env:BUNDLER_VERSION
|
|
196
|
+
$oldSmokeUrl = $env:WREQ_SMOKE_URL
|
|
197
|
+
$oldWreqGemHome = $env:WREQ_GEM_HOME
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
$env:GEM_HOME = $gemHome
|
|
201
|
+
$env:GEM_PATH = $gemHome
|
|
202
|
+
$env:GEMRC = ""
|
|
203
|
+
$env:RUBYOPT = ""
|
|
204
|
+
$env:RUBYLIB = ""
|
|
205
|
+
$env:BUNDLE_GEMFILE = ""
|
|
206
|
+
$env:BUNDLE_BIN_PATH = ""
|
|
207
|
+
$env:BUNDLER_VERSION = ""
|
|
208
|
+
$env:WREQ_SMOKE_URL = $SmokeUrl
|
|
209
|
+
$env:WREQ_GEM_HOME = $gemHome
|
|
210
|
+
|
|
211
|
+
gem install --norc --local --install-dir $gemHome --bindir $gemBin --no-document --force --no-user-install $gem.FullName
|
|
212
|
+
if ($LASTEXITCODE -ne 0) {
|
|
213
|
+
throw "Failed to install $($gem.FullName)."
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
Push-Location $testDir
|
|
217
|
+
try {
|
|
218
|
+
Invoke-RubySmoke "Load installed platform gem" @'
|
|
219
|
+
STDOUT.sync = true
|
|
220
|
+
STDERR.sync = true
|
|
221
|
+
require "wreq"
|
|
222
|
+
spec = Gem.loaded_specs.fetch("wreq")
|
|
223
|
+
expected = File.expand_path(ENV.fetch("WREQ_GEM_HOME"))
|
|
224
|
+
actual = File.expand_path(spec.full_gem_path)
|
|
225
|
+
raise "loaded #{actual}, expected under #{expected}" unless actual.start_with?(expected)
|
|
226
|
+
puts "loaded #{spec.full_name}"
|
|
227
|
+
puts actual
|
|
228
|
+
puts "wreq #{Wreq::VERSION}"
|
|
229
|
+
'@
|
|
230
|
+
|
|
231
|
+
Invoke-RubySmoke "Request through installed platform gem" @'
|
|
232
|
+
STDOUT.sync = true
|
|
233
|
+
STDERR.sync = true
|
|
234
|
+
require "wreq"
|
|
235
|
+
client = Wreq::Client.new
|
|
236
|
+
resp = client.get(ENV.fetch("WREQ_SMOKE_URL"), timeout: 20)
|
|
237
|
+
puts "HTTP #{resp.code}"
|
|
238
|
+
raise "unexpected status #{resp.code}" unless resp.code == 200
|
|
239
|
+
'@
|
|
240
|
+
} finally {
|
|
241
|
+
Pop-Location
|
|
242
|
+
}
|
|
243
|
+
} finally {
|
|
244
|
+
$env:GEM_HOME = $oldGemHome
|
|
245
|
+
$env:GEM_PATH = $oldGemPath
|
|
246
|
+
$env:GEMRC = $oldGemrc
|
|
247
|
+
$env:RUBYOPT = $oldRubyopt
|
|
248
|
+
$env:RUBYLIB = $oldRubylib
|
|
249
|
+
$env:BUNDLE_GEMFILE = $oldBundleGemfile
|
|
250
|
+
$env:BUNDLE_BIN_PATH = $oldBundleBinPath
|
|
251
|
+
$env:BUNDLER_VERSION = $oldBundlerVersion
|
|
252
|
+
$env:WREQ_SMOKE_URL = $oldSmokeUrl
|
|
253
|
+
$env:WREQ_GEM_HOME = $oldWreqGemHome
|
|
254
|
+
Remove-Item -Recurse -Force $gemHome, $testDir -ErrorAction SilentlyContinue
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
data/src/arch.rs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//! Platform-specific support code.
|
|
2
|
+
//!
|
|
3
|
+
//! Keep this module small and focused on platform quirks that affect linking,
|
|
4
|
+
//! ABI boundaries, or OS APIs used by the Rust extension. Normal HTTP client
|
|
5
|
+
//! behavior should stay in the client/runtime modules so platform workarounds
|
|
6
|
+
//! do not leak into the rest of the binding.
|
|
7
|
+
|
|
8
|
+
#[cfg(all(target_os = "windows", target_env = "gnu"))]
|
|
9
|
+
mod windows_gnu {
|
|
10
|
+
//! Windows GNU support.
|
|
11
|
+
//!
|
|
12
|
+
//! RubyInstaller's GNU/UCRT Ruby exports some symbols with the same names as
|
|
13
|
+
//! Win32 APIs. When GNU ld links this extension against Ruby, those symbols
|
|
14
|
+
//! can shadow the real Windows APIs unless we pin the calls we care about.
|
|
15
|
+
|
|
16
|
+
#[link(name = "kernel32")]
|
|
17
|
+
unsafe extern "system" {
|
|
18
|
+
fn SleepEx(milliseconds: u32, alertable: i32) -> u32;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// RubyInstaller's GNU/UCRT Ruby exports a Ruby-aware Sleep symbol. GNU ld can
|
|
22
|
+
// bind extension calls to that symbol instead of KERNEL32!Sleep, which crashes
|
|
23
|
+
// on native worker threads that have no Ruby TLS. The linker wrapper keeps those
|
|
24
|
+
// calls on the Windows API path.
|
|
25
|
+
#[unsafe(no_mangle)]
|
|
26
|
+
pub extern "system" fn __wrap_Sleep(milliseconds: u32) {
|
|
27
|
+
// SAFETY: SleepEx is a Win32 API. Passing alertable=false matches Sleep's
|
|
28
|
+
// behavior, and any u32 duration is accepted by the API.
|
|
29
|
+
unsafe {
|
|
30
|
+
SleepEx(milliseconds, 0);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
use indexmap::IndexMap;
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
|
|
4
|
+
/// Represents a JSON value for HTTP requests.
|
|
5
|
+
/// Supports objects, arrays, numbers, strings, booleans, and null.
|
|
6
|
+
#[derive(Serialize, Deserialize)]
|
|
7
|
+
#[serde(untagged)]
|
|
8
|
+
pub enum Json {
|
|
9
|
+
Object(IndexMap<String, Json>),
|
|
10
|
+
Boolean(bool),
|
|
11
|
+
Number(isize),
|
|
12
|
+
Float(f64),
|
|
13
|
+
String(String),
|
|
14
|
+
Null(Option<isize>),
|
|
15
|
+
Array(Vec<Json>),
|
|
16
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
pin::Pin,
|
|
3
|
+
sync::RwLock,
|
|
4
|
+
task::{Context, Poll},
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
use bytes::Bytes;
|
|
8
|
+
use futures_util::{Stream, StreamExt};
|
|
9
|
+
use magnus::{Error, RString, TryConvert, Value};
|
|
10
|
+
use tokio::sync::{
|
|
11
|
+
Mutex,
|
|
12
|
+
mpsc::{self},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
use crate::{
|
|
16
|
+
error::{memory_error, mpsc_send_error_to_magnus, wreq_error_to_magnus},
|
|
17
|
+
rt,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/// A receiver for streaming HTTP response bodies.
|
|
21
|
+
pub struct BodyReceiver(Mutex<Pin<Box<dyn Stream<Item = wreq::Result<Bytes>> + Send>>>);
|
|
22
|
+
|
|
23
|
+
/// A sender for streaming HTTP request bodies.
|
|
24
|
+
#[magnus::wrap(class = "Wreq::BodySender", free_immediately, size)]
|
|
25
|
+
pub struct BodySender(RwLock<InnerBodySender>);
|
|
26
|
+
|
|
27
|
+
struct InnerBodySender {
|
|
28
|
+
tx: Option<mpsc::Sender<Bytes>>,
|
|
29
|
+
rx: Option<mpsc::Receiver<Bytes>>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ===== impl BodyReceiver =====
|
|
33
|
+
|
|
34
|
+
impl BodyReceiver {
|
|
35
|
+
/// Create a new [`BodyReceiver`] instance.
|
|
36
|
+
#[inline]
|
|
37
|
+
pub fn new(stream: impl Stream<Item = wreq::Result<Bytes>> + Send + 'static) -> BodyReceiver {
|
|
38
|
+
BodyReceiver(Mutex::new(Box::pin(stream)))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Read the next body chunk, converting stream errors into Ruby errors.
|
|
42
|
+
pub fn next(&self) -> Result<Option<Bytes>, Error> {
|
|
43
|
+
rt::try_block_on(
|
|
44
|
+
async {
|
|
45
|
+
match self.0.lock().await.as_mut().next().await {
|
|
46
|
+
Some(Ok(data)) => Ok(Some(data)),
|
|
47
|
+
Some(Err(err)) => Err(err),
|
|
48
|
+
None => Ok(None),
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
wreq_error_to_magnus,
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ===== impl BodySender =====
|
|
57
|
+
|
|
58
|
+
impl BodySender {
|
|
59
|
+
/// Ruby: `Wreq::Sender.new(capacity = 8)`
|
|
60
|
+
pub fn new(args: &[Value]) -> Self {
|
|
61
|
+
let capacity: usize = if let Some(v) = args.first() {
|
|
62
|
+
usize::try_convert(*v).unwrap_or(8)
|
|
63
|
+
} else {
|
|
64
|
+
8
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let (tx, rx) = mpsc::channel(capacity);
|
|
68
|
+
BodySender(RwLock::new(InnerBodySender {
|
|
69
|
+
tx: Some(tx),
|
|
70
|
+
rx: Some(rx),
|
|
71
|
+
}))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Ruby: `push(data)` where data is String or bytes
|
|
75
|
+
pub fn push(rb_self: &Self, data: RString) -> Result<(), Error> {
|
|
76
|
+
let bytes = data.to_bytes();
|
|
77
|
+
let inner = rb_self.0.read().unwrap();
|
|
78
|
+
if let Some(ref tx) = inner.tx {
|
|
79
|
+
rt::try_block_on(tx.send(bytes), mpsc_send_error_to_magnus)?;
|
|
80
|
+
}
|
|
81
|
+
Ok(())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Ruby: `close` to close the sender
|
|
85
|
+
pub fn close(&self) {
|
|
86
|
+
let mut inner = self.0.write().unwrap();
|
|
87
|
+
inner.tx.take();
|
|
88
|
+
inner.rx.take();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
impl TryFrom<&BodySender> for ReceiverStream<Bytes> {
|
|
93
|
+
type Error = magnus::Error;
|
|
94
|
+
|
|
95
|
+
fn try_from(sender: &BodySender) -> Result<Self, Self::Error> {
|
|
96
|
+
sender
|
|
97
|
+
.0
|
|
98
|
+
.write()
|
|
99
|
+
.unwrap()
|
|
100
|
+
.rx
|
|
101
|
+
.take()
|
|
102
|
+
.map(ReceiverStream::new)
|
|
103
|
+
.ok_or_else(memory_error)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// A wrapper around [`tokio::sync::mpsc::Receiver`] that implements [`Stream`].
|
|
108
|
+
pub struct ReceiverStream<T> {
|
|
109
|
+
inner: mpsc::Receiver<T>,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
impl<T> ReceiverStream<T> {
|
|
113
|
+
/// Create a new [`ReceiverStream`].
|
|
114
|
+
#[inline]
|
|
115
|
+
pub fn new(recv: mpsc::Receiver<T>) -> Self {
|
|
116
|
+
Self { inner: recv }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
impl<T> Stream for ReceiverStream<T> {
|
|
121
|
+
type Item = T;
|
|
122
|
+
|
|
123
|
+
#[inline]
|
|
124
|
+
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
|
125
|
+
self.inner.poll_recv(cx)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Returns the bounds of the stream based on the underlying receiver.
|
|
129
|
+
///
|
|
130
|
+
/// For open channels, it returns `(receiver.len(), None)`.
|
|
131
|
+
///
|
|
132
|
+
/// For closed channels, it returns `(receiver.len(), Some(used_capacity))`
|
|
133
|
+
/// where `used_capacity` is calculated as `receiver.max_capacity() -
|
|
134
|
+
/// receiver.capacity()`. This accounts for any [`Permit`] that is still
|
|
135
|
+
/// able to send a message.
|
|
136
|
+
///
|
|
137
|
+
/// [`Permit`]: struct@tokio::sync::mpsc::Permit
|
|
138
|
+
fn size_hint(&self) -> (usize, Option<usize>) {
|
|
139
|
+
if self.inner.is_closed() {
|
|
140
|
+
let used_capacity = self.inner.max_capacity() - self.inner.capacity();
|
|
141
|
+
(self.inner.len(), Some(used_capacity))
|
|
142
|
+
} else {
|
|
143
|
+
(self.inner.len(), None)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
data/src/client/body.rs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
mod form;
|
|
2
|
+
mod json;
|
|
3
|
+
mod stream;
|
|
4
|
+
|
|
5
|
+
use bytes::Bytes;
|
|
6
|
+
use futures_util::StreamExt;
|
|
7
|
+
use magnus::{
|
|
8
|
+
Error, Module, Object, RModule, RString, Ruby, TryConvert, Value, function, method,
|
|
9
|
+
typed_data::Obj,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
pub use self::{
|
|
13
|
+
form::Form,
|
|
14
|
+
json::Json,
|
|
15
|
+
stream::{BodyReceiver, BodySender, ReceiverStream},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/// Represents the body of an HTTP request.
|
|
19
|
+
/// Supports text, bytes, and streaming bodies (Proc/Enumerator).
|
|
20
|
+
pub enum Body {
|
|
21
|
+
/// Static bytes body
|
|
22
|
+
Bytes(Bytes),
|
|
23
|
+
/// Streaming body
|
|
24
|
+
Stream(ReceiverStream<Bytes>),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl TryConvert for Body {
|
|
28
|
+
fn try_convert(val: Value) -> Result<Self, Error> {
|
|
29
|
+
if let Ok(s) = RString::try_convert(val) {
|
|
30
|
+
return Ok(Body::Bytes(s.to_bytes()));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let obj = Obj::<BodySender>::try_convert(val)?;
|
|
34
|
+
let stream = ReceiverStream::try_from(&*obj)?;
|
|
35
|
+
Ok(Body::Stream(stream))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl From<Body> for wreq::Body {
|
|
40
|
+
fn from(body: Body) -> Self {
|
|
41
|
+
match body {
|
|
42
|
+
Body::Bytes(b) => wreq::Body::from(b),
|
|
43
|
+
Body::Stream(stream) => {
|
|
44
|
+
let try_stream = stream.map(Ok::<Bytes, std::io::Error>);
|
|
45
|
+
wreq::Body::wrap_stream(try_stream)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
|
|
52
|
+
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))?;
|
|
56
|
+
Ok(())
|
|
57
|
+
}
|
data/src/client/param.rs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
use indexmap::IndexMap;
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
|
|
4
|
+
/// Represents HTTP parameters from Python as either a mapping or a sequence of key-value pairs.
|
|
5
|
+
pub type Params = IndexMap<String, ParamValue>;
|
|
6
|
+
|
|
7
|
+
/// Represents a single parameter value that can be automatically converted from Python types.
|
|
8
|
+
#[derive(Serialize, Deserialize)]
|
|
9
|
+
#[serde(untagged)]
|
|
10
|
+
pub enum ParamValue {
|
|
11
|
+
/// A boolean value from Python `bool`.
|
|
12
|
+
Boolean(bool),
|
|
13
|
+
/// An integer value from Python `int`.
|
|
14
|
+
Number(isize),
|
|
15
|
+
/// A floating-point value from Python `float`.
|
|
16
|
+
Float64(f64),
|
|
17
|
+
/// A string value from Python `str`.
|
|
18
|
+
String(String),
|
|
19
|
+
}
|
data/src/client/query.rs
ADDED