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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +22 -19
  3. data/Cargo.toml +15 -2
  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 +372 -0
  22. data/crates/wreq-util/src/emulate/profile/chrome/header.rs +74 -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 +37 -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 +29 -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 +153 -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 +72 -45
  58. data/lib/wreq_ruby/body.rb +30 -9
  59. data/lib/wreq_ruby/client.rb +88 -55
  60. data/lib/wreq_ruby/cookie.rb +81 -21
  61. data/lib/wreq_ruby/emulate.rb +77 -7
  62. data/lib/wreq_ruby/error.rb +5 -7
  63. data/lib/wreq_ruby/header.rb +205 -114
  64. data/lib/wreq_ruby/http.rb +74 -0
  65. data/lib/wreq_ruby/response.rb +31 -3
  66. data/script/build_windows_gnu.ps1 +6 -0
  67. data/src/arch.rs +22 -0
  68. data/src/client/body/form.rs +2 -0
  69. data/src/client/body/json.rs +47 -14
  70. data/src/client/body/stream.rs +147 -43
  71. data/src/client/body.rs +11 -15
  72. data/src/client/param.rs +7 -7
  73. data/src/client/req.rs +87 -47
  74. data/src/client/resp.rs +23 -24
  75. data/src/client.rs +310 -230
  76. data/src/cookie.rs +280 -87
  77. data/src/emulate.rs +86 -50
  78. data/src/error.rs +198 -45
  79. data/src/extractor.rs +16 -76
  80. data/src/header.rs +318 -130
  81. data/src/http.rs +62 -33
  82. data/src/lib.rs +26 -20
  83. data/src/macros.rs +71 -46
  84. data/src/options.rs +284 -0
  85. data/src/rt.rs +22 -11
  86. data/src/serde/de/array_deserializer.rs +48 -0
  87. data/src/serde/de/array_enumerator.rs +59 -0
  88. data/src/serde/de/deserializer.rs +307 -0
  89. data/src/serde/de/enum_deserializer.rs +47 -0
  90. data/src/serde/de/hash_deserializer.rs +89 -0
  91. data/src/serde/de/number_deserializer.rs +51 -0
  92. data/src/serde/de/variant_deserializer.rs +101 -0
  93. data/src/serde/de.rs +33 -0
  94. data/src/serde/error.rs +146 -0
  95. data/src/serde/ser/enums.rs +14 -0
  96. data/src/serde/ser/map_serializer.rs +56 -0
  97. data/src/serde/ser/seq_serializer.rs +71 -0
  98. data/src/serde/ser/serializer.rs +194 -0
  99. data/src/serde/ser/struct_serializer.rs +106 -0
  100. data/src/serde/ser/struct_variant_serializer.rs +44 -0
  101. data/src/serde/ser/tuple_variant_serializer.rs +41 -0
  102. data/src/serde/ser.rs +18 -0
  103. data/src/serde/tests.rs +237 -0
  104. data/src/serde.rs +202 -0
  105. data/test/body_sender_test.rb +246 -0
  106. data/test/cookie_test.rb +211 -10
  107. data/test/header_test.rb +228 -192
  108. data/test/json_precision_test.rb +209 -0
  109. data/test/option_validation_test.rb +311 -0
  110. data/test/stream_test.rb +36 -0
  111. data/test/value_semantics_test.rb +238 -0
  112. 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, HeaderName, HeaderValue};
14
+ use http::{HeaderMap, HeaderValue};
5
15
  use magnus::{
6
- Error, Module, Object, RArray, RHash, RModule, RString, Ruby, TryConvert, Value,
7
- block::Yield,
8
- function, method,
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
- header_name_error_to_magnus, header_value_error_to_magnus, type_value_error_to_magnus,
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 wrapper for the User-Agent header value.
29
+ /// A validated User-Agent header value accepted from Ruby.
19
30
  pub struct UserAgent(pub HeaderValue);
20
31
 
21
- /// HTTP headers collection with read and write operations.
32
+ /// Mutable HTTP headers exposed as `Wreq::Headers`.
22
33
  ///
23
- /// This class wraps HTTP headers and provides convenient methods for
24
- /// accessing, modifying, and iterating over header name-value pairs.
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
- /// A map from header names to their original casing as received in an HTTP message.
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
- 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(|err| header_value_error(&ruby, err))
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 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 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);
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 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)
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
- /// Check if a header exists (case-insensitive).
108
- #[inline]
109
- pub fn contains(&self, name: String) -> bool {
110
- self.0.borrow().contains_key(&name)
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
- /// Get the number of headers.
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
- /// Check if headers are empty.
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
- /// 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]
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
- /// Get all header values.
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
- /// 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.
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
- 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)
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: magnus::Value) -> Result<Self, magnus::Error> {
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(|| type_value_error_to_magnus("Expected an array of strings"))?;
272
+ .ok_or_else(|| header_type_error(&ruby, "Expected an array of strings"))?;
197
273
 
198
- for value in rarray.into_iter().flat_map(RString::from_value) {
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
- // ===== 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)))
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
- (None, None) => None,
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
- headers_class.define_singleton_method("new", function!(Headers::new, 0))?;
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
  }