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
@@ -1,16 +1,49 @@
1
- use indexmap::IndexMap;
2
- use serde::{Deserialize, Serialize};
1
+ //! JSON conversion at the Ruby request and response boundary.
2
+ //!
3
+ //! Request values are converted from Ruby into an owned [`Json`] tree before
4
+ //! network I/O. Response bodies are deserialized into the same tree by wreq and
5
+ //! then converted back into Ruby values.
6
+ //!
7
+ //! The underlying `serde_json` configuration preserves object insertion order
8
+ //! and arbitrary-size integer tokens in both directions.
3
9
 
4
- /// Represents a JSON value for HTTP requests.
5
- /// Supports objects, arrays, numbers, strings, booleans, and null.
6
- #[derive(Serialize, Deserialize)]
7
- #[serde(untagged)]
8
- pub enum Json {
9
- Object(IndexMap<String, Json>),
10
- Boolean(bool),
11
- Number(isize),
12
- Float(f64),
13
- String(String),
14
- Null(Option<isize>),
15
- Array(Vec<Json>),
10
+ use ::serde::{Deserialize, Serialize};
11
+ use magnus::{Error, Ruby, TryConvert, Value};
12
+
13
+ use crate::{error::json_serialization_error, serde::deserialize_json};
14
+
15
+ /// An owned JSON tree shared by request and response conversion.
16
+ ///
17
+ /// This wrapper keeps the configured `serde_json::Value` representation private
18
+ /// so callers use the same precision and ordering behavior in both directions.
19
+ #[derive(Deserialize, Serialize)]
20
+ #[serde(transparent)]
21
+ pub struct Json(serde_json::Value);
22
+
23
+ /// Convert supported Ruby request values into an owned JSON tree.
24
+ ///
25
+ /// Supported values are `Hash`, `Array`, `String`, `Symbol`, `Integer`, finite
26
+ /// `Float`, booleans, and `nil`. Hash keys must be strings or symbols. Arrays
27
+ /// and hashes are limited to 100 nesting levels, which also bounds cyclic input.
28
+ /// Unsupported values are reported as `Wreq::BuilderError` before network I/O.
29
+ impl TryConvert for Json {
30
+ fn try_convert(value: Value) -> Result<Self, Error> {
31
+ let ruby = Ruby::get_with(value);
32
+ deserialize_json(&ruby, value)
33
+ .map(Self)
34
+ .map_err(|error| json_serialization_error(&ruby, error))
35
+ }
36
+ }
37
+
38
+ #[cfg(test)]
39
+ mod tests {
40
+ use super::Json;
41
+
42
+ #[test]
43
+ fn preserves_number_precision_and_object_order() {
44
+ let source = br#"{"second":115792089237316195423570985008687907853269984665640564039457584007913129639936,"first":1}"#;
45
+ let json: Json = serde_json::from_slice(source).unwrap();
46
+
47
+ assert_eq!(source, serde_json::to_vec(&json).unwrap().as_slice());
48
+ }
16
49
  }
@@ -1,34 +1,58 @@
1
+ //! Streaming request and response body support.
2
+
1
3
  use std::{
4
+ cell::{Ref, RefCell, RefMut},
5
+ panic::catch_unwind,
2
6
  pin::Pin,
3
- sync::RwLock,
4
7
  task::{Context, Poll},
5
8
  };
6
9
 
7
10
  use bytes::Bytes;
8
11
  use futures_util::{Stream, StreamExt};
9
- use magnus::{Error, RString, TryConvert, Value};
10
- use tokio::sync::{
11
- Mutex,
12
- mpsc::{self},
13
- };
12
+ use magnus::{Error, Integer, RString, Ruby, Value, scan_args::scan_args};
13
+ use tokio::sync::{Mutex, Semaphore, mpsc};
14
14
 
15
15
  use crate::{
16
- error::{memory_error, mpsc_send_error_to_magnus, wreq_error_to_magnus},
16
+ error::{
17
+ argument_error, body_sender_borrow_error, body_sender_borrow_mut_error,
18
+ body_sender_send_error, closed_body_sender_error, memory_error, type_error, wreq_error,
19
+ },
17
20
  rt,
18
21
  };
19
22
 
23
+ /// Number of chunks buffered when Ruby omits the channel capacity.
24
+ const DEFAULT_CHANNEL_CAPACITY: usize = 8;
25
+
20
26
  /// A receiver for streaming HTTP response bodies.
21
27
  pub struct BodyReceiver(Mutex<Pin<Box<dyn Stream<Item = wreq::Result<Bytes>> + Send>>>);
22
28
 
23
- /// A sender for streaming HTTP request bodies.
29
+ /// A bounded producer for a single streaming HTTP request body.
30
+ ///
31
+ /// The receiving side may be attached to one request. The producer remains
32
+ /// writable until [`BodySender::close`] is called or the request drops its
33
+ /// receiver. Ruby's GVL protects state access; no [`RefCell`] borrow is kept
34
+ /// while request backpressure waits without the GVL.
24
35
  #[magnus::wrap(class = "Wreq::BodySender", free_immediately, size)]
25
- pub struct BodySender(RwLock<InnerBodySender>);
36
+ pub struct BodySender(RefCell<InnerBodySender>);
26
37
 
38
+ /// Mutable ownership state for both halves of the body channel.
27
39
  struct InnerBodySender {
40
+ /// Producing side, removed by [`BodySender::close`].
28
41
  tx: Option<mpsc::Sender<Bytes>>,
42
+ /// Receiving side, removed when the sender is attached to a request.
29
43
  rx: Option<mpsc::Receiver<Bytes>>,
30
44
  }
31
45
 
46
+ impl InnerBodySender {
47
+ /// Return whether the channel can no longer accept body chunks.
48
+ fn is_closed(&self) -> bool {
49
+ match &self.tx {
50
+ Some(tx) => tx.is_closed(),
51
+ None => true,
52
+ }
53
+ }
54
+ }
55
+
32
56
  // ===== impl BodyReceiver =====
33
57
 
34
58
  impl BodyReceiver {
@@ -39,8 +63,9 @@ impl BodyReceiver {
39
63
  }
40
64
 
41
65
  /// Read the next body chunk, converting stream errors into Ruby errors.
42
- pub fn next(&self) -> Result<Option<Bytes>, Error> {
66
+ pub fn next(&self, ruby: &Ruby) -> Result<Option<Bytes>, Error> {
43
67
  rt::try_block_on(
68
+ ruby,
44
69
  async {
45
70
  match self.0.lock().await.as_mut().next().await {
46
71
  Some(Ok(data)) => Ok(Some(data)),
@@ -48,7 +73,7 @@ impl BodyReceiver {
48
73
  None => Ok(None),
49
74
  }
50
75
  },
51
- wreq_error_to_magnus,
76
+ wreq_error,
52
77
  )
53
78
  }
54
79
  }
@@ -56,54 +81,133 @@ impl BodyReceiver {
56
81
  // ===== impl BodySender =====
57
82
 
58
83
  impl BodySender {
59
- /// Ruby: `Wreq::Sender.new(capacity = 8)`
60
- pub fn new(args: &[Value]) -> Self {
61
- let capacity: usize = if let Some(v) = args.first() {
62
- usize::try_convert(*v).unwrap_or(8)
63
- } else {
64
- 8
65
- };
66
-
67
- let (tx, rx) = mpsc::channel(capacity);
68
- BodySender(RwLock::new(InnerBodySender {
84
+ /// Create a bounded request-body channel.
85
+ ///
86
+ /// Ruby: `Wreq::BodySender.new(capacity = 8)`. Capacity must be greater
87
+ /// than zero and no larger than [`Semaphore::MAX_PERMITS`].
88
+ ///
89
+ /// # Errors
90
+ ///
91
+ /// Returns `TypeError` for a non-Integer capacity and `ArgumentError` for
92
+ /// an invalid range or argument count.
93
+ pub fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
94
+ let capacity = parse_capacity(ruby, args)?;
95
+
96
+ // Create the Tokio channel without allowing an unwind to cross the Ruby FFI boundary.
97
+ //
98
+ // Known panic conditions are rejected by [`parse_capacity`]. The unwind guard
99
+ // remains as a defensive fallback if Tokio adds another channel invariant.
100
+ let (tx, rx) =
101
+ catch_unwind(|| mpsc::channel(capacity)).map_err(|_| invalid_capacity_error(ruby))?;
102
+
103
+ Ok(BodySender(RefCell::new(InnerBodySender {
69
104
  tx: Some(tx),
70
105
  rx: Some(rx),
71
- }))
106
+ })))
72
107
  }
73
108
 
74
- /// Ruby: `push(data)` where data is String or bytes
75
- pub fn push(rb_self: &Self, data: RString) -> Result<(), Error> {
76
- let bytes = data.to_bytes();
77
- let inner = rb_self.0.read().unwrap();
78
- if let Some(ref tx) = inner.tx {
79
- rt::try_block_on(tx.send(bytes), mpsc_send_error_to_magnus)?;
80
- }
81
- Ok(())
109
+ /// Push a binary chunk, waiting for capacity when the channel is full.
110
+ ///
111
+ /// Ruby: `push(data)` where `data` is a String.
112
+ ///
113
+ /// # Errors
114
+ ///
115
+ /// Returns `IOError` after either channel side has closed. An interrupted
116
+ /// wait raises Ruby's standard `Interrupt` exception.
117
+ pub fn push(ruby: &Ruby, rb_self: &Self, data: RString) -> Result<(), Error> {
118
+ // Clone during the shared borrow, then release it before waiting
119
+ // for capacity. Request attachment needs a mutable borrow.
120
+ let tx = match &rb_self.read_inner(ruby)?.tx {
121
+ Some(tx) if !tx.is_closed() => tx.clone(),
122
+ _ => return Err(closed_body_sender_error(ruby)),
123
+ };
124
+
125
+ rt::try_block_on(ruby, tx.send(data.to_bytes()), body_sender_send_error)
82
126
  }
83
127
 
84
- /// Ruby: `close` to close the sender
85
- pub fn close(&self) {
86
- let mut inner = self.0.write().unwrap();
128
+ /// Close the producing side while retaining the receiver and queued chunks.
129
+ ///
130
+ /// Calling this method more than once has no additional effect.
131
+ ///
132
+ /// # Errors
133
+ ///
134
+ /// Returns `Wreq::BodyError` if the internal state is already borrowed.
135
+ pub fn close(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
136
+ let mut inner = rb_self.write_inner(ruby)?;
87
137
  inner.tx.take();
88
- inner.rx.take();
138
+ Ok(())
89
139
  }
90
- }
91
140
 
92
- impl TryFrom<&BodySender> for ReceiverStream<Bytes> {
93
- type Error = magnus::Error;
141
+ /// Return whether this sender can no longer accept body chunks.
142
+ ///
143
+ /// # Errors
144
+ ///
145
+ /// Returns `Wreq::BodyError` if the internal state is already borrowed.
146
+ pub fn is_closed(ruby: &Ruby, rb_self: &Self) -> Result<bool, Error> {
147
+ rb_self.read_inner(ruby).map(|r| r.is_closed())
148
+ }
94
149
 
95
- fn try_from(sender: &BodySender) -> Result<Self, Self::Error> {
96
- sender
97
- .0
98
- .write()
99
- .unwrap()
150
+ /// Borrow the channel state without panicking on accidental re-entry.
151
+ fn read_inner(&self, ruby: &Ruby) -> Result<Ref<'_, InnerBodySender>, Error> {
152
+ self.0
153
+ .try_borrow()
154
+ .map_err(|err| body_sender_borrow_error(ruby, err))
155
+ }
156
+
157
+ /// Mutably borrow the channel state without panicking on accidental re-entry.
158
+ fn write_inner(&self, ruby: &Ruby) -> Result<RefMut<'_, InnerBodySender>, Error> {
159
+ self.0
160
+ .try_borrow_mut()
161
+ .map_err(|err| body_sender_borrow_mut_error(ruby, err))
162
+ }
163
+
164
+ /// Move the receiving side into one request body.
165
+ ///
166
+ /// # Errors
167
+ ///
168
+ /// Returns `Wreq::MemoryError` if the receiver was already consumed, or
169
+ /// `Wreq::BodyError` if Ruby re-enters while the state is borrowed.
170
+ pub(super) fn take_receiver(&self, ruby: &Ruby) -> Result<ReceiverStream<Bytes>, Error> {
171
+ self.write_inner(ruby)?
100
172
  .rx
101
173
  .take()
102
174
  .map(ReceiverStream::new)
103
- .ok_or_else(memory_error)
175
+ .ok_or_else(|| memory_error(ruby))
104
176
  }
105
177
  }
106
178
 
179
+ /// Parse and validate the optional Ruby channel capacity.
180
+ ///
181
+ /// [`mpsc::channel`] panics for zero or values above
182
+ /// [`Semaphore::MAX_PERMITS`], so validation must finish before channel creation.
183
+ fn parse_capacity(ruby: &Ruby, args: &[Value]) -> Result<usize, Error> {
184
+ let Some(value) = scan_args::<(), (Option<Value>,), (), (), (), ()>(args)?
185
+ .optional
186
+ .0
187
+ else {
188
+ return Ok(DEFAULT_CHANNEL_CAPACITY);
189
+ };
190
+
191
+ let integer = Integer::from_value(value)
192
+ .ok_or_else(|| type_error(ruby, "capacity must be an Integer"))?;
193
+ let capacity = integer
194
+ .to_i64()
195
+ .ok()
196
+ .and_then(|capacity| usize::try_from(capacity).ok())
197
+ .filter(|capacity| (1..=Semaphore::MAX_PERMITS).contains(capacity))
198
+ .ok_or_else(|| invalid_capacity_error(ruby))?;
199
+
200
+ Ok(capacity)
201
+ }
202
+
203
+ /// Build the synchronous Ruby error used for an invalid channel capacity.
204
+ fn invalid_capacity_error(ruby: &Ruby) -> Error {
205
+ argument_error(
206
+ ruby,
207
+ format!("capacity must be between 1 and {}", Semaphore::MAX_PERMITS),
208
+ )
209
+ }
210
+
107
211
  /// A wrapper around [`tokio::sync::mpsc::Receiver`] that implements [`Stream`].
108
212
  pub struct ReceiverStream<T> {
109
213
  inner: mpsc::Receiver<T>,
data/src/client/body.rs CHANGED
@@ -1,6 +1,6 @@
1
- mod form;
2
- mod json;
3
- mod stream;
1
+ pub mod form;
2
+ pub mod json;
3
+ pub mod stream;
4
4
 
5
5
  use bytes::Bytes;
6
6
  use futures_util::StreamExt;
@@ -9,29 +9,24 @@ use magnus::{
9
9
  typed_data::Obj,
10
10
  };
11
11
 
12
- pub use self::{
13
- form::Form,
14
- json::Json,
15
- stream::{BodyReceiver, BodySender, ReceiverStream},
16
- };
17
-
18
12
  /// Represents the body of an HTTP request.
19
13
  /// Supports text, bytes, and streaming bodies (Proc/Enumerator).
20
14
  pub enum Body {
21
15
  /// Static bytes body
22
16
  Bytes(Bytes),
23
17
  /// Streaming body
24
- Stream(ReceiverStream<Bytes>),
18
+ Stream(stream::ReceiverStream<Bytes>),
25
19
  }
26
20
 
27
21
  impl TryConvert for Body {
28
22
  fn try_convert(val: Value) -> Result<Self, Error> {
23
+ let ruby = Ruby::get_with(val);
29
24
  if let Ok(s) = RString::try_convert(val) {
30
25
  return Ok(Body::Bytes(s.to_bytes()));
31
26
  }
32
27
 
33
- let obj = Obj::<BodySender>::try_convert(val)?;
34
- let stream = ReceiverStream::try_from(&*obj)?;
28
+ let obj = Obj::<stream::BodySender>::try_convert(val)?;
29
+ let stream = obj.take_receiver(&ruby)?;
35
30
  Ok(Body::Stream(stream))
36
31
  }
37
32
  }
@@ -50,8 +45,9 @@ impl From<Body> for wreq::Body {
50
45
 
51
46
  pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
52
47
  let sender_class = gem_module.define_class("BodySender", ruby.class_object())?;
53
- sender_class.define_singleton_method("new", function!(BodySender::new, -1))?;
54
- sender_class.define_method("push", method!(BodySender::push, 1))?;
55
- sender_class.define_method("close", magnus::method!(BodySender::close, 0))?;
48
+ sender_class.define_singleton_method("new", function!(stream::BodySender::new, -1))?;
49
+ sender_class.define_method("push", method!(stream::BodySender::push, 1))?;
50
+ sender_class.define_method("close", magnus::method!(stream::BodySender::close, 0))?;
51
+ sender_class.define_method("closed?", magnus::method!(stream::BodySender::is_closed, 0))?;
56
52
  Ok(())
57
53
  }
data/src/client/param.rs CHANGED
@@ -1,19 +1,19 @@
1
+ use ::serde::{Deserialize, Serialize};
1
2
  use indexmap::IndexMap;
2
- use serde::{Deserialize, Serialize};
3
3
 
4
- /// Represents HTTP parameters from Python as either a mapping or a sequence of key-value pairs.
4
+ /// HTTP parameters represented as an insertion-ordered Ruby mapping.
5
5
  pub type Params = IndexMap<String, ParamValue>;
6
6
 
7
- /// Represents a single parameter value that can be automatically converted from Python types.
7
+ /// A scalar Ruby value accepted in query-string and form mappings.
8
8
  #[derive(Serialize, Deserialize)]
9
9
  #[serde(untagged)]
10
10
  pub enum ParamValue {
11
- /// A boolean value from Python `bool`.
11
+ /// A Ruby `true` or `false` value.
12
12
  Boolean(bool),
13
- /// An integer value from Python `int`.
13
+ /// A Ruby Integer that fits in the native pointer-sized range.
14
14
  Number(isize),
15
- /// A floating-point value from Python `float`.
15
+ /// A Ruby Float.
16
16
  Float64(f64),
17
- /// A string value from Python `str`.
17
+ /// A Ruby String or Symbol.
18
18
  String(String),
19
19
  }
data/src/client/req.rs CHANGED
@@ -1,19 +1,21 @@
1
1
  use std::{net::IpAddr, time::Duration};
2
2
 
3
+ use ::serde::Deserialize;
3
4
  use http::header;
4
5
  use magnus::{RHash, TryConvert, typed_data::Obj, value::ReprValue};
5
- use serde::Deserialize;
6
6
  use wreq::{Client, Proxy};
7
7
 
8
- use super::body::{Body, Form, Json};
8
+ use super::body::{Body, form::Form, json::Json};
9
9
  use crate::{
10
+ arch::SUPPORTS_INTERFACE,
10
11
  client::{query::Query, resp::Response},
11
12
  cookie::Cookies,
12
13
  emulate::Emulation,
13
- error::wreq_error_to_magnus,
14
+ error::wreq_error,
14
15
  extractor::Extractor,
15
16
  header::{Headers, OrigHeaders},
16
17
  http::{Method, Version},
18
+ options::{NativeOption, Options},
17
19
  rt,
18
20
  };
19
21
 
@@ -22,12 +24,12 @@ use crate::{
22
24
  #[non_exhaustive]
23
25
  pub struct Request {
24
26
  /// The emulation option for the request.
25
- #[serde(skip)]
26
- emulation: Option<Emulation>,
27
+ #[serde(default)]
28
+ emulation: NativeOption<Emulation>,
27
29
 
28
30
  /// The proxy to use for the request.
29
- #[serde(skip)]
30
- proxy: Option<Proxy>,
31
+ #[serde(default)]
32
+ proxy: NativeOption<Proxy>,
31
33
 
32
34
  /// Bind to a local IP Address.
33
35
  local_address: Option<IpAddr>,
@@ -43,23 +45,23 @@ pub struct Request {
43
45
  read_timeout: Option<u64>,
44
46
 
45
47
  /// The HTTP version to use for the request.
46
- #[serde(skip)]
47
- version: Option<Version>,
48
+ #[serde(default)]
49
+ version: NativeOption<Version>,
48
50
 
49
51
  /// The option enables default headers.
50
52
  default_headers: Option<bool>,
51
53
 
52
54
  /// The headers to use for the request.
53
- #[serde(skip)]
54
- headers: Option<Headers>,
55
+ #[serde(default)]
56
+ headers: NativeOption<Headers>,
55
57
 
56
58
  /// The original headers to use for the request.
57
- #[serde(skip)]
58
- orig_headers: Option<OrigHeaders>,
59
+ #[serde(default)]
60
+ orig_headers: NativeOption<OrigHeaders>,
59
61
 
60
62
  /// The cookies to use for the request.
61
- #[serde(skip)]
62
- cookies: Option<Cookies>,
63
+ #[serde(default)]
64
+ cookies: NativeOption<Cookies>,
63
65
 
64
66
  /// Whether to allow redirects.
65
67
  allow_redirects: Option<bool>,
@@ -95,57 +97,95 @@ pub struct Request {
95
97
  form: Option<Form>,
96
98
 
97
99
  /// The JSON body to use for the request.
98
- json: Option<Json>,
100
+ #[serde(default)]
101
+ json: NativeOption<Json>,
99
102
 
100
103
  /// The body to use for the request.
101
- #[serde(skip)]
102
- body: Option<Body>,
104
+ #[serde(default)]
105
+ body: NativeOption<Body>,
103
106
  }
104
107
 
105
108
  impl Request {
106
109
  /// Create a new [`Request`] from Ruby keyword arguments.
110
+ ///
111
+ /// # Errors
112
+ ///
113
+ /// Returns before network I/O for unknown, duplicate, unsupported,
114
+ /// conflicting, ineffective, or invalid option values.
107
115
  pub fn new(ruby: &magnus::Ruby, hash: RHash) -> Result<Self, magnus::Error> {
108
116
  let keyword = hash.as_value();
109
- let mut builder: Self = serde_magnus::deserialize(ruby, keyword)?;
110
-
111
- if let Some(v) = hash.get(ruby.to_symbol(stringify!(emulation))) {
112
- let obj = Obj::<Emulation>::try_convert(v)?;
113
- builder.emulation = Some((*obj).clone());
114
- }
115
-
116
- if let Some(v) = hash.get(ruby.to_symbol(stringify!(version))) {
117
- builder.version = Some(Version::try_convert(v)?);
118
- }
119
-
120
- if let Some(v) = hash.get(ruby.to_symbol(stringify!(headers))) {
121
- builder.headers = Some(Headers::try_convert(v)?);
122
- }
123
-
124
- if let Some(v) = hash.get(ruby.to_symbol(stringify!(orig_headers))) {
125
- builder.orig_headers = Some(OrigHeaders::try_convert(v)?);
126
- }
127
-
128
- if let Some(v) = hash.get(ruby.to_symbol(stringify!(cookies))) {
129
- builder.cookies = Some(Cookies::try_convert(v)?);
130
- }
131
-
132
- if let Some(v) = hash.get(ruby.to_symbol(stringify!(body))) {
133
- builder.body = Some(Body::try_convert(v)?);
134
- }
135
-
136
- builder.proxy = Extractor::<Proxy>::try_convert(keyword)?.into_inner();
117
+ let options = Options::new(ruby, hash);
118
+ let mut builder = Self::deserialize_options(&options)?;
119
+ options
120
+ .validator()
121
+ .require_when_present(
122
+ stringify!(max_redirects),
123
+ builder.max_redirects.is_some(),
124
+ builder.allow_redirects == Some(true),
125
+ ":allow_redirects to be true",
126
+ )
127
+ .finish()?;
128
+
129
+ extract_native_option!(
130
+ options,
131
+ builder,
132
+ emulation,
133
+ Obj<Emulation> => |value| (*value).clone()
134
+ );
135
+ extract_native_option!(options, builder, version);
136
+ extract_native_option!(options, builder, headers);
137
+ extract_native_option!(options, builder, orig_headers);
138
+ extract_native_option!(options, builder, cookies);
139
+ extract_native_option!(options, builder, json, present);
140
+ builder
141
+ .proxy
142
+ .set(Extractor::<Proxy>::try_convert(keyword)?.into_inner());
143
+ extract_native_option!(options, builder, body);
137
144
 
138
145
  Ok(builder)
139
146
  }
147
+
148
+ /// Validate pre-conversion rules and deserialize the request options.
149
+ ///
150
+ /// # Errors
151
+ ///
152
+ /// Returns `ArgumentError` for failed rules or the Ruby conversion error
153
+ /// produced by an invalid option value.
154
+ fn deserialize_options(options: &Options<'_>) -> Result<Self, magnus::Error> {
155
+ options
156
+ .validate_keys::<Self>()?
157
+ .validator()
158
+ .reject_unsupported(stringify!(interface), SUPPORTS_INTERFACE)
159
+ .reject_conflicts([
160
+ (stringify!(body), options.is_non_nil(stringify!(body))),
161
+ (stringify!(form), options.is_non_nil(stringify!(form))),
162
+ (stringify!(json), options.is_present(stringify!(json))),
163
+ ])
164
+ .reject_conflicts([
165
+ (stringify!(auth), options.is_non_nil(stringify!(auth))),
166
+ (
167
+ stringify!(bearer_auth),
168
+ options.is_non_nil(stringify!(bearer_auth)),
169
+ ),
170
+ (
171
+ stringify!(basic_auth),
172
+ options.is_non_nil(stringify!(basic_auth)),
173
+ ),
174
+ ])
175
+ .finish()?
176
+ .deserialize::<Self>()
177
+ }
140
178
  }
141
179
 
142
180
  pub fn execute_request<U: AsRef<str>>(
181
+ ruby: &magnus::Ruby,
143
182
  client: Client,
144
183
  method: Method,
145
184
  url: U,
146
185
  mut request: Request,
147
186
  ) -> Result<Response, magnus::Error> {
148
187
  rt::try_block_on(
188
+ ruby,
149
189
  async move {
150
190
  let mut builder = client.request(method.into_ffi(), url.as_ref());
151
191
 
@@ -269,6 +309,6 @@ pub fn execute_request<U: AsRef<str>>(
269
309
  // Send request.
270
310
  builder.send().await.map(Response::new)
271
311
  },
272
- wreq_error_to_magnus,
312
+ wreq_error,
273
313
  )
274
314
  }