wreq 1.2.4 → 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 +22 -19
- data/Cargo.toml +15 -2
- data/crates/wreq-util/.github/FUNDING.yml +15 -0
- data/crates/wreq-util/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- data/crates/wreq-util/.github/ISSUE_TEMPLATE/custom.md +10 -0
- data/crates/wreq-util/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/crates/wreq-util/.github/dependabot.yml +22 -0
- data/crates/wreq-util/.github/workflows/ci.yml +82 -0
- data/crates/wreq-util/.github/workflows/release-plz.yml +52 -0
- data/crates/wreq-util/.gitignore +24 -0
- data/crates/wreq-util/CHANGELOG.md +74 -0
- data/crates/wreq-util/Cargo.toml +94 -0
- data/crates/wreq-util/LICENSE +201 -0
- data/crates/wreq-util/README.md +62 -0
- data/crates/wreq-util/examples/emulate.rs +39 -0
- data/crates/wreq-util/examples/tower_delay.rs +191 -0
- data/crates/wreq-util/release-plz.toml +2 -0
- data/crates/wreq-util/rustfmt.toml +5 -0
- data/crates/wreq-util/src/emulate/compress.rs +85 -0
- data/crates/wreq-util/src/emulate/macros.rs +372 -0
- data/crates/wreq-util/src/emulate/profile/chrome/header.rs +74 -0
- data/crates/wreq-util/src/emulate/profile/chrome/http2.rs +65 -0
- data/crates/wreq-util/src/emulate/profile/chrome/tls.rs +153 -0
- data/crates/wreq-util/src/emulate/profile/chrome.rs +2028 -0
- data/crates/wreq-util/src/emulate/profile/firefox/header.rs +37 -0
- data/crates/wreq-util/src/emulate/profile/firefox/http2.rs +113 -0
- data/crates/wreq-util/src/emulate/profile/firefox/tls.rs +253 -0
- data/crates/wreq-util/src/emulate/profile/firefox.rs +518 -0
- data/crates/wreq-util/src/emulate/profile/okhttp.rs +241 -0
- data/crates/wreq-util/src/emulate/profile/opera/header.rs +29 -0
- data/crates/wreq-util/src/emulate/profile/opera/http2.rs +50 -0
- data/crates/wreq-util/src/emulate/profile/opera/tls.rs +97 -0
- data/crates/wreq-util/src/emulate/profile/opera.rs +299 -0
- data/crates/wreq-util/src/emulate/profile/safari/header.rs +59 -0
- data/crates/wreq-util/src/emulate/profile/safari/http2.rs +116 -0
- data/crates/wreq-util/src/emulate/profile/safari/tls.rs +177 -0
- data/crates/wreq-util/src/emulate/profile/safari.rs +222 -0
- data/crates/wreq-util/src/emulate/profile.rs +47 -0
- data/crates/wreq-util/src/emulate.rs +369 -0
- data/crates/wreq-util/src/lib.rs +14 -0
- data/crates/wreq-util/src/rand.rs +24 -0
- data/crates/wreq-util/src/tower/delay/future.rs +43 -0
- data/crates/wreq-util/src/tower/delay/layer.rs +201 -0
- data/crates/wreq-util/src/tower/delay/service.rs +190 -0
- data/crates/wreq-util/src/tower/delay.rs +98 -0
- data/crates/wreq-util/src/tower.rs +7 -0
- data/crates/wreq-util/tests/client.rs +153 -0
- data/crates/wreq-util/tests/emulate_chrome.rs +270 -0
- data/crates/wreq-util/tests/emulate_firefox.rs +92 -0
- data/crates/wreq-util/tests/emulate_okhttp.rs +46 -0
- data/crates/wreq-util/tests/emulate_opera.rs +113 -0
- data/crates/wreq-util/tests/emulate_safari.rs +151 -0
- data/crates/wreq-util/tests/support/mod.rs +55 -0
- data/crates/wreq-util/tests/support/server.rs +232 -0
- data/examples/emulate_request.rb +1 -1
- data/lib/wreq.rb +72 -45
- data/lib/wreq_ruby/body.rb +30 -9
- data/lib/wreq_ruby/client.rb +88 -55
- data/lib/wreq_ruby/cookie.rb +81 -21
- data/lib/wreq_ruby/emulate.rb +77 -7
- data/lib/wreq_ruby/error.rb +5 -7
- data/lib/wreq_ruby/header.rb +205 -114
- data/lib/wreq_ruby/http.rb +74 -0
- data/lib/wreq_ruby/response.rb +31 -3
- 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 +86 -50
- data/src/error.rs +198 -45
- data/src/extractor.rs +16 -76
- data/src/header.rs +318 -130
- 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/header_test.rb +228 -192
- data/test/json_precision_test.rb +209 -0
- data/test/option_validation_test.rb +311 -0
- data/test/stream_test.rb +36 -0
- data/test/value_semantics_test.rb +238 -0
- metadata +77 -1
data/src/header.rs
CHANGED
|
@@ -1,161 +1,254 @@
|
|
|
1
|
+
//! Native support for `Wreq::Headers`.
|
|
2
|
+
//!
|
|
3
|
+
//! Header names are normalized by [`HeaderMap`] and compared without regard to
|
|
4
|
+
//! case, as required by [RFC 9110 section 5.1]. Ruby collection adapters add
|
|
5
|
+
//! construction, indexing, and enumeration without changing the underlying
|
|
6
|
+
//! header representation. Exact wire casing and order remain the responsibility
|
|
7
|
+
//! of the `orig_headers` request option.
|
|
8
|
+
//!
|
|
9
|
+
//! [RFC 9110 section 5.1]: https://www.rfc-editor.org/rfc/rfc9110.html#section-5.1
|
|
10
|
+
|
|
1
11
|
use std::cell::RefCell;
|
|
2
12
|
|
|
3
13
|
use bytes::Bytes;
|
|
4
|
-
use http::{HeaderMap,
|
|
14
|
+
use http::{HeaderMap, HeaderValue};
|
|
5
15
|
use magnus::{
|
|
6
|
-
Error,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
r_hash::ForEach,
|
|
16
|
+
Error, RArray, RModule, RString, Ruby, TryConvert, Value, function, method,
|
|
17
|
+
prelude::*,
|
|
18
|
+
scan_args::scan_args,
|
|
10
19
|
typed_data::{Inspect, Obj},
|
|
11
20
|
};
|
|
12
21
|
use wreq::header::OrigHeaderMap;
|
|
13
22
|
|
|
14
|
-
use crate::error::{
|
|
15
|
-
|
|
23
|
+
use crate::error::{header_type_error, header_value_error};
|
|
24
|
+
|
|
25
|
+
use self::helper::{
|
|
26
|
+
ensure_header_count, from_source, header_count_error, parse_header_name, parse_header_values,
|
|
16
27
|
};
|
|
17
28
|
|
|
18
|
-
/// A
|
|
29
|
+
/// A validated User-Agent header value accepted from Ruby.
|
|
19
30
|
pub struct UserAgent(pub HeaderValue);
|
|
20
31
|
|
|
21
|
-
/// HTTP headers
|
|
32
|
+
/// Mutable HTTP headers exposed as `Wreq::Headers`.
|
|
22
33
|
///
|
|
23
|
-
///
|
|
24
|
-
///
|
|
34
|
+
/// Names are stored in their normalized form and lookups are case-insensitive.
|
|
35
|
+
/// A name can have multiple values, each counted as a separate occurrence.
|
|
25
36
|
#[derive(Clone, Default)]
|
|
26
37
|
#[magnus::wrap(class = "Wreq::Headers", free_immediately, size)]
|
|
27
38
|
pub struct Headers(pub RefCell<HeaderMap>);
|
|
28
39
|
|
|
29
|
-
///
|
|
40
|
+
/// Header casing and order supplied through the `orig_headers` request option.
|
|
30
41
|
pub struct OrigHeaders(pub OrigHeaderMap);
|
|
31
42
|
|
|
32
|
-
struct HeaderIter {
|
|
33
|
-
inner: http::header::IntoIter<HeaderValue>,
|
|
34
|
-
next_name: Option<HeaderName>,
|
|
35
|
-
}
|
|
36
|
-
|
|
37
43
|
// ===== impl UserAgent =====
|
|
38
44
|
|
|
39
45
|
impl TryConvert for UserAgent {
|
|
40
46
|
fn try_convert(value: Value) -> Result<Self, Error> {
|
|
47
|
+
let ruby = Ruby::get_with(value);
|
|
41
48
|
let s = RString::try_convert(value)?;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
HeaderValue::from_maybe_shared(s.to_bytes())
|
|
50
|
+
.map(Self)
|
|
51
|
+
.map_err(|err| header_value_error(&ruby, err))
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
// ===== impl Headers =====
|
|
49
56
|
|
|
50
57
|
impl Headers {
|
|
51
|
-
///
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
/// Return the first value for a String or Symbol header name.
|
|
59
|
+
///
|
|
60
|
+
/// Returns `nil` when the normalized name is not present.
|
|
61
|
+
pub fn get(&self, name: Value) -> Result<Option<Bytes>, Error> {
|
|
62
|
+
let name = parse_header_name(name)?;
|
|
63
|
+
Ok(self.0.borrow().get(name).cloned().map(Bytes::from_owner))
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
///
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
/// Return every value for a String or Symbol header name.
|
|
67
|
+
///
|
|
68
|
+
/// Values retain their append order. A missing name returns an empty Array.
|
|
69
|
+
pub fn get_all(ruby: &Ruby, rb_self: &Self, name: Value) -> Result<RArray, Error> {
|
|
70
|
+
let name = parse_header_name(name)?;
|
|
71
|
+
let headers = rb_self.0.borrow();
|
|
72
|
+
let values = headers.get_all(name).iter().cloned().map(Bytes::from_owner);
|
|
73
|
+
Ok(ruby.ary_from_iter(values))
|
|
61
74
|
}
|
|
62
75
|
|
|
63
|
-
///
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.map_err(
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
/// Replace every value for a header name.
|
|
77
|
+
///
|
|
78
|
+
/// A String stores one occurrence, while an Array stores each String as a
|
|
79
|
+
/// separate occurrence. An empty Array removes the header.
|
|
80
|
+
pub fn set(&self, name: Value, value: Value) -> Result<(), Error> {
|
|
81
|
+
let ruby = Ruby::get_with(name);
|
|
82
|
+
let name = parse_header_name(name)?;
|
|
83
|
+
let values = parse_header_values(value)?;
|
|
84
|
+
let mut headers = self.0.borrow_mut();
|
|
85
|
+
let replaced = headers.get_all(&name).iter().count();
|
|
86
|
+
ensure_header_count(&ruby, headers.len(), replaced, values.len())?;
|
|
87
|
+
|
|
88
|
+
let mut values = values.into_iter();
|
|
89
|
+
let Some(first) = values.next() else {
|
|
90
|
+
headers.remove(name);
|
|
91
|
+
return Ok(());
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
headers
|
|
95
|
+
.try_insert(name.clone(), first)
|
|
96
|
+
.map_err(|_| header_count_error(&ruby))?;
|
|
97
|
+
for value in values {
|
|
98
|
+
headers
|
|
99
|
+
.try_append(name.clone(), value)
|
|
100
|
+
.map_err(|_| header_count_error(&ruby))?;
|
|
101
|
+
}
|
|
86
102
|
Ok(())
|
|
87
103
|
}
|
|
88
104
|
|
|
89
|
-
/// Append
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
let
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
/// Append one or more values without replacing existing occurrences.
|
|
106
|
+
///
|
|
107
|
+
/// Array elements are appended separately and are never comma-folded.
|
|
108
|
+
pub fn append(&self, name: Value, value: Value) -> Result<(), Error> {
|
|
109
|
+
let ruby = Ruby::get_with(name);
|
|
110
|
+
let name = parse_header_name(name)?;
|
|
111
|
+
let values = parse_header_values(value)?;
|
|
112
|
+
let mut headers = self.0.borrow_mut();
|
|
113
|
+
ensure_header_count(&ruby, headers.len(), 0, values.len())?;
|
|
114
|
+
|
|
115
|
+
for value in values {
|
|
116
|
+
headers
|
|
117
|
+
.try_append(name.clone(), value)
|
|
118
|
+
.map_err(|_| header_count_error(&ruby))?;
|
|
119
|
+
}
|
|
98
120
|
Ok(())
|
|
99
121
|
}
|
|
100
122
|
|
|
101
|
-
/// Remove
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
123
|
+
/// Remove every value for a header name and return its first value.
|
|
124
|
+
///
|
|
125
|
+
/// Returns `nil` when the normalized name is not present.
|
|
126
|
+
pub fn remove(&self, name: Value) -> Result<Option<Bytes>, Error> {
|
|
127
|
+
let name = parse_header_name(name)?;
|
|
128
|
+
Ok(self.0.borrow_mut().remove(name).map(Bytes::from_owner))
|
|
105
129
|
}
|
|
106
130
|
|
|
107
|
-
///
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
self.0.borrow().contains_key(
|
|
131
|
+
/// Return whether a String or Symbol header name is present.
|
|
132
|
+
pub fn contains(&self, name: Value) -> Result<bool, Error> {
|
|
133
|
+
let name = parse_header_name(name)?;
|
|
134
|
+
Ok(self.0.borrow().contains_key(name))
|
|
111
135
|
}
|
|
112
136
|
|
|
113
|
-
///
|
|
137
|
+
/// Return the total number of header occurrences.
|
|
138
|
+
///
|
|
139
|
+
/// This can be greater than `keys.length` when names have multiple values.
|
|
114
140
|
#[inline]
|
|
115
141
|
pub fn len(&self) -> usize {
|
|
116
142
|
self.0.borrow().len()
|
|
117
143
|
}
|
|
118
144
|
|
|
119
|
-
///
|
|
145
|
+
/// Return whether the collection contains no header occurrences.
|
|
120
146
|
#[inline]
|
|
121
147
|
pub fn is_empty(&self) -> bool {
|
|
122
148
|
self.0.borrow().is_empty()
|
|
123
149
|
}
|
|
124
150
|
|
|
125
|
-
///
|
|
126
|
-
#[inline]
|
|
127
|
-
pub fn clear(&self) {
|
|
128
|
-
self.0.borrow_mut().clear();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/// Get all header names.
|
|
132
|
-
#[inline]
|
|
151
|
+
/// Return each unique normalized header name.
|
|
133
152
|
pub fn keys(ruby: &Ruby, rb_self: &Self) -> RArray {
|
|
134
153
|
ruby.ary_from_iter(rb_self.0.borrow().keys().cloned().map(Bytes::from_owner))
|
|
135
154
|
}
|
|
136
155
|
|
|
137
|
-
///
|
|
156
|
+
/// Return all values, including duplicate-name occurrences.
|
|
138
157
|
#[inline]
|
|
139
158
|
pub fn values(ruby: &Ruby, rb_self: &Self) -> RArray {
|
|
140
159
|
ruby.ary_from_iter(rb_self.0.borrow().values().cloned().map(Bytes::from_owner))
|
|
141
160
|
}
|
|
142
161
|
|
|
143
|
-
///
|
|
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.
|
|
162
|
+
/// Return the debug representation of the underlying header map.
|
|
153
163
|
#[inline]
|
|
154
164
|
pub fn to_s(&self) -> String {
|
|
155
165
|
self.0.borrow().inspect()
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
168
|
|
|
169
|
+
// Ruby collection adapters are kept separate from the core HeaderMap operations.
|
|
170
|
+
impl Headers {
|
|
171
|
+
/// Create an empty collection or populate it from a Ruby source.
|
|
172
|
+
///
|
|
173
|
+
/// The optional source may be a Hash, another `Wreq::Headers`, or an
|
|
174
|
+
/// Enumerable whose elements are name-value pairs.
|
|
175
|
+
pub fn new(args: &[Value]) -> Result<Self, Error> {
|
|
176
|
+
scan_args::<(), (Option<Value>,), (), (), (), ()>(args)?
|
|
177
|
+
.optional
|
|
178
|
+
.0
|
|
179
|
+
.map(from_source)
|
|
180
|
+
.transpose()
|
|
181
|
+
.map(Option::unwrap_or_default)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Return a value using Ruby collection semantics.
|
|
185
|
+
///
|
|
186
|
+
/// A missing name returns `nil`, one occurrence returns a String, and
|
|
187
|
+
/// multiple occurrences return an Array of Strings.
|
|
188
|
+
pub fn index(ruby: &Ruby, rb_self: &Self, name: Value) -> Result<Value, Error> {
|
|
189
|
+
let name = parse_header_name(name)?;
|
|
190
|
+
let headers = rb_self.0.borrow();
|
|
191
|
+
let all_values = headers.get_all(name);
|
|
192
|
+
let mut values = all_values.iter();
|
|
193
|
+
|
|
194
|
+
let Some(first) = values.next() else {
|
|
195
|
+
return Ok(ruby.qnil().as_value());
|
|
196
|
+
};
|
|
197
|
+
let Some(second) = values.next() else {
|
|
198
|
+
return Ok(ruby.into_value(Bytes::from_owner(first.clone())));
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
let values = std::iter::once(first)
|
|
202
|
+
.chain(std::iter::once(second))
|
|
203
|
+
.chain(values)
|
|
204
|
+
.cloned()
|
|
205
|
+
.map(Bytes::from_owner);
|
|
206
|
+
Ok(ruby.into_value(ruby.ary_from_iter(values)))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Replace a header and return the assigned Ruby value for `headers[name] = value`.
|
|
210
|
+
pub fn set_index(&self, name: Value, value: Value) -> Result<Value, Error> {
|
|
211
|
+
self.set(name, value)?;
|
|
212
|
+
Ok(value)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// Remove every occurrence and return the same `Wreq::Headers` object.
|
|
216
|
+
pub fn clear(rb_self: Value) -> Result<Value, Error> {
|
|
217
|
+
let headers = Obj::<Headers>::try_convert(rb_self)?;
|
|
218
|
+
headers.0.borrow_mut().clear();
|
|
219
|
+
Ok(rb_self)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Yield every normalized name-value occurrence.
|
|
223
|
+
///
|
|
224
|
+
/// Returns an Enumerator without a block and returns the collection after
|
|
225
|
+
/// yielding when a block is provided.
|
|
226
|
+
pub fn each(ruby: &Ruby, rb_self: Value) -> Result<Value, Error> {
|
|
227
|
+
if !ruby.block_given() {
|
|
228
|
+
return Ok(ruby.into_value(rb_self.enumeratorize("each", ())));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let headers = Obj::<Headers>::try_convert(rb_self)?;
|
|
232
|
+
// Release the RefCell borrow before yielding because Ruby code may
|
|
233
|
+
// mutate this collection from inside the block.
|
|
234
|
+
let entries: Vec<_> = headers
|
|
235
|
+
.0
|
|
236
|
+
.borrow()
|
|
237
|
+
.iter()
|
|
238
|
+
.map(|(name, value)| {
|
|
239
|
+
(
|
|
240
|
+
Bytes::from_owner(name.clone()),
|
|
241
|
+
Bytes::from_owner(value.clone()),
|
|
242
|
+
)
|
|
243
|
+
})
|
|
244
|
+
.collect();
|
|
245
|
+
for (name, value) in entries {
|
|
246
|
+
let _: Value = ruby.yield_values((name, value))?;
|
|
247
|
+
}
|
|
248
|
+
Ok(rb_self)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
159
252
|
impl From<HeaderMap> for Headers {
|
|
160
253
|
fn from(headers: HeaderMap) -> Self {
|
|
161
254
|
Self(RefCell::new(headers))
|
|
@@ -164,38 +257,23 @@ impl From<HeaderMap> for Headers {
|
|
|
164
257
|
|
|
165
258
|
impl TryConvert for Headers {
|
|
166
259
|
fn try_convert(value: Value) -> Result<Self, Error> {
|
|
167
|
-
|
|
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)
|
|
260
|
+
from_source(value)
|
|
186
261
|
}
|
|
187
262
|
}
|
|
188
263
|
|
|
189
264
|
// ===== impl OrigHeaders =====
|
|
190
265
|
|
|
191
266
|
impl TryConvert for OrigHeaders {
|
|
192
|
-
fn try_convert(value:
|
|
267
|
+
fn try_convert(value: Value) -> Result<Self, Error> {
|
|
268
|
+
let ruby = Ruby::get_with(value);
|
|
193
269
|
let mut map = OrigHeaderMap::new();
|
|
194
270
|
|
|
195
271
|
let rarray = RArray::from_value(value)
|
|
196
|
-
.ok_or_else(||
|
|
272
|
+
.ok_or_else(|| header_type_error(&ruby, "Expected an array of strings"))?;
|
|
197
273
|
|
|
198
|
-
for value in rarray
|
|
274
|
+
for value in rarray {
|
|
275
|
+
let value = RString::try_convert(value)
|
|
276
|
+
.map_err(|_| header_type_error(&ruby, "Expected an array of strings"))?;
|
|
199
277
|
map.insert(value.to_bytes());
|
|
200
278
|
}
|
|
201
279
|
|
|
@@ -203,30 +281,134 @@ impl TryConvert for OrigHeaders {
|
|
|
203
281
|
}
|
|
204
282
|
}
|
|
205
283
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
284
|
+
mod helper {
|
|
285
|
+
//! Ruby value conversion helpers for `Wreq::Headers`.
|
|
286
|
+
|
|
287
|
+
use bytes::Bytes;
|
|
288
|
+
use http::{HeaderName, HeaderValue};
|
|
289
|
+
use magnus::{
|
|
290
|
+
Error, RArray, RString, Ruby, Symbol, TryConvert, Value, prelude::*, typed_data::Obj,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
use crate::error::{header_name_error, header_type_error, header_value_error};
|
|
294
|
+
|
|
295
|
+
use super::Headers;
|
|
296
|
+
|
|
297
|
+
/// Maximum number of field-value occurrences supported by `HeaderMap`.
|
|
298
|
+
const MAX_HEADER_ENTRIES: usize = 1 << 15;
|
|
299
|
+
|
|
300
|
+
/// Build a header collection from a Ruby source object.
|
|
301
|
+
///
|
|
302
|
+
/// Accepts another `Wreq::Headers`, a Hash, or any object whose `to_a` result
|
|
303
|
+
/// contains two-element name-value pairs. Array values are delegated to
|
|
304
|
+
/// [`Headers::append`] so each value remains a separate occurrence.
|
|
305
|
+
pub(super) fn from_source(source: Value) -> Result<Headers, Error> {
|
|
306
|
+
let ruby = Ruby::get_with(source);
|
|
307
|
+
if let Ok(headers) = Obj::<Headers>::try_convert(source) {
|
|
308
|
+
return Ok((*headers).clone());
|
|
309
|
+
}
|
|
310
|
+
if !source.respond_to("to_a", false)? {
|
|
311
|
+
return Err(header_type_error(
|
|
312
|
+
&ruby,
|
|
313
|
+
"Expected Headers, a Hash, or an enumerable of pairs",
|
|
314
|
+
));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let pairs: RArray = source.funcall_public("to_a", ())?;
|
|
318
|
+
let headers = Headers::default();
|
|
319
|
+
for pair in pairs {
|
|
320
|
+
let pair = RArray::try_convert(pair)
|
|
321
|
+
.map_err(|_| header_type_error(&ruby, "Expected each header entry to be a pair"))?;
|
|
322
|
+
if pair.len() != 2 {
|
|
323
|
+
return Err(header_type_error(
|
|
324
|
+
&ruby,
|
|
325
|
+
"Expected each header entry to contain a name and value",
|
|
326
|
+
));
|
|
220
327
|
}
|
|
221
|
-
|
|
328
|
+
|
|
329
|
+
headers.append(pair.entry(0)?, pair.entry(1)?)?;
|
|
222
330
|
}
|
|
331
|
+
Ok(headers)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// Convert a Ruby String or Symbol into a normalized HTTP header name.
|
|
335
|
+
///
|
|
336
|
+
/// Symbol underscores are changed to hyphens before [`HeaderName`] validates
|
|
337
|
+
/// and normalizes the name. Other Ruby types produce `Wreq::BuilderError`.
|
|
338
|
+
pub(super) fn parse_header_name(value: Value) -> Result<HeaderName, Error> {
|
|
339
|
+
let ruby = Ruby::get_with(value);
|
|
340
|
+
let name = match (RString::from_value(value), Symbol::from_value(value)) {
|
|
341
|
+
(Some(name), _) => name.to_bytes(),
|
|
342
|
+
(None, Some(name)) => Bytes::from(name.name()?.replace('_', "-")),
|
|
343
|
+
(None, None) => {
|
|
344
|
+
return Err(header_type_error(
|
|
345
|
+
&ruby,
|
|
346
|
+
"Expected a String or Symbol header name",
|
|
347
|
+
));
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
HeaderName::from_bytes(name.as_ref()).map_err(|err| header_name_error(&ruby, err))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// Convert a Ruby String or Array of Strings into validated header values.
|
|
354
|
+
///
|
|
355
|
+
/// Each Array element becomes one [`HeaderValue`]. An empty Array therefore
|
|
356
|
+
/// produces no values, allowing `set` to remove a header and `append` to do
|
|
357
|
+
/// nothing.
|
|
358
|
+
pub(super) fn parse_header_values(value: Value) -> Result<Vec<HeaderValue>, Error> {
|
|
359
|
+
if let Some(values) = RArray::from_value(value) {
|
|
360
|
+
values.into_iter().map(parse_header_value).collect()
|
|
361
|
+
} else {
|
|
362
|
+
Ok(vec![parse_header_value(value)?])
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// Convert one Ruby String into a validated HTTP header value.
|
|
367
|
+
///
|
|
368
|
+
/// Invalid Ruby types and bytes rejected by [`HeaderValue`] are mapped to
|
|
369
|
+
/// `Wreq::BuilderError`.
|
|
370
|
+
fn parse_header_value(value: Value) -> Result<HeaderValue, Error> {
|
|
371
|
+
let ruby = Ruby::get_with(value);
|
|
372
|
+
let value = RString::try_convert(value)
|
|
373
|
+
.map_err(|_| header_type_error(&ruby, "Expected a String header value"))?;
|
|
374
|
+
HeaderValue::from_maybe_shared(value.to_bytes())
|
|
375
|
+
.map_err(|err| header_value_error(&ruby, err))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/// Validate the resulting number of header occurrences before a mutation.
|
|
379
|
+
///
|
|
380
|
+
/// `current` is the collection length, `replaced` is the number of existing
|
|
381
|
+
/// occurrences removed by `set`, and `added` is the incoming value count.
|
|
382
|
+
/// Checked arithmetic prevents overflow; an invalid calculation or a result
|
|
383
|
+
/// above the native [`HeaderMap`](http::HeaderMap) limit returns
|
|
384
|
+
/// `Wreq::BuilderError` without mutating the collection.
|
|
385
|
+
pub(super) fn ensure_header_count(
|
|
386
|
+
ruby: &Ruby,
|
|
387
|
+
current: usize,
|
|
388
|
+
replaced: usize,
|
|
389
|
+
added: usize,
|
|
390
|
+
) -> Result<(), Error> {
|
|
391
|
+
let count = current
|
|
392
|
+
.checked_sub(replaced)
|
|
393
|
+
.and_then(|count| count.checked_add(added));
|
|
394
|
+
if count.is_some_and(|count| count <= MAX_HEADER_ENTRIES) {
|
|
395
|
+
Ok(())
|
|
396
|
+
} else {
|
|
397
|
+
Err(header_count_error(ruby))
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/// Build the error returned when the native header map reaches its entry limit.
|
|
402
|
+
pub(super) fn header_count_error(ruby: &Ruby) -> Error {
|
|
403
|
+
header_type_error(ruby, "Header collection exceeds 32,768 entries")
|
|
223
404
|
}
|
|
224
405
|
}
|
|
225
406
|
|
|
407
|
+
/// Register `Wreq::Headers` and its native methods with Ruby.
|
|
226
408
|
pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
|
|
227
|
-
// Define Headers class with methods
|
|
228
409
|
let headers_class = gem_module.define_class("Headers", ruby.class_object())?;
|
|
229
|
-
|
|
410
|
+
|
|
411
|
+
// Core bindings expose direct HeaderMap operations.
|
|
230
412
|
headers_class.define_method("get", method!(Headers::get, 1))?;
|
|
231
413
|
headers_class.define_method("get_all", method!(Headers::get_all, 1))?;
|
|
232
414
|
headers_class.define_method("set", method!(Headers::set, 2))?;
|
|
@@ -236,10 +418,16 @@ pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
|
|
|
236
418
|
headers_class.define_method("key?", method!(Headers::contains, 1))?;
|
|
237
419
|
headers_class.define_method("length", method!(Headers::len, 0))?;
|
|
238
420
|
headers_class.define_method("empty?", method!(Headers::is_empty, 0))?;
|
|
239
|
-
headers_class.define_method("clear", method!(Headers::clear, 0))?;
|
|
240
421
|
headers_class.define_method("keys", method!(Headers::keys, 0))?;
|
|
241
422
|
headers_class.define_method("values", method!(Headers::values, 0))?;
|
|
242
|
-
headers_class.define_method("each", method!(Headers::each, 0))?;
|
|
243
423
|
headers_class.define_method("to_s", method!(Headers::to_s, 0))?;
|
|
424
|
+
|
|
425
|
+
// Ruby collection bindings cover construction, indexing, and block semantics.
|
|
426
|
+
headers_class.include_module(ruby.module_enumerable())?;
|
|
427
|
+
headers_class.define_singleton_method("new", function!(Headers::new, -1))?;
|
|
428
|
+
headers_class.define_method("[]", method!(Headers::index, 1))?;
|
|
429
|
+
headers_class.define_method("[]=", method!(Headers::set_index, 2))?;
|
|
430
|
+
headers_class.define_method("clear", method!(Headers::clear, 0))?;
|
|
431
|
+
headers_class.define_method("each", method!(Headers::each, 0))?;
|
|
244
432
|
Ok(())
|
|
245
433
|
}
|