wreq 1.2.4 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +3 -7
  3. data/Cargo.toml +6 -1
  4. data/crates/wreq-util/.github/FUNDING.yml +15 -0
  5. data/crates/wreq-util/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  6. data/crates/wreq-util/.github/ISSUE_TEMPLATE/custom.md +10 -0
  7. data/crates/wreq-util/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  8. data/crates/wreq-util/.github/dependabot.yml +22 -0
  9. data/crates/wreq-util/.github/workflows/ci.yml +82 -0
  10. data/crates/wreq-util/.github/workflows/release-plz.yml +52 -0
  11. data/crates/wreq-util/.gitignore +24 -0
  12. data/crates/wreq-util/CHANGELOG.md +74 -0
  13. data/crates/wreq-util/Cargo.toml +94 -0
  14. data/crates/wreq-util/LICENSE +201 -0
  15. data/crates/wreq-util/README.md +62 -0
  16. data/crates/wreq-util/examples/emulate.rs +39 -0
  17. data/crates/wreq-util/examples/tower_delay.rs +191 -0
  18. data/crates/wreq-util/release-plz.toml +2 -0
  19. data/crates/wreq-util/rustfmt.toml +5 -0
  20. data/crates/wreq-util/src/emulate/compress.rs +85 -0
  21. data/crates/wreq-util/src/emulate/macros.rs +370 -0
  22. data/crates/wreq-util/src/emulate/profile/chrome/header.rs +59 -0
  23. data/crates/wreq-util/src/emulate/profile/chrome/http2.rs +65 -0
  24. data/crates/wreq-util/src/emulate/profile/chrome/tls.rs +153 -0
  25. data/crates/wreq-util/src/emulate/profile/chrome.rs +2028 -0
  26. data/crates/wreq-util/src/emulate/profile/firefox/header.rs +21 -0
  27. data/crates/wreq-util/src/emulate/profile/firefox/http2.rs +113 -0
  28. data/crates/wreq-util/src/emulate/profile/firefox/tls.rs +253 -0
  29. data/crates/wreq-util/src/emulate/profile/firefox.rs +518 -0
  30. data/crates/wreq-util/src/emulate/profile/okhttp.rs +241 -0
  31. data/crates/wreq-util/src/emulate/profile/opera/header.rs +30 -0
  32. data/crates/wreq-util/src/emulate/profile/opera/http2.rs +50 -0
  33. data/crates/wreq-util/src/emulate/profile/opera/tls.rs +97 -0
  34. data/crates/wreq-util/src/emulate/profile/opera.rs +299 -0
  35. data/crates/wreq-util/src/emulate/profile/safari/header.rs +59 -0
  36. data/crates/wreq-util/src/emulate/profile/safari/http2.rs +116 -0
  37. data/crates/wreq-util/src/emulate/profile/safari/tls.rs +177 -0
  38. data/crates/wreq-util/src/emulate/profile/safari.rs +222 -0
  39. data/crates/wreq-util/src/emulate/profile.rs +47 -0
  40. data/crates/wreq-util/src/emulate.rs +369 -0
  41. data/crates/wreq-util/src/lib.rs +14 -0
  42. data/crates/wreq-util/src/rand.rs +24 -0
  43. data/crates/wreq-util/src/tower/delay/future.rs +43 -0
  44. data/crates/wreq-util/src/tower/delay/layer.rs +201 -0
  45. data/crates/wreq-util/src/tower/delay/service.rs +190 -0
  46. data/crates/wreq-util/src/tower/delay.rs +98 -0
  47. data/crates/wreq-util/src/tower.rs +7 -0
  48. data/crates/wreq-util/tests/client.rs +51 -0
  49. data/crates/wreq-util/tests/emulate_chrome.rs +270 -0
  50. data/crates/wreq-util/tests/emulate_firefox.rs +92 -0
  51. data/crates/wreq-util/tests/emulate_okhttp.rs +46 -0
  52. data/crates/wreq-util/tests/emulate_opera.rs +113 -0
  53. data/crates/wreq-util/tests/emulate_safari.rs +151 -0
  54. data/crates/wreq-util/tests/support/mod.rs +55 -0
  55. data/crates/wreq-util/tests/support/server.rs +232 -0
  56. data/examples/emulate_request.rb +1 -1
  57. data/lib/wreq.rb +20 -18
  58. data/lib/wreq_ruby/body.rb +3 -0
  59. data/lib/wreq_ruby/client.rb +18 -18
  60. data/lib/wreq_ruby/cookie.rb +2 -2
  61. data/lib/wreq_ruby/emulate.rb +3 -1
  62. data/lib/wreq_ruby/header.rb +205 -114
  63. data/lib/wreq_ruby/response.rb +28 -3
  64. data/src/emulate.rs +1 -0
  65. data/src/error.rs +2 -6
  66. data/src/header/helper.rs +112 -0
  67. data/src/header.rs +198 -133
  68. data/test/header_test.rb +228 -192
  69. data/test/stream_test.rb +36 -0
  70. metadata +54 -1
data/src/header.rs CHANGED
@@ -1,161 +1,257 @@
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
+
11
+ mod helper;
12
+
1
13
  use std::cell::RefCell;
2
14
 
3
15
  use bytes::Bytes;
4
- use http::{HeaderMap, HeaderName, HeaderValue};
16
+ use http::{HeaderMap, HeaderValue};
5
17
  use magnus::{
6
- Error, Module, Object, RArray, RHash, RModule, RString, Ruby, TryConvert, Value,
7
- block::Yield,
8
- function, method,
9
- r_hash::ForEach,
18
+ Error, RArray, RModule, RString, Ruby, TryConvert, Value, function, method,
19
+ prelude::*,
10
20
  typed_data::{Inspect, Obj},
11
21
  };
12
22
  use wreq::header::OrigHeaderMap;
13
23
 
14
- use crate::error::{
15
- header_name_error_to_magnus, header_value_error_to_magnus, type_value_error_to_magnus,
24
+ use crate::error::{header_value_error_to_magnus, type_value_error_to_magnus};
25
+
26
+ use self::helper::{
27
+ ensure_header_count, from_source, header_count_error, parse_header_name, parse_header_values,
16
28
  };
17
29
 
18
- /// A wrapper for the User-Agent header value.
30
+ /// A validated User-Agent header value accepted from Ruby.
19
31
  pub struct UserAgent(pub HeaderValue);
20
32
 
21
- /// HTTP headers collection with read and write operations.
33
+ /// Mutable HTTP headers exposed as `Wreq::Headers`.
22
34
  ///
23
- /// This class wraps HTTP headers and provides convenient methods for
24
- /// accessing, modifying, and iterating over header name-value pairs.
35
+ /// Names are stored in their normalized form and lookups are case-insensitive.
36
+ /// A name can have multiple values, each counted as a separate occurrence.
25
37
  #[derive(Clone, Default)]
26
38
  #[magnus::wrap(class = "Wreq::Headers", free_immediately, size)]
27
39
  pub struct Headers(pub RefCell<HeaderMap>);
28
40
 
29
- /// A map from header names to their original casing as received in an HTTP message.
41
+ /// Header casing and order supplied through the `orig_headers` request option.
30
42
  pub struct OrigHeaders(pub OrigHeaderMap);
31
43
 
32
- struct HeaderIter {
33
- inner: http::header::IntoIter<HeaderValue>,
34
- next_name: Option<HeaderName>,
35
- }
36
-
37
44
  // ===== impl UserAgent =====
38
45
 
39
46
  impl TryConvert for UserAgent {
40
47
  fn try_convert(value: Value) -> Result<Self, Error> {
41
48
  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))
49
+ HeaderValue::from_maybe_shared(s.to_bytes())
50
+ .map(Self)
51
+ .map_err(header_value_error_to_magnus)
45
52
  }
46
53
  }
47
54
 
48
55
  // ===== impl Headers =====
49
56
 
50
57
  impl Headers {
51
- /// Create a new empty Headers instance.
52
- #[inline]
53
- pub fn new() -> Self {
54
- Self::from(HeaderMap::new())
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
- /// 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)
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
- /// 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);
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 name = parse_header_name(name)?;
82
+ let values = parse_header_values(value)?;
83
+ let mut headers = self.0.borrow_mut();
84
+ let replaced = headers.get_all(&name).iter().count();
85
+ ensure_header_count(headers.len(), replaced, values.len())?;
86
+
87
+ let mut values = values.into_iter();
88
+ let Some(first) = values.next() else {
89
+ headers.remove(name);
90
+ return Ok(());
91
+ };
92
+
93
+ headers
94
+ .try_insert(name.clone(), first)
95
+ .map_err(|_| header_count_error())?;
96
+ for value in values {
97
+ headers
98
+ .try_append(name.clone(), value)
99
+ .map_err(|_| header_count_error())?;
100
+ }
86
101
  Ok(())
87
102
  }
88
103
 
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);
104
+ /// Append one or more values without replacing existing occurrences.
105
+ ///
106
+ /// Array elements are appended separately and are never comma-folded.
107
+ pub fn append(&self, name: Value, value: Value) -> Result<(), Error> {
108
+ let name = parse_header_name(name)?;
109
+ let values = parse_header_values(value)?;
110
+ let mut headers = self.0.borrow_mut();
111
+ ensure_header_count(headers.len(), 0, values.len())?;
112
+
113
+ for value in values {
114
+ headers
115
+ .try_append(name.clone(), value)
116
+ .map_err(|_| header_count_error())?;
117
+ }
98
118
  Ok(())
99
119
  }
100
120
 
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)
121
+ /// Remove every value for a header name and return its first value.
122
+ ///
123
+ /// Returns `nil` when the normalized name is not present.
124
+ pub fn remove(&self, name: Value) -> Result<Option<Bytes>, Error> {
125
+ let name = parse_header_name(name)?;
126
+ Ok(self.0.borrow_mut().remove(name).map(Bytes::from_owner))
105
127
  }
106
128
 
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)
129
+ /// Return whether a String or Symbol header name is present.
130
+ pub fn contains(&self, name: Value) -> Result<bool, Error> {
131
+ let name = parse_header_name(name)?;
132
+ Ok(self.0.borrow().contains_key(name))
111
133
  }
112
134
 
113
- /// Get the number of headers.
135
+ /// Return the total number of header occurrences.
136
+ ///
137
+ /// This can be greater than `keys.length` when names have multiple values.
114
138
  #[inline]
115
139
  pub fn len(&self) -> usize {
116
140
  self.0.borrow().len()
117
141
  }
118
142
 
119
- /// Check if headers are empty.
143
+ /// Return whether the collection contains no header occurrences.
120
144
  #[inline]
121
145
  pub fn is_empty(&self) -> bool {
122
146
  self.0.borrow().is_empty()
123
147
  }
124
148
 
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]
149
+ /// Return each unique normalized header name.
133
150
  pub fn keys(ruby: &Ruby, rb_self: &Self) -> RArray {
134
151
  ruby.ary_from_iter(rb_self.0.borrow().keys().cloned().map(Bytes::from_owner))
135
152
  }
136
153
 
137
- /// Get all header values.
154
+ /// Return all values, including duplicate-name occurrences.
138
155
  #[inline]
139
156
  pub fn values(ruby: &Ruby, rb_self: &Self) -> RArray {
140
157
  ruby.ary_from_iter(rb_self.0.borrow().values().cloned().map(Bytes::from_owner))
141
158
  }
142
159
 
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.
160
+ /// Return the debug representation of the underlying header map.
153
161
  #[inline]
154
162
  pub fn to_s(&self) -> String {
155
163
  self.0.borrow().inspect()
156
164
  }
157
165
  }
158
166
 
167
+ // Ruby collection adapters are kept separate from the core HeaderMap operations.
168
+ impl Headers {
169
+ /// Create an empty collection or populate it from a Ruby source.
170
+ ///
171
+ /// The optional source may be a Hash, another `Wreq::Headers`, or an
172
+ /// Enumerable whose elements are name-value pairs.
173
+ pub fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
174
+ match args {
175
+ [] => Ok(Self::default()),
176
+ [source] => from_source(*source),
177
+ _ => Err(Error::new(
178
+ ruby.exception_arg_error(),
179
+ format!(
180
+ "wrong number of arguments (given {}, expected 0..1)",
181
+ args.len()
182
+ ),
183
+ )),
184
+ }
185
+ }
186
+
187
+ /// Return a value using Ruby collection semantics.
188
+ ///
189
+ /// A missing name returns `nil`, one occurrence returns a String, and
190
+ /// multiple occurrences return an Array of Strings.
191
+ pub fn index(ruby: &Ruby, rb_self: &Self, name: Value) -> Result<Value, Error> {
192
+ let name = parse_header_name(name)?;
193
+ let headers = rb_self.0.borrow();
194
+ let all_values = headers.get_all(name);
195
+ let mut values = all_values.iter();
196
+
197
+ let Some(first) = values.next() else {
198
+ return Ok(ruby.qnil().as_value());
199
+ };
200
+ let Some(second) = values.next() else {
201
+ return Ok(ruby.into_value(Bytes::from_owner(first.clone())));
202
+ };
203
+
204
+ let values = std::iter::once(first)
205
+ .chain(std::iter::once(second))
206
+ .chain(values)
207
+ .cloned()
208
+ .map(Bytes::from_owner);
209
+ Ok(ruby.into_value(ruby.ary_from_iter(values)))
210
+ }
211
+
212
+ /// Replace a header and return the assigned Ruby value for `headers[name] = value`.
213
+ pub fn set_index(&self, name: Value, value: Value) -> Result<Value, Error> {
214
+ self.set(name, value)?;
215
+ Ok(value)
216
+ }
217
+
218
+ /// Remove every occurrence and return the same `Wreq::Headers` object.
219
+ pub fn clear(rb_self: Value) -> Result<Value, Error> {
220
+ let headers = Obj::<Headers>::try_convert(rb_self)?;
221
+ headers.0.borrow_mut().clear();
222
+ Ok(rb_self)
223
+ }
224
+
225
+ /// Yield every normalized name-value occurrence.
226
+ ///
227
+ /// Returns an Enumerator without a block and returns the collection after
228
+ /// yielding when a block is provided.
229
+ pub fn each(ruby: &Ruby, rb_self: Value) -> Result<Value, Error> {
230
+ if !ruby.block_given() {
231
+ return Ok(ruby.into_value(rb_self.enumeratorize("each", ())));
232
+ }
233
+
234
+ let headers = Obj::<Headers>::try_convert(rb_self)?;
235
+ // Release the RefCell borrow before yielding because Ruby code may
236
+ // mutate this collection from inside the block.
237
+ let entries: Vec<_> = headers
238
+ .0
239
+ .borrow()
240
+ .iter()
241
+ .map(|(name, value)| {
242
+ (
243
+ Bytes::from_owner(name.clone()),
244
+ Bytes::from_owner(value.clone()),
245
+ )
246
+ })
247
+ .collect();
248
+ for (name, value) in entries {
249
+ let _: Value = ruby.yield_values((name, value))?;
250
+ }
251
+ Ok(rb_self)
252
+ }
253
+ }
254
+
159
255
  impl From<HeaderMap> for Headers {
160
256
  fn from(headers: HeaderMap) -> Self {
161
257
  Self(RefCell::new(headers))
@@ -164,32 +260,14 @@ impl From<HeaderMap> for Headers {
164
260
 
165
261
  impl TryConvert for Headers {
166
262
  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)
263
+ from_source(value)
186
264
  }
187
265
  }
188
266
 
189
267
  // ===== impl OrigHeaders =====
190
268
 
191
269
  impl TryConvert for OrigHeaders {
192
- fn try_convert(value: magnus::Value) -> Result<Self, magnus::Error> {
270
+ fn try_convert(value: Value) -> Result<Self, Error> {
193
271
  let mut map = OrigHeaderMap::new();
194
272
 
195
273
  let rarray = RArray::from_value(value)
@@ -203,30 +281,11 @@ impl TryConvert for OrigHeaders {
203
281
  }
204
282
  }
205
283
 
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
-
284
+ /// Register `Wreq::Headers` and its native methods with Ruby.
226
285
  pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
227
- // Define Headers class with methods
228
286
  let headers_class = gem_module.define_class("Headers", ruby.class_object())?;
229
- headers_class.define_singleton_method("new", function!(Headers::new, 0))?;
287
+
288
+ // Core bindings expose direct HeaderMap operations.
230
289
  headers_class.define_method("get", method!(Headers::get, 1))?;
231
290
  headers_class.define_method("get_all", method!(Headers::get_all, 1))?;
232
291
  headers_class.define_method("set", method!(Headers::set, 2))?;
@@ -236,10 +295,16 @@ pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
236
295
  headers_class.define_method("key?", method!(Headers::contains, 1))?;
237
296
  headers_class.define_method("length", method!(Headers::len, 0))?;
238
297
  headers_class.define_method("empty?", method!(Headers::is_empty, 0))?;
239
- headers_class.define_method("clear", method!(Headers::clear, 0))?;
240
298
  headers_class.define_method("keys", method!(Headers::keys, 0))?;
241
299
  headers_class.define_method("values", method!(Headers::values, 0))?;
242
- headers_class.define_method("each", method!(Headers::each, 0))?;
243
300
  headers_class.define_method("to_s", method!(Headers::to_s, 0))?;
301
+
302
+ // Ruby collection bindings cover construction, indexing, and block semantics.
303
+ headers_class.include_module(ruby.module_enumerable())?;
304
+ headers_class.define_singleton_method("new", function!(Headers::new, -1))?;
305
+ headers_class.define_method("[]", method!(Headers::index, 1))?;
306
+ headers_class.define_method("[]=", method!(Headers::set_index, 2))?;
307
+ headers_class.define_method("clear", method!(Headers::clear, 0))?;
308
+ headers_class.define_method("each", method!(Headers::each, 0))?;
244
309
  Ok(())
245
310
  }