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
data/src/error.rs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
use magnus::{
|
|
2
|
+
Error as MagnusError, RModule, Ruby, exception::ExceptionClass, prelude::*, value::Lazy,
|
|
3
|
+
};
|
|
4
|
+
use tokio::sync::mpsc::error::SendError;
|
|
5
|
+
|
|
6
|
+
const RACE_CONDITION_ERROR_MSG: &str = r#"Due to Rust's memory management with borrowing,
|
|
7
|
+
you cannot use certain instances multiple times as they may be consumed.
|
|
8
|
+
|
|
9
|
+
This error can occur in the following cases:
|
|
10
|
+
1) You passed a non-clonable instance to a function that requires ownership.
|
|
11
|
+
2) You attempted to use a method that consumes ownership more than once (e.g., reading a response body twice).
|
|
12
|
+
3) You tried to reference an instance after it was borrowed.
|
|
13
|
+
|
|
14
|
+
Potential solutions:
|
|
15
|
+
1) Avoid sharing instances; create a new instance each time you use it.
|
|
16
|
+
2) Refrain from performing actions that consume ownership multiple times.
|
|
17
|
+
3) Change the order of operations to reference the instance before borrowing it.
|
|
18
|
+
"#;
|
|
19
|
+
|
|
20
|
+
static WREQ: Lazy<RModule> = Lazy::new(|ruby| ruby.define_module(crate::RUBY_MODULE_NAME).unwrap());
|
|
21
|
+
|
|
22
|
+
macro_rules! define_exception {
|
|
23
|
+
($name:ident, $ruby_name:literal, $parent_method:ident) => {
|
|
24
|
+
static $name: Lazy<ExceptionClass> = Lazy::new(|ruby| {
|
|
25
|
+
ruby.get_inner(&WREQ)
|
|
26
|
+
.define_error($ruby_name, ruby.$parent_method())
|
|
27
|
+
.unwrap()
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
macro_rules! map_wreq_error {
|
|
33
|
+
($ruby:expr, $err:expr, $msg:expr, $($check_method:ident => $exception:ident),* $(,)?) => {
|
|
34
|
+
{
|
|
35
|
+
$(
|
|
36
|
+
if $err.$check_method() {
|
|
37
|
+
return MagnusError::new($ruby.get_inner(&$exception), $msg);
|
|
38
|
+
}
|
|
39
|
+
)*
|
|
40
|
+
MagnusError::new($ruby.exception_runtime_error(), $msg)
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// System-level and runtime errors
|
|
46
|
+
define_exception!(MEMORY, "MemoryError", exception_runtime_error);
|
|
47
|
+
|
|
48
|
+
// Network connection errors
|
|
49
|
+
define_exception!(CONNECTION_ERROR, "ConnectionError", exception_runtime_error);
|
|
50
|
+
define_exception!(
|
|
51
|
+
PROXY_CONNECTION_ERROR,
|
|
52
|
+
"ProxyConnectionError",
|
|
53
|
+
exception_runtime_error
|
|
54
|
+
);
|
|
55
|
+
define_exception!(
|
|
56
|
+
CONNECTION_RESET_ERROR,
|
|
57
|
+
"ConnectionResetError",
|
|
58
|
+
exception_runtime_error
|
|
59
|
+
);
|
|
60
|
+
define_exception!(TLS_ERROR, "TlsError", exception_runtime_error);
|
|
61
|
+
|
|
62
|
+
// HTTP protocol and request/response errors
|
|
63
|
+
define_exception!(REQUEST_ERROR, "RequestError", exception_runtime_error);
|
|
64
|
+
define_exception!(STATUS_ERROR, "StatusError", exception_runtime_error);
|
|
65
|
+
define_exception!(REDIRECT_ERROR, "RedirectError", exception_runtime_error);
|
|
66
|
+
define_exception!(TIMEOUT_ERROR, "TimeoutError", exception_runtime_error);
|
|
67
|
+
|
|
68
|
+
// Data processing and encoding errors
|
|
69
|
+
define_exception!(BODY_ERROR, "BodyError", exception_runtime_error);
|
|
70
|
+
define_exception!(DECODING_ERROR, "DecodingError", exception_runtime_error);
|
|
71
|
+
|
|
72
|
+
// Configuration and builder errors
|
|
73
|
+
define_exception!(BUILDER_ERROR, "BuilderError", exception_runtime_error);
|
|
74
|
+
|
|
75
|
+
// Thread interruption error
|
|
76
|
+
define_exception!(INTERRUPT_ERROR, "InterruptError", exception_interrupt);
|
|
77
|
+
|
|
78
|
+
/// Memory error constant
|
|
79
|
+
pub fn memory_error() -> MagnusError {
|
|
80
|
+
MagnusError::new(ruby!().get_inner(&MEMORY), RACE_CONDITION_ERROR_MSG)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Thread interruption error (raised when Thread.kill cancels a request)
|
|
84
|
+
pub fn interrupt_error() -> MagnusError {
|
|
85
|
+
MagnusError::new(ruby!().get_inner(&INTERRUPT_ERROR), "request interrupted")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// LocalJumpError for methods that require a Ruby block.
|
|
89
|
+
pub fn no_block_given_error() -> MagnusError {
|
|
90
|
+
MagnusError::new(
|
|
91
|
+
ruby!().exception_local_jump_error(),
|
|
92
|
+
"no block given (yield)",
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Map [`tokio::sync::mpsc::error::SendError`] to corresponding [`magnus::Error`]
|
|
97
|
+
pub fn mpsc_send_error_to_magnus<T>(err: SendError<T>) -> MagnusError {
|
|
98
|
+
MagnusError::new(
|
|
99
|
+
ruby!().get_inner(&BODY_ERROR),
|
|
100
|
+
format!("failed to send body chunk: {}", err),
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Map [`wreq::header::InvalidHeaderName`] to corresponding [`magnus::Error`]
|
|
105
|
+
pub fn header_name_error_to_magnus(err: wreq::header::InvalidHeaderName) -> MagnusError {
|
|
106
|
+
MagnusError::new(
|
|
107
|
+
ruby!().get_inner(&BUILDER_ERROR),
|
|
108
|
+
format!("invalid header name: {err}"),
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Map [`wreq::header::InvalidHeaderValue`] to corresponding [`magnus::Error`]
|
|
113
|
+
pub fn header_value_error_to_magnus(err: wreq::header::InvalidHeaderValue) -> MagnusError {
|
|
114
|
+
MagnusError::new(
|
|
115
|
+
ruby!().get_inner(&BUILDER_ERROR),
|
|
116
|
+
format!("invalid header value: {err}"),
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Map type/value errors to corresponding [`magnus::Error`]
|
|
121
|
+
pub fn type_value_error_to_magnus(err: &str) -> MagnusError {
|
|
122
|
+
MagnusError::new(
|
|
123
|
+
ruby!().get_inner(&BUILDER_ERROR),
|
|
124
|
+
format!("type error: {err}"),
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Map [`wreq::Error`] to corresponding [`magnus::Error`]
|
|
129
|
+
pub fn wreq_error_to_magnus(err: wreq::Error) -> MagnusError {
|
|
130
|
+
let error_msg = err.to_string();
|
|
131
|
+
map_wreq_error!(
|
|
132
|
+
ruby!(),
|
|
133
|
+
err,
|
|
134
|
+
error_msg,
|
|
135
|
+
is_builder => BUILDER_ERROR,
|
|
136
|
+
is_body => BODY_ERROR,
|
|
137
|
+
is_tls => TLS_ERROR,
|
|
138
|
+
is_connection_reset => CONNECTION_RESET_ERROR,
|
|
139
|
+
is_connect => CONNECTION_ERROR,
|
|
140
|
+
is_proxy_connect => PROXY_CONNECTION_ERROR,
|
|
141
|
+
is_decode => DECODING_ERROR,
|
|
142
|
+
is_redirect => REDIRECT_ERROR,
|
|
143
|
+
is_timeout => TIMEOUT_ERROR,
|
|
144
|
+
is_status => STATUS_ERROR,
|
|
145
|
+
is_request => REQUEST_ERROR,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pub fn include(ruby: &Ruby) {
|
|
150
|
+
Lazy::force(&MEMORY, ruby);
|
|
151
|
+
Lazy::force(&CONNECTION_ERROR, ruby);
|
|
152
|
+
Lazy::force(&PROXY_CONNECTION_ERROR, ruby);
|
|
153
|
+
Lazy::force(&CONNECTION_RESET_ERROR, ruby);
|
|
154
|
+
Lazy::force(&TLS_ERROR, ruby);
|
|
155
|
+
Lazy::force(&REQUEST_ERROR, ruby);
|
|
156
|
+
Lazy::force(&STATUS_ERROR, ruby);
|
|
157
|
+
Lazy::force(&REDIRECT_ERROR, ruby);
|
|
158
|
+
Lazy::force(&TIMEOUT_ERROR, ruby);
|
|
159
|
+
Lazy::force(&BODY_ERROR, ruby);
|
|
160
|
+
Lazy::force(&DECODING_ERROR, ruby);
|
|
161
|
+
Lazy::force(&BUILDER_ERROR, ruby);
|
|
162
|
+
Lazy::force(&INTERRUPT_ERROR, ruby);
|
|
163
|
+
}
|
data/src/extractor.rs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
use magnus::{RArray, RHash, RString, Ruby, TryConvert, r_hash::ForEach};
|
|
2
|
+
use wreq::{
|
|
3
|
+
Proxy,
|
|
4
|
+
header::{HeaderMap, HeaderName, HeaderValue, OrigHeaderMap},
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
use crate::error::{
|
|
8
|
+
header_name_error_to_magnus, header_value_error_to_magnus, wreq_error_to_magnus,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/// A trait that defines the parameter name for extraction.
|
|
12
|
+
pub trait ExtractorName {
|
|
13
|
+
/// The name of the parameter in the Ruby hash.
|
|
14
|
+
const NAME: &str;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// A generic extractor for various types.
|
|
18
|
+
pub struct Extractor<T>(Option<T>)
|
|
19
|
+
where
|
|
20
|
+
T: ExtractorName;
|
|
21
|
+
|
|
22
|
+
impl<T> Extractor<T>
|
|
23
|
+
where
|
|
24
|
+
T: ExtractorName,
|
|
25
|
+
{
|
|
26
|
+
/// Consumes the extractor and returns the wrapped value.
|
|
27
|
+
///
|
|
28
|
+
/// Returns `Some(T)` if a value was extracted, `None` otherwise.
|
|
29
|
+
#[inline]
|
|
30
|
+
pub fn into_inner(self) -> Option<T> {
|
|
31
|
+
self.0
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ===== impl Extractor<HeaderMap> =====
|
|
36
|
+
|
|
37
|
+
impl ExtractorName for HeaderMap {
|
|
38
|
+
const NAME: &str = "headers";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl TryConvert for Extractor<HeaderMap> {
|
|
42
|
+
fn try_convert(value: magnus::Value) -> Result<Self, magnus::Error> {
|
|
43
|
+
let ruby = Ruby::get_with(value);
|
|
44
|
+
let keyword = RHash::try_convert(value)?;
|
|
45
|
+
let mut headers = HeaderMap::new();
|
|
46
|
+
|
|
47
|
+
if let Some(hash) = keyword
|
|
48
|
+
.get(ruby.to_symbol(HeaderMap::NAME))
|
|
49
|
+
.and_then(RHash::from_value)
|
|
50
|
+
{
|
|
51
|
+
hash.foreach(|name: RString, value: RString| {
|
|
52
|
+
let name = HeaderName::from_bytes(&name.to_bytes())
|
|
53
|
+
.map_err(header_name_error_to_magnus)?;
|
|
54
|
+
let value = HeaderValue::from_maybe_shared(value.to_bytes())
|
|
55
|
+
.map_err(header_value_error_to_magnus)?;
|
|
56
|
+
headers.insert(name, value);
|
|
57
|
+
|
|
58
|
+
Ok(ForEach::Continue)
|
|
59
|
+
})?;
|
|
60
|
+
|
|
61
|
+
return Ok(Extractor(Some(headers)));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Ok(Extractor(None))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ===== impl Extractor<OrigHeaderMap> =====
|
|
69
|
+
|
|
70
|
+
impl ExtractorName for OrigHeaderMap {
|
|
71
|
+
const NAME: &str = "orig_headers";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
impl TryConvert for Extractor<OrigHeaderMap> {
|
|
75
|
+
fn try_convert(value: magnus::Value) -> Result<Self, magnus::Error> {
|
|
76
|
+
let ruby = Ruby::get_with(value);
|
|
77
|
+
let keyword = RHash::try_convert(value)?;
|
|
78
|
+
|
|
79
|
+
if let Some(orig_headers) = keyword
|
|
80
|
+
.get(ruby.to_symbol(OrigHeaderMap::NAME))
|
|
81
|
+
.and_then(RArray::from_value)
|
|
82
|
+
{
|
|
83
|
+
let mut map = OrigHeaderMap::new();
|
|
84
|
+
for value in orig_headers.into_iter().flat_map(RString::from_value) {
|
|
85
|
+
map.insert(value.to_bytes());
|
|
86
|
+
}
|
|
87
|
+
return Ok(Extractor(Some(map)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Ok(Extractor(None))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ===== impl Extractor<Proxy> =====
|
|
95
|
+
|
|
96
|
+
impl ExtractorName for Proxy {
|
|
97
|
+
const NAME: &str = "proxy";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl TryConvert for Extractor<Proxy> {
|
|
101
|
+
fn try_convert(value: magnus::Value) -> Result<Self, magnus::Error> {
|
|
102
|
+
let ruby = Ruby::get_with(value);
|
|
103
|
+
let rhash = RHash::try_convert(value)?;
|
|
104
|
+
|
|
105
|
+
if let Some(proxy) = rhash
|
|
106
|
+
.get(ruby.to_symbol(Proxy::NAME))
|
|
107
|
+
.and_then(RString::from_value)
|
|
108
|
+
{
|
|
109
|
+
return Proxy::all(proxy.to_bytes().as_ref())
|
|
110
|
+
.map(Some)
|
|
111
|
+
.map(Extractor)
|
|
112
|
+
.map_err(wreq_error_to_magnus);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Ok(Extractor(None))
|
|
116
|
+
}
|
|
117
|
+
}
|
data/src/gvl.rs
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//! Allow usage of unsafe code for FFI with Ruby's GVL functions.
|
|
2
|
+
#![allow(unsafe_code)]
|
|
3
|
+
|
|
4
|
+
use std::{ffi::c_void, mem::MaybeUninit, ptr::null_mut};
|
|
5
|
+
|
|
6
|
+
use rb_sys::rb_thread_call_without_gvl;
|
|
7
|
+
use tokio::sync::watch;
|
|
8
|
+
|
|
9
|
+
/// Container for safely passing closure and result through C callback.
|
|
10
|
+
struct Args<F, R> {
|
|
11
|
+
func: Option<F>,
|
|
12
|
+
result: MaybeUninit<R>,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// Cancellation flag for thread interruption support.
|
|
16
|
+
#[derive(Clone)]
|
|
17
|
+
pub struct CancelFlag {
|
|
18
|
+
rx: watch::Receiver<bool>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
struct CancelSender {
|
|
22
|
+
tx: watch::Sender<bool>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl CancelSender {
|
|
26
|
+
fn new() -> (Self, CancelFlag) {
|
|
27
|
+
let (tx, rx) = watch::channel(false);
|
|
28
|
+
(Self { tx }, CancelFlag { rx })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn cancel(&self) {
|
|
32
|
+
let _ = self.tx.send(true);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl CancelFlag {
|
|
37
|
+
/// Wait until cancellation is signaled (zero-latency, no polling).
|
|
38
|
+
pub async fn cancelled(&self) {
|
|
39
|
+
let mut rx = self.rx.clone();
|
|
40
|
+
if *rx.borrow_and_update() {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
loop {
|
|
44
|
+
if rx.changed().await.is_err() {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if *rx.borrow() {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
struct UnblockData {
|
|
55
|
+
sender: CancelSender,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
unsafe extern "C" fn call_without_gvl<F, R>(arg: *mut c_void) -> *mut c_void
|
|
59
|
+
where
|
|
60
|
+
F: FnOnce() -> R,
|
|
61
|
+
R: Sized,
|
|
62
|
+
{
|
|
63
|
+
let args = unsafe { &mut *(arg as *mut Args<F, R>) };
|
|
64
|
+
|
|
65
|
+
// Take closure from Option to transfer ownership.
|
|
66
|
+
if let Some(func) = args.func.take() {
|
|
67
|
+
args.result.write(func());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
null_mut()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
unsafe extern "C" fn unblock_func(arg: *mut c_void) {
|
|
74
|
+
if !arg.is_null() {
|
|
75
|
+
let data = unsafe { &*(arg as *const UnblockData) };
|
|
76
|
+
data.sender.cancel();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Executes the given closure without holding the Ruby GVL (Global VM Lock).
|
|
81
|
+
///
|
|
82
|
+
/// WARNING: Do NOT nest calls to [`nogvl`] or [`nogvl_cancellable`] inside each other.
|
|
83
|
+
/// Nesting these functions will cause Ruby thread deadlock, because the inner call
|
|
84
|
+
/// will block waiting for the GVL while the outer call has already released it.
|
|
85
|
+
/// This results in all Ruby threads being suspended indefinitely.
|
|
86
|
+
pub fn nogvl<F, R>(func: F) -> R
|
|
87
|
+
where
|
|
88
|
+
F: FnOnce() -> R,
|
|
89
|
+
R: Sized,
|
|
90
|
+
{
|
|
91
|
+
// Create stable wrapper to keep data valid during callback.
|
|
92
|
+
let mut args = Args {
|
|
93
|
+
func: Some(func),
|
|
94
|
+
result: MaybeUninit::uninit(),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
let arg_ptr = &mut args as *mut _ as *mut c_void;
|
|
98
|
+
|
|
99
|
+
unsafe {
|
|
100
|
+
rb_thread_call_without_gvl(Some(call_without_gvl::<F, R>), arg_ptr, None, null_mut());
|
|
101
|
+
args.result.assume_init()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Executes the given closure without GVL, supporting cancellation via thread interrupt.
|
|
106
|
+
///
|
|
107
|
+
/// WARNING: Do NOT nest calls to [`nogvl`] or [`nogvl_cancellable`] inside each other.
|
|
108
|
+
/// Nesting these functions will cause Ruby thread deadlock, because the inner call
|
|
109
|
+
/// will block waiting for the GVL while the outer call has already released it.
|
|
110
|
+
/// This results in all Ruby threads being suspended indefinitely.
|
|
111
|
+
pub fn nogvl_cancellable<F, R>(func: F) -> R
|
|
112
|
+
where
|
|
113
|
+
F: FnOnce(CancelFlag) -> R,
|
|
114
|
+
R: Sized,
|
|
115
|
+
{
|
|
116
|
+
let (sender, flag) = CancelSender::new();
|
|
117
|
+
let unblock_data = UnblockData { sender };
|
|
118
|
+
|
|
119
|
+
struct Wrapper<F, R> {
|
|
120
|
+
func: Option<F>,
|
|
121
|
+
flag: CancelFlag,
|
|
122
|
+
result: MaybeUninit<R>,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let mut wrapper = Wrapper {
|
|
126
|
+
func: Some(func),
|
|
127
|
+
flag,
|
|
128
|
+
result: MaybeUninit::uninit(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
unsafe extern "C" fn call_with_flag<F, R>(arg: *mut c_void) -> *mut c_void
|
|
132
|
+
where
|
|
133
|
+
F: FnOnce(CancelFlag) -> R,
|
|
134
|
+
{
|
|
135
|
+
let wrapper = unsafe { &mut *(arg as *mut Wrapper<F, R>) };
|
|
136
|
+
if let Some(func) = wrapper.func.take() {
|
|
137
|
+
wrapper.result.write(func(wrapper.flag.clone()));
|
|
138
|
+
}
|
|
139
|
+
null_mut()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let wrapper_ptr = &mut wrapper as *mut _ as *mut c_void;
|
|
143
|
+
let unblock_data_ptr = &unblock_data as *const _ as *mut c_void;
|
|
144
|
+
|
|
145
|
+
unsafe {
|
|
146
|
+
rb_thread_call_without_gvl(
|
|
147
|
+
Some(call_with_flag::<F, R>),
|
|
148
|
+
wrapper_ptr,
|
|
149
|
+
Some(unblock_func),
|
|
150
|
+
unblock_data_ptr,
|
|
151
|
+
);
|
|
152
|
+
wrapper.result.assume_init()
|
|
153
|
+
}
|
|
154
|
+
}
|
data/src/header.rs
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
use std::cell::RefCell;
|
|
2
|
+
|
|
3
|
+
use bytes::Bytes;
|
|
4
|
+
use http::{HeaderMap, HeaderName, HeaderValue};
|
|
5
|
+
use magnus::{
|
|
6
|
+
Error, Module, Object, RArray, RHash, RModule, RString, Ruby, TryConvert, Value,
|
|
7
|
+
block::Yield,
|
|
8
|
+
function, method,
|
|
9
|
+
r_hash::ForEach,
|
|
10
|
+
typed_data::{Inspect, Obj},
|
|
11
|
+
};
|
|
12
|
+
use wreq::header::OrigHeaderMap;
|
|
13
|
+
|
|
14
|
+
use crate::error::{
|
|
15
|
+
header_name_error_to_magnus, header_value_error_to_magnus, type_value_error_to_magnus,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/// A wrapper for the User-Agent header value.
|
|
19
|
+
pub struct UserAgent(pub HeaderValue);
|
|
20
|
+
|
|
21
|
+
/// HTTP headers collection with read and write operations.
|
|
22
|
+
///
|
|
23
|
+
/// This class wraps HTTP headers and provides convenient methods for
|
|
24
|
+
/// accessing, modifying, and iterating over header name-value pairs.
|
|
25
|
+
#[derive(Clone, Default)]
|
|
26
|
+
#[magnus::wrap(class = "Wreq::Headers", free_immediately, size)]
|
|
27
|
+
pub struct Headers(pub RefCell<HeaderMap>);
|
|
28
|
+
|
|
29
|
+
/// A map from header names to their original casing as received in an HTTP message.
|
|
30
|
+
pub struct OrigHeaders(pub OrigHeaderMap);
|
|
31
|
+
|
|
32
|
+
struct HeaderIter {
|
|
33
|
+
inner: http::header::IntoIter<HeaderValue>,
|
|
34
|
+
next_name: Option<HeaderName>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ===== impl UserAgent =====
|
|
38
|
+
|
|
39
|
+
impl TryConvert for UserAgent {
|
|
40
|
+
fn try_convert(value: Value) -> Result<Self, Error> {
|
|
41
|
+
let s = RString::try_convert(value)?;
|
|
42
|
+
let header_value =
|
|
43
|
+
HeaderValue::from_maybe_shared(s.to_bytes()).map_err(header_value_error_to_magnus)?;
|
|
44
|
+
Ok(Self(header_value))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ===== impl Headers =====
|
|
49
|
+
|
|
50
|
+
impl Headers {
|
|
51
|
+
/// Create a new empty Headers instance.
|
|
52
|
+
#[inline]
|
|
53
|
+
pub fn new() -> Self {
|
|
54
|
+
Self::from(HeaderMap::new())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Get a header value by name (case-insensitive).
|
|
58
|
+
#[inline]
|
|
59
|
+
pub fn get(&self, name: String) -> Option<Bytes> {
|
|
60
|
+
self.0.borrow().get(&name).cloned().map(Bytes::from_owner)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Get all values for a header name (case-insensitive).
|
|
64
|
+
#[inline]
|
|
65
|
+
pub fn get_all(ruby: &Ruby, rb_self: &Self, name: String) -> RArray {
|
|
66
|
+
ruby.ary_from_iter(
|
|
67
|
+
rb_self
|
|
68
|
+
.0
|
|
69
|
+
.borrow()
|
|
70
|
+
.get_all(&name)
|
|
71
|
+
.iter()
|
|
72
|
+
.cloned()
|
|
73
|
+
.map(Bytes::from_owner),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Set a header, replacing any existing values.
|
|
78
|
+
pub fn set(&self, name: String, value: String) -> Result<(), Error> {
|
|
79
|
+
let header_name = name
|
|
80
|
+
.parse::<HeaderName>()
|
|
81
|
+
.map_err(header_name_error_to_magnus)?;
|
|
82
|
+
let header_value = HeaderValue::from_maybe_shared(Bytes::from(value))
|
|
83
|
+
.map_err(header_value_error_to_magnus)?;
|
|
84
|
+
|
|
85
|
+
self.0.borrow_mut().insert(header_name, header_value);
|
|
86
|
+
Ok(())
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Append a header value without replacing existing values.
|
|
90
|
+
pub fn append(&self, name: String, value: String) -> Result<(), Error> {
|
|
91
|
+
let header_name = name
|
|
92
|
+
.parse::<http::header::HeaderName>()
|
|
93
|
+
.map_err(header_name_error_to_magnus)?;
|
|
94
|
+
let header_value = HeaderValue::from_maybe_shared(Bytes::from(value))
|
|
95
|
+
.map_err(header_value_error_to_magnus)?;
|
|
96
|
+
|
|
97
|
+
self.0.borrow_mut().append(header_name, header_value);
|
|
98
|
+
Ok(())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Remove all values for a header name.
|
|
102
|
+
#[inline]
|
|
103
|
+
pub fn remove(&self, name: String) -> Option<Bytes> {
|
|
104
|
+
self.0.borrow_mut().remove(&name).map(Bytes::from_owner)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Check if a header exists (case-insensitive).
|
|
108
|
+
#[inline]
|
|
109
|
+
pub fn contains(&self, name: String) -> bool {
|
|
110
|
+
self.0.borrow().contains_key(&name)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Get the number of headers.
|
|
114
|
+
#[inline]
|
|
115
|
+
pub fn len(&self) -> usize {
|
|
116
|
+
self.0.borrow().len()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Check if headers are empty.
|
|
120
|
+
#[inline]
|
|
121
|
+
pub fn is_empty(&self) -> bool {
|
|
122
|
+
self.0.borrow().is_empty()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Clear all headers.
|
|
126
|
+
#[inline]
|
|
127
|
+
pub fn clear(&self) {
|
|
128
|
+
self.0.borrow_mut().clear();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Get all header names.
|
|
132
|
+
#[inline]
|
|
133
|
+
pub fn keys(ruby: &Ruby, rb_self: &Self) -> RArray {
|
|
134
|
+
ruby.ary_from_iter(rb_self.0.borrow().keys().cloned().map(Bytes::from_owner))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Get all header values.
|
|
138
|
+
#[inline]
|
|
139
|
+
pub fn values(ruby: &Ruby, rb_self: &Self) -> RArray {
|
|
140
|
+
ruby.ary_from_iter(rb_self.0.borrow().values().cloned().map(Bytes::from_owner))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Iterate over headers with Ruby block support.
|
|
144
|
+
#[inline]
|
|
145
|
+
pub fn each(&self) -> Yield<impl Iterator<Item = (Bytes, Bytes)>> {
|
|
146
|
+
Yield::Iter(HeaderIter {
|
|
147
|
+
inner: self.0.borrow().clone().into_iter(),
|
|
148
|
+
next_name: None,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Convert headers to string representation.
|
|
153
|
+
#[inline]
|
|
154
|
+
pub fn to_s(&self) -> String {
|
|
155
|
+
self.0.borrow().inspect()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
impl From<HeaderMap> for Headers {
|
|
160
|
+
fn from(headers: HeaderMap) -> Self {
|
|
161
|
+
Self(RefCell::new(headers))
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
impl TryConvert for Headers {
|
|
166
|
+
fn try_convert(value: Value) -> Result<Self, Error> {
|
|
167
|
+
if let Some(rhash) = RHash::from_value(value) {
|
|
168
|
+
let mut headers = HeaderMap::new();
|
|
169
|
+
|
|
170
|
+
rhash.foreach(|name: RString, value: RString| {
|
|
171
|
+
let name = HeaderName::from_bytes(&name.to_bytes())
|
|
172
|
+
.map_err(header_name_error_to_magnus)?;
|
|
173
|
+
let value = HeaderValue::from_maybe_shared(value.to_bytes())
|
|
174
|
+
.map_err(header_value_error_to_magnus)?;
|
|
175
|
+
headers.insert(name, value);
|
|
176
|
+
|
|
177
|
+
Ok(ForEach::Continue)
|
|
178
|
+
})?;
|
|
179
|
+
|
|
180
|
+
return Ok(Self::from(headers));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
Obj::<Headers>::try_convert(value)
|
|
184
|
+
.map(|headers| headers.0.clone())
|
|
185
|
+
.map(Self)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ===== impl OrigHeaders =====
|
|
190
|
+
|
|
191
|
+
impl TryConvert for OrigHeaders {
|
|
192
|
+
fn try_convert(value: magnus::Value) -> Result<Self, magnus::Error> {
|
|
193
|
+
let mut map = OrigHeaderMap::new();
|
|
194
|
+
|
|
195
|
+
let rarray = RArray::from_value(value)
|
|
196
|
+
.ok_or_else(|| type_value_error_to_magnus("Expected an array of strings"))?;
|
|
197
|
+
|
|
198
|
+
for value in rarray.into_iter().flat_map(RString::from_value) {
|
|
199
|
+
map.insert(value.to_bytes());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
Ok(Self(map))
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ===== impl HeaderIter =====
|
|
207
|
+
|
|
208
|
+
impl Iterator for HeaderIter {
|
|
209
|
+
type Item = (Bytes, Bytes);
|
|
210
|
+
fn next(&mut self) -> Option<Self::Item> {
|
|
211
|
+
let (name, value) = self.inner.next()?;
|
|
212
|
+
match (&self.next_name, name) {
|
|
213
|
+
(Some(next_name), None) => Some((
|
|
214
|
+
Bytes::from_owner(next_name.clone()),
|
|
215
|
+
Bytes::from_owner(value),
|
|
216
|
+
)),
|
|
217
|
+
(_, Some(name)) => {
|
|
218
|
+
self.next_name = Some(name.clone());
|
|
219
|
+
Some((Bytes::from_owner(name), Bytes::from_owner(value)))
|
|
220
|
+
}
|
|
221
|
+
(None, None) => None,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
|
|
227
|
+
// Define Headers class with methods
|
|
228
|
+
let headers_class = gem_module.define_class("Headers", ruby.class_object())?;
|
|
229
|
+
headers_class.define_singleton_method("new", function!(Headers::new, 0))?;
|
|
230
|
+
headers_class.define_method("get", method!(Headers::get, 1))?;
|
|
231
|
+
headers_class.define_method("get_all", method!(Headers::get_all, 1))?;
|
|
232
|
+
headers_class.define_method("set", method!(Headers::set, 2))?;
|
|
233
|
+
headers_class.define_method("append", method!(Headers::append, 2))?;
|
|
234
|
+
headers_class.define_method("remove", method!(Headers::remove, 1))?;
|
|
235
|
+
headers_class.define_method("contains?", method!(Headers::contains, 1))?;
|
|
236
|
+
headers_class.define_method("key?", method!(Headers::contains, 1))?;
|
|
237
|
+
headers_class.define_method("length", method!(Headers::len, 0))?;
|
|
238
|
+
headers_class.define_method("empty?", method!(Headers::is_empty, 0))?;
|
|
239
|
+
headers_class.define_method("clear", method!(Headers::clear, 0))?;
|
|
240
|
+
headers_class.define_method("keys", method!(Headers::keys, 0))?;
|
|
241
|
+
headers_class.define_method("values", method!(Headers::values, 0))?;
|
|
242
|
+
headers_class.define_method("each", method!(Headers::each, 0))?;
|
|
243
|
+
headers_class.define_method("to_s", method!(Headers::to_s, 0))?;
|
|
244
|
+
Ok(())
|
|
245
|
+
}
|