wreq 1.2.5 → 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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +20 -13
  3. data/Cargo.toml +10 -2
  4. data/crates/wreq-util/src/emulate/macros.rs +12 -10
  5. data/crates/wreq-util/src/emulate/profile/chrome/header.rs +18 -3
  6. data/crates/wreq-util/src/emulate/profile/firefox/header.rs +16 -0
  7. data/crates/wreq-util/src/emulate/profile/opera/header.rs +6 -7
  8. data/crates/wreq-util/tests/client.rs +103 -1
  9. data/lib/wreq.rb +70 -45
  10. data/lib/wreq_ruby/body.rb +29 -11
  11. data/lib/wreq_ruby/client.rb +88 -55
  12. data/lib/wreq_ruby/cookie.rb +79 -19
  13. data/lib/wreq_ruby/emulate.rb +74 -6
  14. data/lib/wreq_ruby/error.rb +5 -7
  15. data/lib/wreq_ruby/http.rb +74 -0
  16. data/lib/wreq_ruby/response.rb +3 -0
  17. data/script/build_windows_gnu.ps1 +6 -0
  18. data/src/arch.rs +22 -0
  19. data/src/client/body/form.rs +2 -0
  20. data/src/client/body/json.rs +47 -14
  21. data/src/client/body/stream.rs +147 -43
  22. data/src/client/body.rs +11 -15
  23. data/src/client/param.rs +7 -7
  24. data/src/client/req.rs +87 -47
  25. data/src/client/resp.rs +23 -24
  26. data/src/client.rs +310 -230
  27. data/src/cookie.rs +280 -87
  28. data/src/emulate.rs +85 -50
  29. data/src/error.rs +198 -41
  30. data/src/extractor.rs +16 -76
  31. data/src/header.rs +146 -23
  32. data/src/http.rs +62 -33
  33. data/src/lib.rs +26 -20
  34. data/src/macros.rs +71 -46
  35. data/src/options.rs +284 -0
  36. data/src/rt.rs +22 -11
  37. data/src/serde/de/array_deserializer.rs +48 -0
  38. data/src/serde/de/array_enumerator.rs +59 -0
  39. data/src/serde/de/deserializer.rs +307 -0
  40. data/src/serde/de/enum_deserializer.rs +47 -0
  41. data/src/serde/de/hash_deserializer.rs +89 -0
  42. data/src/serde/de/number_deserializer.rs +51 -0
  43. data/src/serde/de/variant_deserializer.rs +101 -0
  44. data/src/serde/de.rs +33 -0
  45. data/src/serde/error.rs +146 -0
  46. data/src/serde/ser/enums.rs +14 -0
  47. data/src/serde/ser/map_serializer.rs +56 -0
  48. data/src/serde/ser/seq_serializer.rs +71 -0
  49. data/src/serde/ser/serializer.rs +194 -0
  50. data/src/serde/ser/struct_serializer.rs +106 -0
  51. data/src/serde/ser/struct_variant_serializer.rs +44 -0
  52. data/src/serde/ser/tuple_variant_serializer.rs +41 -0
  53. data/src/serde/ser.rs +18 -0
  54. data/src/serde/tests.rs +237 -0
  55. data/src/serde.rs +202 -0
  56. data/test/body_sender_test.rb +246 -0
  57. data/test/cookie_test.rb +211 -10
  58. data/test/json_precision_test.rb +209 -0
  59. data/test/option_validation_test.rb +311 -0
  60. data/test/value_semantics_test.rb +238 -0
  61. metadata +25 -2
  62. data/src/header/helper.rs +0 -112
data/src/header.rs CHANGED
@@ -8,8 +8,6 @@
8
8
  //!
9
9
  //! [RFC 9110 section 5.1]: https://www.rfc-editor.org/rfc/rfc9110.html#section-5.1
10
10
 
11
- mod helper;
12
-
13
11
  use std::cell::RefCell;
14
12
 
15
13
  use bytes::Bytes;
@@ -17,11 +15,12 @@ use http::{HeaderMap, HeaderValue};
17
15
  use magnus::{
18
16
  Error, RArray, RModule, RString, Ruby, TryConvert, Value, function, method,
19
17
  prelude::*,
18
+ scan_args::scan_args,
20
19
  typed_data::{Inspect, Obj},
21
20
  };
22
21
  use wreq::header::OrigHeaderMap;
23
22
 
24
- use crate::error::{header_value_error_to_magnus, type_value_error_to_magnus};
23
+ use crate::error::{header_type_error, header_value_error};
25
24
 
26
25
  use self::helper::{
27
26
  ensure_header_count, from_source, header_count_error, parse_header_name, parse_header_values,
@@ -45,10 +44,11 @@ pub struct OrigHeaders(pub OrigHeaderMap);
45
44
 
46
45
  impl TryConvert for UserAgent {
47
46
  fn try_convert(value: Value) -> Result<Self, Error> {
47
+ let ruby = Ruby::get_with(value);
48
48
  let s = RString::try_convert(value)?;
49
49
  HeaderValue::from_maybe_shared(s.to_bytes())
50
50
  .map(Self)
51
- .map_err(header_value_error_to_magnus)
51
+ .map_err(|err| header_value_error(&ruby, err))
52
52
  }
53
53
  }
54
54
 
@@ -78,11 +78,12 @@ impl Headers {
78
78
  /// A String stores one occurrence, while an Array stores each String as a
79
79
  /// separate occurrence. An empty Array removes the header.
80
80
  pub fn set(&self, name: Value, value: Value) -> Result<(), Error> {
81
+ let ruby = Ruby::get_with(name);
81
82
  let name = parse_header_name(name)?;
82
83
  let values = parse_header_values(value)?;
83
84
  let mut headers = self.0.borrow_mut();
84
85
  let replaced = headers.get_all(&name).iter().count();
85
- ensure_header_count(headers.len(), replaced, values.len())?;
86
+ ensure_header_count(&ruby, headers.len(), replaced, values.len())?;
86
87
 
87
88
  let mut values = values.into_iter();
88
89
  let Some(first) = values.next() else {
@@ -92,11 +93,11 @@ impl Headers {
92
93
 
93
94
  headers
94
95
  .try_insert(name.clone(), first)
95
- .map_err(|_| header_count_error())?;
96
+ .map_err(|_| header_count_error(&ruby))?;
96
97
  for value in values {
97
98
  headers
98
99
  .try_append(name.clone(), value)
99
- .map_err(|_| header_count_error())?;
100
+ .map_err(|_| header_count_error(&ruby))?;
100
101
  }
101
102
  Ok(())
102
103
  }
@@ -105,15 +106,16 @@ impl Headers {
105
106
  ///
106
107
  /// Array elements are appended separately and are never comma-folded.
107
108
  pub fn append(&self, name: Value, value: Value) -> Result<(), Error> {
109
+ let ruby = Ruby::get_with(name);
108
110
  let name = parse_header_name(name)?;
109
111
  let values = parse_header_values(value)?;
110
112
  let mut headers = self.0.borrow_mut();
111
- ensure_header_count(headers.len(), 0, values.len())?;
113
+ ensure_header_count(&ruby, headers.len(), 0, values.len())?;
112
114
 
113
115
  for value in values {
114
116
  headers
115
117
  .try_append(name.clone(), value)
116
- .map_err(|_| header_count_error())?;
118
+ .map_err(|_| header_count_error(&ruby))?;
117
119
  }
118
120
  Ok(())
119
121
  }
@@ -170,18 +172,13 @@ impl Headers {
170
172
  ///
171
173
  /// The optional source may be a Hash, another `Wreq::Headers`, or an
172
174
  /// 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
- }
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)
185
182
  }
186
183
 
187
184
  /// Return a value using Ruby collection semantics.
@@ -268,12 +265,15 @@ impl TryConvert for Headers {
268
265
 
269
266
  impl TryConvert for OrigHeaders {
270
267
  fn try_convert(value: Value) -> Result<Self, Error> {
268
+ let ruby = Ruby::get_with(value);
271
269
  let mut map = OrigHeaderMap::new();
272
270
 
273
271
  let rarray = RArray::from_value(value)
274
- .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"))?;
275
273
 
276
- 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"))?;
277
277
  map.insert(value.to_bytes());
278
278
  }
279
279
 
@@ -281,6 +281,129 @@ impl TryConvert for OrigHeaders {
281
281
  }
282
282
  }
283
283
 
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
+ ));
327
+ }
328
+
329
+ headers.append(pair.entry(0)?, pair.entry(1)?)?;
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")
404
+ }
405
+ }
406
+
284
407
  /// Register `Wreq::Headers` and its native methods with Ruby.
285
408
  pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
286
409
  let headers_class = gem_module.define_class("Headers", ruby.class_object())?;
data/src/http.rs CHANGED
@@ -1,31 +1,38 @@
1
- use magnus::{Error, Module, RModule, Ruby, TryConvert, Value, method, typed_data::Inspect};
1
+ use magnus::{Error, Module, RModule, Ruby, TryConvert, Value, method};
2
2
 
3
+ // Defines constant registration, `into_ffi`/`from_ffi`, and handlers for
4
+ // Ruby's `to_s`, `==`, `eql?`, and `hash` methods.
5
+ // `wreq::Version` only exposes protocol strings through `Debug`, so static
6
+ // labels avoid allocating an intermediate `String`.
3
7
  define_ruby_enum!(
4
8
  /// An HTTP version.
5
- const,
6
9
  Version,
7
10
  "Wreq::Version",
8
11
  wreq::Version,
9
- HTTP_09,
10
- HTTP_10,
11
- HTTP_11,
12
- HTTP_2,
13
- HTTP_3,
12
+ strings:
13
+ HTTP_09 => "HTTP/0.9",
14
+ HTTP_10 => "HTTP/1.0",
15
+ HTTP_11 => "HTTP/1.1",
16
+ HTTP_2 => "HTTP/2.0",
17
+ HTTP_3 => "HTTP/3.0",
14
18
  );
15
19
 
20
+ // Defines constant registration, `into_ffi`/`from_ffi`, and handlers for
21
+ // Ruby's `to_s`, `to_sym`, `==`, `eql?`, and `hash` methods.
16
22
  define_ruby_enum!(
17
23
  /// An HTTP method.
18
24
  Method,
19
25
  "Wreq::Method",
20
26
  wreq::Method,
21
- GET,
22
- HEAD,
23
- POST,
24
- PUT,
25
- DELETE,
26
- OPTIONS,
27
- TRACE,
28
- PATCH,
27
+ symbols:
28
+ GET => "get",
29
+ HEAD => "head",
30
+ POST => "post",
31
+ PUT => "put",
32
+ DELETE => "delete",
33
+ OPTIONS => "options",
34
+ TRACE => "trace",
35
+ PATCH => "patch",
29
36
  );
30
37
 
31
38
  /// HTTP status code.
@@ -33,24 +40,6 @@ define_ruby_enum!(
33
40
  #[magnus::wrap(class = "Wreq::StatusCode", free_immediately, size)]
34
41
  pub struct StatusCode(pub wreq::StatusCode);
35
42
 
36
- // ===== impl Version =====
37
-
38
- impl Version {
39
- /// Convert version to string.
40
- #[inline]
41
- pub fn to_s(&self) -> String {
42
- self.into_ffi().inspect()
43
- }
44
-
45
- /// Value-based equality for Ruby (`==`).
46
- #[inline]
47
- pub fn equals(&self, other: Value) -> bool {
48
- <&Version>::try_convert(other)
49
- .map(|other| *self == *other)
50
- .unwrap_or(false)
51
- }
52
- }
53
-
54
43
  impl TryConvert for Version {
55
44
  fn try_convert(value: magnus::Value) -> Result<Self, magnus::Error> {
56
45
  <&Version>::try_convert(value).cloned()
@@ -101,6 +90,35 @@ impl StatusCode {
101
90
  pub fn to_s(&self) -> String {
102
91
  self.0.to_string()
103
92
  }
93
+
94
+ /// Value-based equality for Ruby (`==`).
95
+ #[inline]
96
+ pub fn equals(&self, other: Value) -> bool {
97
+ <&StatusCode>::try_convert(other)
98
+ .map(|other| *self == *other)
99
+ .unwrap_or(false)
100
+ }
101
+
102
+ /// Strict equality for Ruby (`eql?`).
103
+ #[inline]
104
+ pub fn is_eql(&self, other: Value) -> bool {
105
+ self.equals(other)
106
+ }
107
+
108
+ /// Hash value for Ruby (`hash`).
109
+ #[inline]
110
+ pub fn hash_value(&self) -> u64 {
111
+ use std::hash::{Hash, Hasher};
112
+ let mut hasher = std::collections::hash_map::DefaultHasher::new();
113
+ self.0.hash(&mut hasher);
114
+ hasher.finish()
115
+ }
116
+
117
+ /// Return the status code as an integer (Ruby `to_i`).
118
+ #[inline]
119
+ pub const fn to_i(&self) -> u16 {
120
+ self.0.as_u16()
121
+ }
104
122
  }
105
123
 
106
124
  impl From<wreq::StatusCode> for StatusCode {
@@ -112,11 +130,18 @@ impl From<wreq::StatusCode> for StatusCode {
112
130
  pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
113
131
  let method_class = gem_module.define_class("Method", ruby.class_object())?;
114
132
  Method::define_constants(method_class)?;
133
+ method_class.define_method("to_s", method!(Method::to_s, 0))?;
134
+ method_class.define_method("to_sym", method!(Method::to_sym, 0))?;
135
+ method_class.define_method("==", method!(Method::equals, 1))?;
136
+ method_class.define_method("eql?", method!(Method::is_eql, 1))?;
137
+ method_class.define_method("hash", method!(Method::hash_value, 0))?;
115
138
 
116
139
  let version_class = gem_module.define_class("Version", ruby.class_object())?;
117
140
  Version::define_constants(version_class)?;
118
141
  version_class.define_method("to_s", method!(Version::to_s, 0))?;
119
142
  version_class.define_method("==", method!(Version::equals, 1))?;
143
+ version_class.define_method("eql?", method!(Version::is_eql, 1))?;
144
+ version_class.define_method("hash", method!(Version::hash_value, 0))?;
120
145
 
121
146
  let status_code_class = gem_module.define_class("StatusCode", ruby.class_object())?;
122
147
  status_code_class.define_method("as_int", method!(StatusCode::as_int, 0))?;
@@ -126,6 +151,10 @@ pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
126
151
  status_code_class.define_method("client_error?", method!(StatusCode::is_client_error, 0))?;
127
152
  status_code_class.define_method("server_error?", method!(StatusCode::is_server_error, 0))?;
128
153
  status_code_class.define_method("to_s", method!(StatusCode::to_s, 0))?;
154
+ status_code_class.define_method("==", method!(StatusCode::equals, 1))?;
155
+ status_code_class.define_method("eql?", method!(StatusCode::is_eql, 1))?;
156
+ status_code_class.define_method("hash", method!(StatusCode::hash_value, 0))?;
157
+ status_code_class.define_method("to_i", method!(StatusCode::to_i, 0))?;
129
158
 
130
159
  Ok(())
131
160
  }
data/src/lib.rs CHANGED
@@ -1,3 +1,4 @@
1
+ #![deny(unsafe_code)]
1
2
  #![allow(clippy::wrong_self_convention)]
2
3
 
3
4
  #[macro_use]
@@ -11,67 +12,72 @@ mod extractor;
11
12
  mod gvl;
12
13
  mod header;
13
14
  mod http;
15
+ mod options;
14
16
  mod rt;
17
+ mod serde;
15
18
 
16
19
  use magnus::{Error, Module, Ruby, Value};
17
20
 
18
- use crate::client::{Client, resp::Response};
21
+ use crate::{
22
+ client::{Client, resp::Response},
23
+ http::Method,
24
+ };
19
25
 
20
26
  const RUBY_MODULE_NAME: &str = "Wreq";
21
27
  const VERSION: &str = env!("CARGO_PKG_VERSION");
22
28
 
23
29
  /// Send a HTTP request.
24
30
  #[inline]
25
- pub fn request(args: &[Value]) -> Result<Response, magnus::Error> {
26
- Client::request(&Client::default(), args)
31
+ pub fn request(ruby: &Ruby, args: &[Value]) -> Result<Response, magnus::Error> {
32
+ Client::request_with_default_client(ruby, args)
27
33
  }
28
34
 
29
35
  /// Send a GET request.
30
36
  #[inline]
31
- pub fn get(args: &[Value]) -> Result<Response, magnus::Error> {
32
- Client::get(&Client::default(), args)
37
+ pub fn get(ruby: &Ruby, args: &[Value]) -> Result<Response, magnus::Error> {
38
+ Client::execute_with_default_client(ruby, Method::GET, args)
33
39
  }
34
40
 
35
41
  /// Send a POST request.
36
42
  #[inline]
37
- pub fn post(args: &[Value]) -> Result<Response, magnus::Error> {
38
- Client::post(&Client::default(), args)
43
+ pub fn post(ruby: &Ruby, args: &[Value]) -> Result<Response, magnus::Error> {
44
+ Client::execute_with_default_client(ruby, Method::POST, args)
39
45
  }
40
46
 
41
47
  /// Send a PUT request.
42
48
  #[inline]
43
- pub fn put(args: &[Value]) -> Result<Response, magnus::Error> {
44
- Client::put(&Client::default(), args)
49
+ pub fn put(ruby: &Ruby, args: &[Value]) -> Result<Response, magnus::Error> {
50
+ Client::execute_with_default_client(ruby, Method::PUT, args)
45
51
  }
46
52
 
47
53
  /// Send a DELETE request.
48
54
  #[inline]
49
- pub fn delete(args: &[Value]) -> Result<Response, magnus::Error> {
50
- Client::delete(&Client::default(), args)
55
+ pub fn delete(ruby: &Ruby, args: &[Value]) -> Result<Response, magnus::Error> {
56
+ Client::execute_with_default_client(ruby, Method::DELETE, args)
51
57
  }
52
58
 
53
59
  /// Send a HEAD request.
54
60
  #[inline]
55
- pub fn head(args: &[Value]) -> Result<Response, magnus::Error> {
56
- Client::head(&Client::default(), args)
61
+ pub fn head(ruby: &Ruby, args: &[Value]) -> Result<Response, magnus::Error> {
62
+ Client::execute_with_default_client(ruby, Method::HEAD, args)
57
63
  }
58
64
 
59
65
  /// Send an OPTIONS request.
60
66
  #[inline]
61
- pub fn options(args: &[Value]) -> Result<Response, magnus::Error> {
62
- Client::options(&Client::default(), args)
67
+ pub fn options(ruby: &Ruby, args: &[Value]) -> Result<Response, magnus::Error> {
68
+ Client::execute_with_default_client(ruby, Method::OPTIONS, args)
63
69
  }
64
70
 
65
71
  /// Send a TRACE request.
66
72
  #[inline]
67
- pub fn trace(args: &[Value]) -> Result<Response, magnus::Error> {
68
- Client::trace(&Client::default(), args)
73
+ pub fn trace(ruby: &Ruby, args: &[Value]) -> Result<Response, magnus::Error> {
74
+ Client::execute_with_default_client(ruby, Method::TRACE, args)
69
75
  }
70
76
 
71
77
  /// Send a PATCH request.
72
78
  #[inline]
73
- pub fn patch(args: &[Value]) -> Result<Response, magnus::Error> {
74
- Client::patch(&Client::default(), args)
79
+ pub fn patch(ruby: &Ruby, args: &[Value]) -> Result<Response, magnus::Error> {
80
+ Client::execute_with_default_client(ruby, Method::PATCH, args)
75
81
  }
76
82
 
77
83
  /// wreq ruby binding
@@ -93,6 +99,6 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
93
99
  cookie::include(ruby, &gem_module)?;
94
100
  client::include(ruby, &gem_module)?;
95
101
  emulate::include(ruby, &gem_module)?;
96
- error::include(ruby);
102
+ error::include(ruby, &gem_module)?;
97
103
  Ok(())
98
104
  }
data/src/macros.rs CHANGED
@@ -34,92 +34,116 @@ macro_rules! apply_option {
34
34
  $builder = $builder.$method();
35
35
  }
36
36
  };
37
- (set_if_true_with, $builder:expr, $option:expr, $method:ident, $default:expr, $value:expr) => {
38
- if $option.unwrap_or($default) {
39
- $builder = $builder.$method($value);
40
- }
41
- };
37
+ }
38
+
39
+ /// Convert a Ruby-native option whose field name is also its keyword name.
40
+ macro_rules! extract_native_option {
41
+ ($options:expr, $target:expr, $field:ident) => {{
42
+ $target.$field.set($options.convert(stringify!($field))?);
43
+ }};
44
+ ($options:expr, $target:expr, $field:ident, present) => {{
45
+ $target
46
+ .$field
47
+ .set($options.convert_present(stringify!($field))?);
48
+ }};
49
+ ($options:expr, $target:expr, $field:ident, $source:ty => $map:expr) => {{
50
+ $target
51
+ .$field
52
+ .set($options.convert::<$source>(stringify!($field))?.map($map));
53
+ }};
42
54
  }
43
55
 
44
56
  macro_rules! define_ruby_enum {
45
- ($(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $($variant:ident),* $(,)?) => {
46
- define_ruby_enum!($(#[$meta])* $enum_type, $ruby_class, $ffi_type, $( ($variant, $variant) ),*);
57
+ ($(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, strings: $($variant:ident => $display:expr),* $(,)?) => {
58
+ define_ruby_enum!(@impl $(#[$meta])* $enum_type, $ruby_class, $ffi_type, [$(($variant, $display)),*], []);
59
+ };
60
+
61
+ ($(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, symbols: $($variant:ident => $symbol:expr),* $(,)?) => {
62
+ define_ruby_enum!(@impl $(#[$meta])* $enum_type, $ruby_class, $ffi_type, [$(($variant, stringify!($variant))),*], [$($variant => $symbol),*]);
47
63
  };
48
64
 
49
- ($(#[$meta:meta])* const, $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $($variant:ident),* $(,)?) => {
50
- define_ruby_enum!($(#[$meta])* const, $enum_type, $ruby_class, $ffi_type, $( ($variant, $variant) ),*);
65
+ ($(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $($variant:ident),* $(,)?) => {
66
+ define_ruby_enum!(@impl $(#[$meta])* $enum_type, $ruby_class, $ffi_type, [$(($variant, stringify!($variant))),*], []);
51
67
  };
52
68
 
53
- ($(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $(($rust_variant:ident, $ffi_variant:ident)),* $(,)?) => {
69
+ (@impl $(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, [$(($variant:ident, $display:expr)),*], [$($symbol_variant:ident => $symbol:expr),*]) => {
54
70
  $(#[$meta])*
55
71
  #[magnus::wrap(class = $ruby_class, free_immediately, size)]
56
72
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57
73
  #[allow(non_camel_case_types)]
58
74
  #[allow(clippy::upper_case_acronyms)]
59
75
  pub enum $enum_type {
60
- $($rust_variant),*
76
+ $($variant),*
61
77
  }
62
78
 
63
79
  impl $enum_type {
80
+ /// Return the static text exposed through Ruby's `to_s`.
81
+ #[inline]
82
+ pub const fn to_s(&self) -> &'static str {
83
+ match self {
84
+ $(<$enum_type>::$variant => $display,)*
85
+ }
86
+ }
87
+
88
+ define_ruby_enum!(@to_sym $enum_type, [$($symbol_variant => $symbol),*]);
89
+
90
+ /// Convert this Ruby wrapper into its native enum value.
64
91
  pub fn into_ffi(self) -> $ffi_type {
65
92
  match self {
66
- $(<$enum_type>::$rust_variant => <$ffi_type>::$ffi_variant,)*
93
+ $(<$enum_type>::$variant => <$ffi_type>::$variant,)*
67
94
  }
68
95
  }
69
96
 
97
+ /// Convert a known native enum value into its Ruby wrapper.
70
98
  #[allow(dead_code)]
71
99
  pub fn from_ffi(ffi: $ffi_type) -> Self {
72
100
  #[allow(unreachable_patterns)]
73
101
  match ffi {
74
- $(<$ffi_type>::$ffi_variant => <$enum_type>::$rust_variant,)*
102
+ $(<$ffi_type>::$variant => <$enum_type>::$variant,)*
75
103
  _ => unreachable!(),
76
104
  }
77
105
  }
78
106
 
107
+ /// Register every enum variant as a constant on the Ruby class.
79
108
  pub fn define_constants(class: magnus::RClass) -> Result<(), magnus::Error> {
80
- $(class.const_set(stringify!($rust_variant), <$enum_type>::$rust_variant)?;)*
109
+ $(class.const_set(stringify!($variant), <$enum_type>::$variant)?;)*
81
110
  Ok(())
82
111
  }
83
- }
84
- };
85
-
86
- ($(#[$meta:meta])* const, $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $(($rust_variant:ident, $ffi_variant:ident)),* $(,)?) => {
87
- $(#[$meta])*
88
- #[magnus::wrap(class = $ruby_class, free_immediately, size)]
89
- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90
- #[allow(non_camel_case_types)]
91
- #[allow(clippy::upper_case_acronyms)]
92
- pub enum $enum_type {
93
- $($rust_variant),*
94
- }
95
112
 
96
- impl $enum_type {
97
- pub const fn into_ffi(self) -> $ffi_type {
98
- match self {
99
- $(<$enum_type>::$rust_variant => <$ffi_type>::$ffi_variant,)*
100
- }
113
+ /// Compare two wrapped enum values for Ruby's `==`.
114
+ pub fn equals(&self, other: magnus::Value) -> bool {
115
+ use magnus::TryConvert;
116
+ <&$enum_type>::try_convert(other)
117
+ .map(|other| *self == *other)
118
+ .unwrap_or(false)
101
119
  }
102
120
 
103
- #[allow(dead_code)]
104
- pub const fn from_ffi(ffi: $ffi_type) -> Self {
105
- #[allow(unreachable_patterns)]
106
- match ffi {
107
- $(<$ffi_type>::$ffi_variant => <$enum_type>::$rust_variant,)*
108
- _ => unreachable!(),
109
- }
121
+ /// Compare two wrapped enum values for Ruby's `eql?`.
122
+ pub fn is_eql(&self, other: magnus::Value) -> bool {
123
+ self.equals(other)
110
124
  }
111
125
 
112
- pub fn define_constants(class: magnus::RClass) -> Result<(), magnus::Error> {
113
- $(class.const_set(stringify!($rust_variant), <$enum_type>::$rust_variant)?;)*
114
- Ok(())
126
+ /// Return a hash consistent with Ruby's `eql?` contract.
127
+ pub fn hash_value(&self) -> u64 {
128
+ use std::hash::{Hash, Hasher};
129
+ let mut hasher = std::collections::hash_map::DefaultHasher::new();
130
+ self.hash(&mut hasher);
131
+ hasher.finish()
115
132
  }
116
133
  }
117
134
  };
118
- }
119
135
 
120
- macro_rules! ruby {
121
- () => {
122
- magnus::Ruby::get().expect("Failed to get Ruby VM instance")
136
+ (@to_sym $enum_type:ident, []) => {};
137
+
138
+ (@to_sym $enum_type:ident, [$($variant:ident => $symbol:expr),+]) => {
139
+ /// Return the configured Ruby symbol for this enum value.
140
+ #[inline]
141
+ pub fn to_sym(ruby: &magnus::Ruby, rb_self: &Self) -> magnus::Symbol {
142
+ let name = match *rb_self {
143
+ $(<$enum_type>::$variant => $symbol,)+
144
+ };
145
+ ruby.to_symbol(name)
146
+ }
123
147
  };
124
148
  }
125
149
 
@@ -127,7 +151,8 @@ macro_rules! extract_request {
127
151
  ($args:expr, $required:ty) => {{
128
152
  let args = magnus::scan_args::scan_args::<$required, (), (), (), magnus::RHash, ()>($args)?;
129
153
  let required = args.required;
130
- let request = crate::client::req::Request::new(&ruby!(), args.keywords)?;
154
+ let ruby = magnus::Ruby::get_with(args.keywords);
155
+ let request = crate::client::req::Request::new(&ruby, args.keywords)?;
131
156
  (required, request)
132
157
  }};
133
158
  }