wreq 1.1.0 → 1.2.0

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.
@@ -88,26 +88,16 @@ unless defined?(Wreq)
88
88
  def bytes
89
89
  end
90
90
 
91
- # Get the response body as text.
92
- #
93
- # @return [String] Response body decoded as UTF-8 text
94
- # @example
95
- # html = response.text
96
- # puts html
97
- # @raise [Wreq::DecodingError] if body cannot be decoded as binary
98
- def text
99
- end
100
-
101
91
  # Get the response body as text with a specific charset.
102
92
  # This method allows you to specify a default encoding
103
93
  # to use when decoding the response body.
104
94
  # # @param default_encoding [String] Default encoding to use (e.g., "UTF-8")
105
95
  # # @return [String] Response body decoded as text using the specified encoding
106
96
  # @example
107
- # html = response.text_with_charset("ISO-8859-1")
97
+ # html = response.text("ISO-8859-1")
108
98
  # puts html
109
99
  # @raise [Wreq::DecodingError] if body cannot be decoded with the specified encoding
110
- def text_with_charset(default_encoding)
100
+ def text(default_encoding: "UTF-8")
111
101
  end
112
102
 
113
103
  # Parse the response body as JSON.
@@ -120,14 +110,17 @@ unless defined?(Wreq)
120
110
  def json
121
111
  end
122
112
 
123
- # Get a streaming iterator for the response body, yielding each chunk.
113
+ # Stream the response body, yielding each chunk to the given block.
124
114
  #
125
115
  # This method allows you to process large HTTP responses efficiently,
126
116
  # by yielding each chunk of the body as it arrives, without loading
127
117
  # the entire response into memory.
128
118
  #
129
- # @return An iterator over response body chunks (binary String)
119
+ # @return [nil]
130
120
  # @yield [chunk] Each chunk of the response body as a binary String
121
+ # @raise [LocalJumpError] if called without a block
122
+ # @raise [Wreq::TimeoutError, Wreq::BodyError, Wreq::ConnectionResetError, Wreq::RequestError]
123
+ # if streaming fails while reading the response body
131
124
  # @example Save response to file
132
125
  # File.open("output.bin", "wb") do |f|
133
126
  # response.chunks { |chunk| f.write(chunk) }
@@ -137,7 +130,7 @@ unless defined?(Wreq)
137
130
  # response.chunks { |chunk| total += chunk.bytesize }
138
131
  # puts "Downloaded #{total} bytes"
139
132
  #
140
- # Note: The returned Receiver is only for reading response bodies, not for uploads.
133
+ # Exceptions raised inside the block are propagated to the caller.
141
134
  def chunks
142
135
  end
143
136
 
@@ -156,27 +149,35 @@ end
156
149
 
157
150
  module Wreq
158
151
  class Response
159
- # Returns a compact string representation of the response.
152
+ # Returns the response body as a string.
153
+ #
154
+ # @return [String] Response body text
155
+ # @example
156
+ # puts response.to_s
157
+ # puts response
158
+ # File.write("page.html", response)
159
+ def to_s
160
+ text
161
+ end
162
+
163
+ # Returns a compact string representation for debugging.
160
164
  #
161
165
  # Format: #<Wreq::Response STATUS content-type="..." body=SIZE>
162
166
  #
163
167
  # @return [String] Compact formatted response information
164
168
  # @example
165
- # puts response.to_s
169
+ # p response
166
170
  # # => #<Wreq::Response 200 content-type="application/json" body=456B>
167
- def to_s
171
+ def inspect
168
172
  parts = ["#<Wreq::Response"]
169
173
 
170
- # Status code
171
174
  parts << code.to_s
172
175
 
173
- # Content-Type header if present
174
176
  if headers.respond_to?(:get)
175
177
  content_type = headers.get("content-type")
176
178
  parts << "content-type=#{content_type.inspect}" if content_type
177
179
  end
178
180
 
179
- # Body size
180
181
  if content_length
181
182
  parts << "body=#{format_bytes(content_length)}"
182
183
  end
@@ -13,7 +13,7 @@ use tokio::sync::{
13
13
  };
14
14
 
15
15
  use crate::{
16
- error::{memory_error, mpsc_send_error_to_magnus},
16
+ error::{memory_error, mpsc_send_error_to_magnus, wreq_error_to_magnus},
17
17
  rt,
18
18
  };
19
19
 
@@ -37,20 +37,15 @@ impl BodyReceiver {
37
37
  pub fn new(stream: impl Stream<Item = wreq::Result<Bytes>> + Send + 'static) -> BodyReceiver {
38
38
  BodyReceiver(Mutex::new(Box::pin(stream)))
39
39
  }
40
- }
41
40
 
42
- impl Iterator for BodyReceiver {
43
- type Item = Bytes;
44
-
45
- fn next(&mut self) -> Option<Self::Item> {
46
- rt::maybe_block_on(async {
47
- self.0
48
- .lock()
49
- .await
50
- .as_mut()
51
- .next()
52
- .await
53
- .and_then(|r| r.ok())
41
+ /// Read the next body chunk, converting stream errors into Ruby errors.
42
+ pub fn next(&self) -> Result<Option<Bytes>, Error> {
43
+ rt::try_block_on(async {
44
+ match self.0.lock().await.as_mut().next().await {
45
+ Some(Ok(data)) => Ok(Some(data)),
46
+ Some(Err(err)) => Err(wreq_error_to_magnus(err)),
47
+ None => Ok(None),
48
+ }
54
49
  })
55
50
  }
56
51
  }
data/src/client/req.rs CHANGED
@@ -1,19 +1,18 @@
1
1
  use std::{net::IpAddr, time::Duration};
2
2
 
3
- use http::{HeaderValue, header};
3
+ use http::header;
4
4
  use magnus::{RHash, TryConvert, typed_data::Obj, value::ReprValue};
5
5
  use serde::Deserialize;
6
- use wreq::{
7
- Client, Proxy, Version,
8
- header::{HeaderMap, OrigHeaderMap},
9
- };
6
+ use wreq::{Client, Proxy, Version, header::OrigHeaderMap};
10
7
 
11
8
  use super::body::{Body, Form, Json};
12
9
  use crate::{
13
10
  client::{query::Query, resp::Response},
11
+ cookie::Cookies,
14
12
  emulate::Emulation,
15
13
  error::wreq_error_to_magnus,
16
14
  extractor::Extractor,
15
+ header::Headers,
17
16
  http::Method,
18
17
  rt,
19
18
  };
@@ -46,20 +45,20 @@ pub struct Request {
46
45
  #[serde(skip)]
47
46
  version: Option<Version>,
48
47
 
48
+ /// The option enables default headers.
49
+ default_headers: Option<bool>,
50
+
49
51
  /// The headers to use for the request.
50
52
  #[serde(skip)]
51
- headers: Option<HeaderMap>,
53
+ headers: Option<Headers>,
52
54
 
53
55
  /// The original headers to use for the request.
54
56
  #[serde(skip)]
55
57
  orig_headers: Option<OrigHeaderMap>,
56
58
 
57
- /// The option enables default headers.
58
- default_headers: Option<bool>,
59
-
60
59
  /// The cookies to use for the request.
61
60
  #[serde(skip)]
62
- cookies: Option<Vec<HeaderValue>>,
61
+ cookies: Option<Cookies>,
63
62
 
64
63
  /// Whether to allow redirects.
65
64
  allow_redirects: Option<bool>,
@@ -105,35 +104,30 @@ pub struct Request {
105
104
  impl Request {
106
105
  /// Create a new [`Request`] from Ruby keyword arguments.
107
106
  pub fn new(ruby: &magnus::Ruby, hash: RHash) -> Result<Self, magnus::Error> {
108
- let kwargs = hash.as_value();
109
- let mut builder: Self = serde_magnus::deserialize(ruby, kwargs)?;
107
+ let keyword = hash.as_value();
108
+ let mut builder: Self = serde_magnus::deserialize(ruby, keyword)?;
110
109
 
111
- // extra emulation handling
112
- if let Some(v) = hash.get(ruby.to_symbol("emulation")) {
113
- let emulation_obj = Obj::<Emulation>::try_convert(v)?;
114
- builder.emulation = Some((*emulation_obj).clone());
110
+ if let Some(v) = hash.get(ruby.to_symbol(stringify!(emulation))) {
111
+ let obj = Obj::<Emulation>::try_convert(v)?;
112
+ builder.emulation = Some((*obj).clone());
115
113
  }
116
114
 
117
- // extra version handling
118
- builder.version = Extractor::<Version>::try_convert(kwargs)?.into_inner();
119
-
120
- // extra headers handling
121
- builder.headers = Extractor::<HeaderMap>::try_convert(kwargs)?.into_inner();
122
-
123
- // extra original headers handling
124
- builder.orig_headers = Extractor::<OrigHeaderMap>::try_convert(kwargs)?.into_inner();
125
-
126
- // extra cookies handling
127
- builder.cookies = Extractor::<Vec<HeaderValue>>::try_convert(kwargs)?.into_inner();
115
+ if let Some(v) = hash.get(ruby.to_symbol(stringify!(headers))) {
116
+ builder.headers = Some(Headers::try_convert(v)?);
117
+ }
128
118
 
129
- // extra proxy handling
130
- builder.proxy = Extractor::<Proxy>::try_convert(kwargs)?.into_inner();
119
+ if let Some(v) = hash.get(ruby.to_symbol(stringify!(cookies))) {
120
+ builder.cookies = Some(Cookies::try_convert(v)?);
121
+ }
131
122
 
132
- // extra body handling
133
- if let Some(body) = hash.get(ruby.to_symbol("body")) {
134
- builder.body = Some(Body::try_convert(body)?);
123
+ if let Some(v) = hash.get(ruby.to_symbol(stringify!(body))) {
124
+ builder.body = Some(Body::try_convert(v)?);
135
125
  }
136
126
 
127
+ builder.proxy = Extractor::<Proxy>::try_convert(keyword)?.into_inner();
128
+ builder.version = Extractor::<Version>::try_convert(keyword)?.into_inner();
129
+ builder.orig_headers = Extractor::<OrigHeaderMap>::try_convert(keyword)?.into_inner();
130
+
137
131
  Ok(builder)
138
132
  }
139
133
  }
@@ -175,7 +169,7 @@ pub fn execute_request<U: AsRef<str>>(
175
169
  apply_option!(set_if_some, builder, request.interface, interface);
176
170
 
177
171
  // Headers options.
178
- apply_option!(set_if_some, builder, request.headers, headers);
172
+ apply_option!(set_if_some_into_inner, builder, request.headers, headers);
179
173
  apply_option!(set_if_some, builder, request.orig_headers, orig_headers);
180
174
  apply_option!(
181
175
  set_if_some,
@@ -184,6 +178,13 @@ pub fn execute_request<U: AsRef<str>>(
184
178
  default_headers
185
179
  );
186
180
 
181
+ // Cookies options.
182
+ if let Some(cookies) = request.cookies.take() {
183
+ for cookie in cookies.0 {
184
+ builder = builder.header(header::COOKIE, cookie);
185
+ }
186
+ }
187
+
187
188
  // Authentication options.
188
189
  apply_option!(
189
190
  set_if_some_map_ref,
@@ -197,13 +198,6 @@ pub fn execute_request<U: AsRef<str>>(
197
198
  builder = builder.basic_auth(basic_auth.0, basic_auth.1);
198
199
  }
199
200
 
200
- // Cookies options.
201
- if let Some(cookies) = request.cookies.take() {
202
- for cookie in cookies {
203
- builder = builder.header(header::COOKIE, cookie);
204
- }
205
- }
206
-
207
201
  // Allow redirects options.
208
202
  match request.allow_redirects {
209
203
  Some(false) => {
data/src/client/resp.rs CHANGED
@@ -5,14 +5,14 @@ use bytes::Bytes;
5
5
  use futures_util::TryFutureExt;
6
6
  use http::{Extensions, HeaderMap, response::Response as HttpResponse};
7
7
  use http_body_util::BodyExt;
8
- use magnus::{Error, Module, RArray, RModule, Ruby, Value, block::Yield};
8
+ use magnus::{Error, Module, RArray, RModule, Ruby, Value, scan_args::scan_args};
9
9
  use wreq::Uri;
10
10
 
11
11
  use crate::{
12
12
  client::body::{BodyReceiver, Json},
13
13
  cookie::Cookie,
14
- error::{memory_error, wreq_error_to_magnus},
15
- gvl,
14
+ error::{memory_error, no_block_given_error, wreq_error_to_magnus},
15
+ gvl::{self, nogvl},
16
16
  header::Headers,
17
17
  http::{StatusCode, Version},
18
18
  rt,
@@ -173,20 +173,18 @@ impl Response {
173
173
  rt::try_block_on(response.bytes().map_err(wreq_error_to_magnus))
174
174
  }
175
175
 
176
- /// Get the response body as a UTF-8 string.
177
- pub fn text(&self) -> Result<String, Error> {
178
- let response = self.response(false)?;
179
- rt::try_block_on(response.text().map_err(wreq_error_to_magnus))
180
- }
181
-
182
176
  /// Get the full response text given a specific encoding.
183
- pub fn text_with_charset(&self, default_encoding: String) -> Result<String, Error> {
177
+ pub fn text(&self, args: &[Value]) -> Result<String, Error> {
178
+ let args = scan_args::<(), (Option<String>,), (), (), (), ()>(args)?;
184
179
  let response = self.response(false)?;
185
- rt::try_block_on(
186
- response
187
- .text_with_charset(default_encoding)
188
- .map_err(wreq_error_to_magnus),
189
- )
180
+ match args.optional.0 {
181
+ Some(encoding) => rt::try_block_on(
182
+ response
183
+ .text_with_charset(encoding)
184
+ .map_err(wreq_error_to_magnus),
185
+ ),
186
+ None => rt::try_block_on(response.text().map_err(wreq_error_to_magnus)),
187
+ }
190
188
  }
191
189
 
192
190
  /// Get the response body as JSON.
@@ -201,12 +199,24 @@ impl Response {
201
199
  })
202
200
  }
203
201
 
204
- /// Get a chunk iterator for the response body.
205
- pub fn chunks(&self) -> Result<Yield<BodyReceiver>, Error> {
206
- self.response(true)
207
- .map(wreq::Response::bytes_stream)
208
- .map(BodyReceiver::new)
209
- .map(Yield::Iter)
202
+ /// Yield response body chunks to the given Ruby block.
203
+ pub fn chunks(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
204
+ if !ruby.block_given() {
205
+ return Err(no_block_given_error());
206
+ }
207
+
208
+ let receiver = nogvl(|| {
209
+ rb_self
210
+ .response(true)
211
+ .map(wreq::Response::bytes_stream)
212
+ .map(BodyReceiver::new)
213
+ })?;
214
+
215
+ while let Some(chunk) = receiver.next()? {
216
+ let _: Value = ruby.yield_value(chunk)?;
217
+ }
218
+
219
+ Ok(())
210
220
  }
211
221
 
212
222
  /// Close the response body, dropping any resources.
@@ -224,27 +234,23 @@ impl Drop for Response {
224
234
  }
225
235
 
226
236
  pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
227
- let response_class = gem_module.define_class("Response", ruby.class_object())?;
228
- response_class.define_method("code", magnus::method!(Response::code, 0))?;
229
- response_class.define_method("status", magnus::method!(Response::status, 0))?;
230
- response_class.define_method("version", magnus::method!(Response::version, 0))?;
231
- response_class.define_method("url", magnus::method!(Response::url, 0))?;
232
- response_class.define_method(
237
+ let response = gem_module.define_class("Response", ruby.class_object())?;
238
+ response.define_method("code", magnus::method!(Response::code, 0))?;
239
+ response.define_method("status", magnus::method!(Response::status, 0))?;
240
+ response.define_method("version", magnus::method!(Response::version, 0))?;
241
+ response.define_method("url", magnus::method!(Response::url, 0))?;
242
+ response.define_method(
233
243
  "content_length",
234
244
  magnus::method!(Response::content_length, 0),
235
245
  )?;
236
- response_class.define_method("cookies", magnus::method!(Response::cookies, 0))?;
237
- response_class.define_method("headers", magnus::method!(Response::headers, 0))?;
238
- response_class.define_method("local_addr", magnus::method!(Response::local_addr, 0))?;
239
- response_class.define_method("remote_addr", magnus::method!(Response::remote_addr, 0))?;
240
- response_class.define_method("bytes", magnus::method!(Response::bytes, 0))?;
241
- response_class.define_method("text", magnus::method!(Response::text, 0))?;
242
- response_class.define_method(
243
- "text_with_charset",
244
- magnus::method!(Response::text_with_charset, 1),
245
- )?;
246
- response_class.define_method("json", magnus::method!(Response::json, 0))?;
247
- response_class.define_method("chunks", magnus::method!(Response::chunks, 0))?;
248
- response_class.define_method("close", magnus::method!(Response::close, 0))?;
246
+ response.define_method("cookies", magnus::method!(Response::cookies, 0))?;
247
+ response.define_method("headers", magnus::method!(Response::headers, 0))?;
248
+ response.define_method("local_addr", magnus::method!(Response::local_addr, 0))?;
249
+ response.define_method("remote_addr", magnus::method!(Response::remote_addr, 0))?;
250
+ response.define_method("bytes", magnus::method!(Response::bytes, 0))?;
251
+ response.define_method("text", magnus::method!(Response::text, -1))?;
252
+ response.define_method("json", magnus::method!(Response::json, 0))?;
253
+ response.define_method("chunks", magnus::method!(Response::chunks, 0))?;
254
+ response.define_method("close", magnus::method!(Response::close, 0))?;
249
255
  Ok(())
250
256
  }
data/src/client.rs CHANGED
@@ -12,7 +12,7 @@ use magnus::{
12
12
  use serde::Deserialize;
13
13
  use wreq::{
14
14
  Proxy,
15
- header::{HeaderMap, HeaderValue, OrigHeaderMap},
15
+ header::{HeaderValue, OrigHeaderMap},
16
16
  };
17
17
 
18
18
  use crate::{
@@ -22,6 +22,7 @@ use crate::{
22
22
  error::wreq_error_to_magnus,
23
23
  extractor::Extractor,
24
24
  gvl,
25
+ header::Headers,
25
26
  http::Method,
26
27
  };
27
28
 
@@ -36,7 +37,7 @@ struct Builder {
36
37
  user_agent: Option<HeaderValue>,
37
38
  /// The headers to use for the client.
38
39
  #[serde(skip)]
39
- headers: Option<HeaderMap>,
40
+ headers: Option<Headers>,
40
41
  /// The original headers to use for the client.
41
42
  #[serde(skip)]
42
43
  orig_headers: Option<OrigHeaderMap>,
@@ -83,7 +84,7 @@ struct Builder {
83
84
  /// Sets the maximum idle connection per host allowed in the pool.
84
85
  pool_max_idle_per_host: Option<usize>,
85
86
  /// Sets the maximum number of connections in the pool.
86
- pool_max_size: Option<u32>,
87
+ pool_max_size: Option<usize>,
87
88
 
88
89
  // ========= Protocol options =========
89
90
  /// Whether to use the HTTP/1 protocol only.
@@ -128,35 +129,33 @@ pub struct Client(wreq::Client);
128
129
  impl Builder {
129
130
  /// Create a new [`Builder`] from Ruby keyword arguments.
130
131
  fn new(ruby: &magnus::Ruby, keyword: &Value) -> Result<Self, magnus::Error> {
131
- if let Ok(hash) = RHash::try_convert(*keyword) {
132
- let mut builder: Self = serde_magnus::deserialize(ruby, hash)?;
133
- // extra emulation handling
134
- if let Some(v) = hash.get(ruby.to_symbol("emulation")) {
135
- let emulation_obj = Obj::<Emulation>::try_convert(v)?;
136
- builder.emulation = Some((*emulation_obj).clone());
137
- }
132
+ let Ok(hash) = RHash::try_convert(*keyword) else {
133
+ return Ok(Default::default());
134
+ };
138
135
 
139
- // extra user agent handling
140
- builder.user_agent = Extractor::<HeaderValue>::try_convert(*keyword)?.into_inner();
136
+ let mut builder: Self = serde_magnus::deserialize(ruby, hash)?;
141
137
 
142
- // extra headers handling
143
- builder.headers = Extractor::<HeaderMap>::try_convert(*keyword)?.into_inner();
144
-
145
- // extra original headers handling
146
- builder.orig_headers = Extractor::<OrigHeaderMap>::try_convert(*keyword)?.into_inner();
138
+ if let Some(v) = hash.get(ruby.to_symbol(stringify!(emulation))) {
139
+ builder.emulation = Some((*Obj::<Emulation>::try_convert(v)?).clone());
140
+ }
147
141
 
148
- // extra proxy handling
149
- builder.proxy = Extractor::<Proxy>::try_convert(*keyword)?.into_inner();
142
+ if let Some(v) = hash.get(ruby.to_symbol(stringify!(headers))) {
143
+ builder.headers = Some(Headers::try_convert(v)?);
144
+ }
150
145
 
151
- // extra cookie store handling
152
- if let Some(jar) = hash.get(ruby.to_symbol("cookie_provider")) {
153
- builder.cookie_provider = Some((*Obj::<Jar>::try_convert(jar)?).clone());
154
- }
146
+ if let Some(v) = hash.get(ruby.to_symbol(stringify!(cookie_provider))) {
147
+ builder.cookie_provider = Some((*Obj::<Jar>::try_convert(v)?).clone());
148
+ }
155
149
 
156
- return Ok(builder);
150
+ if let Some(jar) = hash.get(ruby.to_symbol(stringify!(cookie_provider))) {
151
+ builder.cookie_provider = Some((*Obj::<Jar>::try_convert(jar)?).clone());
157
152
  }
158
153
 
159
- Ok(Default::default())
154
+ builder.user_agent = Extractor::<HeaderValue>::try_convert(*keyword)?.into_inner();
155
+ builder.orig_headers = Extractor::<OrigHeaderMap>::try_convert(*keyword)?.into_inner();
156
+ builder.proxy = Extractor::<Proxy>::try_convert(*keyword)?.into_inner();
157
+
158
+ Ok(builder)
160
159
  }
161
160
  }
162
161
 
@@ -164,9 +163,9 @@ impl Builder {
164
163
 
165
164
  impl Client {
166
165
  /// Create a new [`Client`] with the given keyword arguments.
167
- pub fn new(ruby: &Ruby, kwargs: &[Value]) -> Result<Self, magnus::Error> {
168
- if let Some(kwargs) = kwargs.first() {
169
- let mut params = Builder::new(ruby, kwargs)?;
166
+ pub fn new(ruby: &Ruby, keyword: &[Value]) -> Result<Self, magnus::Error> {
167
+ if let Some(keyword) = keyword.first() {
168
+ let mut params = Builder::new(ruby, keyword)?;
170
169
  gvl::nogvl(|| {
171
170
  let mut builder = wreq::Client::builder();
172
171
 
@@ -177,7 +176,12 @@ impl Client {
177
176
  apply_option!(set_if_some, builder, params.user_agent, user_agent);
178
177
 
179
178
  // Default headers options.
180
- apply_option!(set_if_some, builder, params.headers, default_headers);
179
+ apply_option!(
180
+ set_if_some_into_inner,
181
+ builder,
182
+ params.headers,
183
+ default_headers
184
+ );
181
185
  apply_option!(set_if_some, builder, params.orig_headers, orig_headers);
182
186
 
183
187
  // Allow redirects options.
@@ -286,7 +290,7 @@ impl Client {
286
290
  apply_option!(set_if_some, builder, params.https_only, https_only);
287
291
 
288
292
  // TLS options.
289
- apply_option!(set_if_some, builder, params.verify, cert_verification);
293
+ apply_option!(set_if_some, builder, params.verify, tls_cert_verification);
290
294
 
291
295
  // Network options.
292
296
  apply_option!(set_if_some, builder, params.proxy, proxy);
data/src/cookie.rs CHANGED
@@ -1,13 +1,14 @@
1
1
  use std::{fmt, sync::Arc, time::SystemTime};
2
2
 
3
+ use bytes::Bytes;
3
4
  use cookie::{Cookie as RawCookie, Expiration, ParseError, time::Duration};
4
5
  use magnus::{
5
- Error, Module, Object, RModule, Ruby, Value, function, method, typed_data::Obj,
6
- value::ReprValue,
6
+ Error, Module, Object, RHash, RModule, RString, Ruby, TryConvert, Value, function, method,
7
+ r_hash::ForEach, typed_data::Obj, value::ReprValue,
7
8
  };
8
9
  use wreq::header::{self, HeaderMap, HeaderValue};
9
10
 
10
- use crate::gvl;
11
+ use crate::{error::header_value_error_to_magnus, gvl};
11
12
 
12
13
  define_ruby_enum!(
13
14
  /// The Cookie SameSite attribute.
@@ -25,6 +26,10 @@ define_ruby_enum!(
25
26
  #[magnus::wrap(class = "Wreq::Cookie", free_immediately, size)]
26
27
  pub struct Cookie(RawCookie<'static>);
27
28
 
29
+ /// A collection of HTTP cookies.
30
+ #[derive(Default)]
31
+ pub struct Cookies(pub Vec<HeaderValue>);
32
+
28
33
  /// A good default `CookieStore` implementation.
29
34
  ///
30
35
  /// This is the implementation used when simply calling `cookie_store(true)`.
@@ -197,6 +202,36 @@ impl fmt::Display for Cookie {
197
202
  }
198
203
  }
199
204
 
205
+ // ===== impl Cookies =====
206
+
207
+ impl TryConvert for Cookies {
208
+ fn try_convert(value: magnus::Value) -> Result<Self, magnus::Error> {
209
+ // try extract uncompressed cookies
210
+ if let Some(rhash) = RHash::from_value(value) {
211
+ let mut cookies = Vec::new();
212
+ rhash.foreach(|name: RString, value: RString| {
213
+ let cookie = format!("{name}={value}");
214
+ let header_value = HeaderValue::from_maybe_shared(Bytes::from(cookie))
215
+ .map_err(header_value_error_to_magnus)?;
216
+ cookies.push(header_value);
217
+ Ok(ForEach::Continue)
218
+ })?;
219
+
220
+ return Ok(Self(cookies));
221
+ }
222
+
223
+ // try extract compressed cookies
224
+ if let Some(cookies) = RString::from_value(value) {
225
+ return Ok(Self(vec![
226
+ HeaderValue::from_maybe_shared(cookies.to_bytes())
227
+ .map_err(header_value_error_to_magnus)?,
228
+ ]));
229
+ }
230
+
231
+ Ok(Self::default())
232
+ }
233
+ }
234
+
200
235
  // ===== impl Jar =====
201
236
 
202
237
  impl Jar {
@@ -221,13 +256,14 @@ impl Jar {
221
256
  }
222
257
 
223
258
  /// Add a cookie to this jar.
224
- pub fn add_cookie(&self, cookie: &Cookie, url: String) {
225
- gvl::nogvl(|| self.0.add(cookie.0.clone(), &url))
226
- }
259
+ pub fn add(&self, cookie: Value, url: String) {
260
+ if let Ok(cookie) = Obj::<Cookie>::try_convert(cookie) {
261
+ gvl::nogvl(|| self.0.add(cookie.0.clone(), &url))
262
+ }
227
263
 
228
- /// Add a cookie str to this jar.
229
- pub fn add_cookie_str(&self, cookie: String, url: String) {
230
- gvl::nogvl(|| self.0.add(cookie.as_ref(), &url))
264
+ if let Ok(cookie_str) = String::try_convert(cookie) {
265
+ gvl::nogvl(|| self.0.add(cookie_str.as_ref(), &url))
266
+ }
231
267
  }
232
268
 
233
269
  /// Remove a cookie from this jar by name and URL.
@@ -268,8 +304,7 @@ pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
268
304
  let jar_class = gem_module.define_class("Jar", ruby.class_object())?;
269
305
  jar_class.define_singleton_method("new", function!(Jar::new, 0))?;
270
306
  jar_class.define_method("get_all", method!(Jar::get_all, 0))?;
271
- jar_class.define_method("add_cookie", method!(Jar::add_cookie, 2))?;
272
- jar_class.define_method("add_cookie_str", method!(Jar::add_cookie_str, 2))?;
307
+ jar_class.define_method("add", method!(Jar::add, 2))?;
273
308
  jar_class.define_method("remove", method!(Jar::remove, 2))?;
274
309
  jar_class.define_method("clear", method!(Jar::clear, 0))?;
275
310