wreq 1.2.5 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.cargo/config.toml +4 -0
  3. data/Cargo.lock +135 -119
  4. data/Cargo.toml +11 -3
  5. data/Rakefile +6 -0
  6. data/crates/wreq-util/README.md +1 -1
  7. data/crates/wreq-util/examples/emulate.rs +3 -3
  8. data/crates/wreq-util/src/emulate/macros.rs +13 -11
  9. data/crates/wreq-util/src/emulate/profile/chrome/header.rs +18 -3
  10. data/crates/wreq-util/src/emulate/profile/firefox/header.rs +16 -0
  11. data/crates/wreq-util/src/emulate/profile/opera/header.rs +6 -7
  12. data/crates/wreq-util/tests/client.rs +103 -1
  13. data/docs/windows-gnu-tokio-crash.md +49 -3
  14. data/extconf.rb +4 -0
  15. data/lib/wreq.rb +70 -45
  16. data/lib/wreq_ruby/body.rb +29 -11
  17. data/lib/wreq_ruby/client.rb +88 -55
  18. data/lib/wreq_ruby/cookie.rb +79 -19
  19. data/lib/wreq_ruby/emulate.rb +74 -6
  20. data/lib/wreq_ruby/error.rb +5 -7
  21. data/lib/wreq_ruby/http.rb +74 -0
  22. data/lib/wreq_ruby/response.rb +3 -0
  23. data/script/rust_env.rb +85 -0
  24. data/src/arch.rs +22 -0
  25. data/src/client/body/form.rs +2 -0
  26. data/src/client/body/json.rs +47 -14
  27. data/src/client/body/stream.rs +147 -43
  28. data/src/client/body.rs +11 -15
  29. data/src/client/param.rs +7 -7
  30. data/src/client/req.rs +87 -47
  31. data/src/client/resp.rs +23 -24
  32. data/src/client.rs +310 -230
  33. data/src/cookie.rs +280 -87
  34. data/src/emulate.rs +85 -50
  35. data/src/error.rs +198 -41
  36. data/src/extractor.rs +16 -76
  37. data/src/header.rs +146 -23
  38. data/src/http.rs +62 -33
  39. data/src/lib.rs +26 -20
  40. data/src/macros.rs +71 -46
  41. data/src/options.rs +284 -0
  42. data/src/rt.rs +22 -11
  43. data/src/serde/de/array_deserializer.rs +48 -0
  44. data/src/serde/de/array_enumerator.rs +59 -0
  45. data/src/serde/de/deserializer.rs +307 -0
  46. data/src/serde/de/enum_deserializer.rs +47 -0
  47. data/src/serde/de/hash_deserializer.rs +89 -0
  48. data/src/serde/de/number_deserializer.rs +51 -0
  49. data/src/serde/de/variant_deserializer.rs +101 -0
  50. data/src/serde/de.rs +33 -0
  51. data/src/serde/error.rs +146 -0
  52. data/src/serde/ser/enums.rs +14 -0
  53. data/src/serde/ser/map_serializer.rs +56 -0
  54. data/src/serde/ser/seq_serializer.rs +71 -0
  55. data/src/serde/ser/serializer.rs +194 -0
  56. data/src/serde/ser/struct_serializer.rs +106 -0
  57. data/src/serde/ser/struct_variant_serializer.rs +44 -0
  58. data/src/serde/ser/tuple_variant_serializer.rs +41 -0
  59. data/src/serde/ser.rs +18 -0
  60. data/src/serde/tests.rs +237 -0
  61. data/src/serde.rs +202 -0
  62. data/test/body_sender_test.rb +246 -0
  63. data/test/cookie_test.rb +211 -10
  64. data/test/json_precision_test.rb +209 -0
  65. data/test/option_validation_test.rb +311 -0
  66. data/test/value_semantics_test.rb +238 -0
  67. data/wreq.gemspec +1 -1
  68. metadata +28 -4
  69. data/script/build_windows_gnu.ps1 +0 -264
  70. data/src/header/helper.rs +0 -112
data/src/options.rs ADDED
@@ -0,0 +1,284 @@
1
+ //! Parsing and validation for Ruby option hashes.
2
+
3
+ use std::{fmt, marker::PhantomData};
4
+
5
+ use ::serde::{
6
+ Deserialize, Deserializer,
7
+ de::{DeserializeOwned, Visitor},
8
+ };
9
+ use magnus::{Error, RHash, Ruby, TryConvert, Value, value::ReprValue};
10
+
11
+ use crate::{
12
+ error::{argument_error, option_value_error, type_error},
13
+ serde,
14
+ };
15
+
16
+ /// Private Serde newtype used to recognize values converted by Magnus.
17
+ pub(crate) const NATIVE_OPTION_TOKEN: &str = "$wreq::private::NativeOption";
18
+
19
+ /// An option represented in the Serde schema but converted through Magnus.
20
+ ///
21
+ /// Serde records the field as accepted without traversing its Ruby value. Once
22
+ /// the complete option hash has been validated, [`Options::convert`] stores the
23
+ /// converted value here.
24
+ pub(crate) struct NativeOption<T>(Option<T>);
25
+
26
+ impl<T> NativeOption<T> {
27
+ /// Replace the value after converting it through Magnus.
28
+ pub(crate) fn set(&mut self, value: Option<T>) {
29
+ self.0 = value;
30
+ }
31
+
32
+ /// Take the converted value.
33
+ pub(crate) fn take(&mut self) -> Option<T> {
34
+ self.0.take()
35
+ }
36
+ }
37
+
38
+ impl<T> Default for NativeOption<T> {
39
+ fn default() -> Self {
40
+ Self(None)
41
+ }
42
+ }
43
+
44
+ impl<'de, T> Deserialize<'de> for NativeOption<T> {
45
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
46
+ where
47
+ D: Deserializer<'de>,
48
+ {
49
+ struct NativeOptionVisitor<T>(PhantomData<fn() -> T>);
50
+
51
+ impl<T> Visitor<'_> for NativeOptionVisitor<T> {
52
+ type Value = NativeOption<T>;
53
+
54
+ fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55
+ formatter.write_str("a Ruby-native option")
56
+ }
57
+
58
+ fn visit_unit<E>(self) -> Result<Self::Value, E> {
59
+ Ok(NativeOption::default())
60
+ }
61
+ }
62
+
63
+ deserializer
64
+ .deserialize_newtype_struct(NATIVE_OPTION_TOKEN, NativeOptionVisitor(PhantomData))
65
+ }
66
+ }
67
+
68
+ /// A Ruby option hash parsed through its derived Serde schema.
69
+ pub(crate) struct Options<'ruby> {
70
+ ruby: &'ruby Ruby,
71
+ hash: RHash,
72
+ }
73
+
74
+ impl<'ruby> Options<'ruby> {
75
+ /// Parse zero or one Ruby options Hash without copying it.
76
+ ///
77
+ /// # Errors
78
+ ///
79
+ /// Returns `ArgumentError` for extra positional arguments and `TypeError`
80
+ /// when the single argument is not a Hash.
81
+ pub(crate) fn from_args(
82
+ ruby: &'ruby Ruby,
83
+ args: &[Value],
84
+ owner: &str,
85
+ ) -> Result<Option<Self>, Error> {
86
+ magnus::scan_args::scan_args::<(), (Option<Value>,), (), (), (), ()>(args)?
87
+ .optional
88
+ .0
89
+ .map(|value| {
90
+ RHash::from_value(value)
91
+ .map(|hash| Self::new(ruby, hash))
92
+ .ok_or_else(|| type_error(ruby, format!("{owner} options must be a Hash")))
93
+ })
94
+ .transpose()
95
+ }
96
+
97
+ /// Wrap a Ruby Hash without copying its keys or values.
98
+ pub(crate) fn new(ruby: &'ruby Ruby, hash: RHash) -> Self {
99
+ Self { ruby, hash }
100
+ }
101
+
102
+ /// Return the original Ruby Hash as a generic value.
103
+ pub(crate) fn as_value(&self) -> Value {
104
+ self.hash.as_value()
105
+ }
106
+
107
+ /// Validate option keys without reading values, then borrow the options for chaining.
108
+ ///
109
+ /// # Errors
110
+ ///
111
+ /// Returns `ArgumentError` for unknown or duplicate keys and `TypeError`
112
+ /// when a key is neither a Ruby Symbol nor String.
113
+ pub(crate) fn validate_keys<T>(&self) -> Result<&Self, Error>
114
+ where
115
+ T: DeserializeOwned,
116
+ {
117
+ serde::validate_option_keys::<_, T>(self.ruby, self.hash)?;
118
+ Ok(self)
119
+ }
120
+
121
+ /// Deserialize validated option values with field-path error context.
122
+ ///
123
+ /// # Errors
124
+ ///
125
+ /// Returns the Ruby exception produced while converting a known value and
126
+ /// includes its option path in the message.
127
+ pub(crate) fn deserialize<T>(&self) -> Result<T, Error>
128
+ where
129
+ T: DeserializeOwned,
130
+ {
131
+ serde::deserialize_options(self.ruby, self.hash)
132
+ }
133
+
134
+ /// Return whether an option key is present, including a `nil` value.
135
+ pub(crate) fn is_present(&self, name: &str) -> bool {
136
+ get(self.ruby, self.hash, name).is_some()
137
+ }
138
+
139
+ /// Return whether an option is present with a non-nil value.
140
+ pub(crate) fn is_non_nil(&self, name: &str) -> bool {
141
+ get(self.ruby, self.hash, name).is_some_and(|value| !value.is_nil())
142
+ }
143
+
144
+ /// Create a fluent validator for rules involving this option hash.
145
+ pub(crate) fn validator(&self) -> Validator<'_, 'ruby> {
146
+ Validator::new(self)
147
+ }
148
+
149
+ /// Convert a present, non-nil option while retaining its name in errors.
150
+ ///
151
+ /// # Errors
152
+ ///
153
+ /// Returns the Ruby conversion error with the option name added.
154
+ pub(crate) fn convert<T>(&self, name: &str) -> Result<Option<T>, Error>
155
+ where
156
+ T: TryConvert,
157
+ {
158
+ get(self.ruby, self.hash, name)
159
+ .filter(|value| !value.is_nil())
160
+ .map(T::try_convert)
161
+ .transpose()
162
+ .map_err(|error| option_value_error(name, error))
163
+ }
164
+
165
+ /// Convert a present option including `nil`, which may be meaningful to `T`.
166
+ ///
167
+ /// # Errors
168
+ ///
169
+ /// Returns the Ruby conversion error with the option name added.
170
+ pub(crate) fn convert_present<T>(&self, name: &str) -> Result<Option<T>, Error>
171
+ where
172
+ T: TryConvert,
173
+ {
174
+ get(self.ruby, self.hash, name)
175
+ .map(T::try_convert)
176
+ .transpose()
177
+ .map_err(|error| option_value_error(name, error))
178
+ }
179
+ }
180
+
181
+ /// A fluent collection of validation rules for one option hash.
182
+ ///
183
+ /// Rules run in declaration order and later rules become no-ops after the
184
+ /// first failure, preserving a deterministic error priority without building
185
+ /// errors for successful rules.
186
+ #[must_use = "call Validator::finish to observe validation errors"]
187
+ pub(crate) struct Validator<'options, 'ruby> {
188
+ options: &'options Options<'ruby>,
189
+ error: Option<Error>,
190
+ }
191
+
192
+ impl<'options, 'ruby> Validator<'options, 'ruby> {
193
+ /// Create an empty validation chain.
194
+ fn new(options: &'options Options<'ruby>) -> Self {
195
+ Self {
196
+ options,
197
+ error: None,
198
+ }
199
+ }
200
+
201
+ /// Run one rule unless an earlier rule has already failed.
202
+ fn check<F>(mut self, validate: F) -> Self
203
+ where
204
+ F: FnOnce(&Options<'ruby>) -> Result<(), Error>,
205
+ {
206
+ if self.error.is_none() {
207
+ self.error = validate(self.options).err();
208
+ }
209
+ self
210
+ }
211
+
212
+ /// Reject a non-nil option when the current target does not support it.
213
+ pub(crate) fn reject_unsupported(self, name: &str, supported: bool) -> Self {
214
+ self.check(|options| {
215
+ if supported || !options.is_non_nil(name) {
216
+ Ok(())
217
+ } else {
218
+ Err(argument_error(
219
+ options.ruby,
220
+ format!("option :{name} is not supported on this platform"),
221
+ ))
222
+ }
223
+ })
224
+ }
225
+
226
+ /// Reject a group when more than one option has an effective value.
227
+ pub(crate) fn reject_conflicts<const N: usize>(self, options: [(&str, bool); N]) -> Self {
228
+ self.check(|state| {
229
+ if options.iter().filter(|(_, present)| *present).count() < 2 {
230
+ return Ok(());
231
+ }
232
+
233
+ let mut message = String::from("mutually exclusive options: ");
234
+ let mut separator = "";
235
+ for (name, present) in options {
236
+ if present {
237
+ message.push_str(separator);
238
+ message.push(':');
239
+ message.push_str(name);
240
+ separator = ", ";
241
+ }
242
+ }
243
+
244
+ Err(argument_error(state.ruby, message))
245
+ })
246
+ }
247
+
248
+ /// Require a companion setting when an option is present.
249
+ pub(crate) fn require_when_present(
250
+ self,
251
+ option: &str,
252
+ present: bool,
253
+ effective: bool,
254
+ requirement: &str,
255
+ ) -> Self {
256
+ self.check(|state| {
257
+ if !present || effective {
258
+ Ok(())
259
+ } else {
260
+ Err(argument_error(
261
+ state.ruby,
262
+ format!("option :{option} requires {requirement}"),
263
+ ))
264
+ }
265
+ })
266
+ }
267
+
268
+ /// Complete validation and return the source options for further processing.
269
+ ///
270
+ /// # Errors
271
+ ///
272
+ /// Returns the error produced by the first failed rule.
273
+ pub(crate) fn finish(self) -> Result<&'options Options<'ruby>, Error> {
274
+ self.error.map_or(Ok(self.options), Err)
275
+ }
276
+ }
277
+
278
+ /// Return an option by either its Symbol key or equivalent String key.
279
+ ///
280
+ /// The Serde schema rejects a hash containing both forms as a duplicate before
281
+ /// native values are converted.
282
+ pub(crate) fn get(ruby: &Ruby, hash: RHash, name: &str) -> Option<Value> {
283
+ hash.get(ruby.to_symbol(name)).or_else(|| hash.get(name))
284
+ }
data/src/rt.rs CHANGED
@@ -1,16 +1,18 @@
1
1
  use std::sync::LazyLock;
2
2
 
3
+ use magnus::Ruby;
3
4
  use tokio::runtime::{Builder, Runtime};
4
5
 
5
- use crate::{error::interrupt_error, gvl};
6
+ use crate::{
7
+ error::{interrupt_error, runtime_initialization_error},
8
+ gvl,
9
+ };
6
10
 
7
- static RUNTIME: LazyLock<Runtime> = LazyLock::new(|| {
11
+ /// Initialize the global runtime lazily and preserve failures for Ruby.
12
+ static RUNTIME: LazyLock<Result<Runtime, std::io::Error>> = LazyLock::new(|| {
8
13
  let mut builder = Builder::new_multi_thread();
9
14
 
10
- builder
11
- .enable_all()
12
- .build()
13
- .expect("Failed to initialize Tokio runtime")
15
+ builder.enable_all().build()
14
16
  });
15
17
 
16
18
  enum BlockOnError<E> {
@@ -23,13 +25,22 @@ enum BlockOnError<E> {
23
25
  /// The future runs without Ruby's GVL, so it must not construct Ruby objects or
24
26
  /// Ruby exceptions. Convert Rust errors back into Ruby errors after the GVL has
25
27
  /// been reacquired.
26
- pub fn try_block_on<F, T, E, M>(future: F, map_err: M) -> Result<T, magnus::Error>
28
+ ///
29
+ /// # Errors
30
+ ///
31
+ /// Returns `Wreq::BuilderError` if the Tokio runtime cannot be initialized,
32
+ /// Ruby's standard `Interrupt` if Ruby interrupts the request, or the error produced
33
+ /// by `map_err` if the future fails.
34
+ pub fn try_block_on<F, T, E, M>(ruby: &Ruby, future: F, map_err: M) -> Result<T, magnus::Error>
27
35
  where
28
36
  F: Future<Output = Result<T, E>>,
29
- M: FnOnce(E) -> magnus::Error,
37
+ M: FnOnce(&Ruby, E) -> magnus::Error,
30
38
  {
39
+ let runtime = RUNTIME
40
+ .as_ref()
41
+ .map_err(|err| runtime_initialization_error(ruby, err))?;
31
42
  let result = gvl::nogvl_cancellable(|flag| {
32
- RUNTIME.block_on(async move {
43
+ runtime.block_on(async move {
33
44
  tokio::select! {
34
45
  biased;
35
46
  _ = flag.cancelled() => Err(BlockOnError::Interrupted),
@@ -40,7 +51,7 @@ where
40
51
 
41
52
  match result {
42
53
  Ok(value) => Ok(value),
43
- Err(BlockOnError::Interrupted) => Err(interrupt_error()),
44
- Err(BlockOnError::Future(err)) => Err(map_err(err)),
54
+ Err(BlockOnError::Interrupted) => Err(interrupt_error(ruby)),
55
+ Err(BlockOnError::Future(err)) => Err(map_err(ruby, err)),
45
56
  }
46
57
  }
@@ -0,0 +1,48 @@
1
+ use ::serde::de::{DeserializeSeed, SeqAccess};
2
+ use magnus::{RArray, Ruby};
3
+
4
+ use super::{Deserializer, Mode, array_enumerator::ArrayEnumerator};
5
+ use crate::serde::Error;
6
+
7
+ /// Serde sequence access over a Ruby array.
8
+ pub(super) struct ArrayDeserializer<'ruby> {
9
+ ruby: &'ruby Ruby,
10
+ entries: ArrayEnumerator<'ruby>,
11
+ depth: usize,
12
+ mode: Mode,
13
+ }
14
+
15
+ impl<'ruby> ArrayDeserializer<'ruby> {
16
+ /// Create sequence access at the supplied JSON nesting depth.
17
+ pub(super) fn new(ruby: &'ruby Ruby, array: RArray, depth: usize, mode: Mode) -> Self {
18
+ Self {
19
+ ruby,
20
+ entries: ArrayEnumerator::new(ruby, array),
21
+ depth,
22
+ mode,
23
+ }
24
+ }
25
+ }
26
+
27
+ impl<'de> SeqAccess<'de> for ArrayDeserializer<'_> {
28
+ type Error = Error;
29
+
30
+ fn next_element_seed<Seed>(&mut self, seed: Seed) -> Result<Option<Seed::Value>, Self::Error>
31
+ where
32
+ Seed: DeserializeSeed<'de>,
33
+ {
34
+ match self.entries.next() {
35
+ Some(Ok(entry)) => seed
36
+ .deserialize(Deserializer::with_mode(
37
+ self.ruby, entry, self.depth, self.mode,
38
+ ))
39
+ .map(Some),
40
+ Some(Err(error)) => Err(error),
41
+ None => Ok(None),
42
+ }
43
+ }
44
+
45
+ fn size_hint(&self) -> Option<usize> {
46
+ Some(self.entries.remaining())
47
+ }
48
+ }
@@ -0,0 +1,59 @@
1
+ use magnus::{RArray, Ruby, Value};
2
+
3
+ use super::super::Error;
4
+
5
+ /// Index-based Ruby array iterator that avoids Ruby Enumerator fiber overhead.
6
+ pub(super) struct ArrayEnumerator<'ruby> {
7
+ ruby: &'ruby Ruby,
8
+ array: RArray,
9
+ index: usize,
10
+ }
11
+
12
+ impl<'ruby> ArrayEnumerator<'ruby> {
13
+ /// Create an iterator over a Ruby array.
14
+ pub(super) fn new(ruby: &'ruby Ruby, array: RArray) -> Self {
15
+ Self {
16
+ ruby,
17
+ array,
18
+ index: 0,
19
+ }
20
+ }
21
+
22
+ /// Return the number of entries that have not been yielded.
23
+ pub(super) fn remaining(&self) -> usize {
24
+ self.array.len().saturating_sub(self.index)
25
+ }
26
+
27
+ /// Return the current array entry without advancing the iterator.
28
+ fn current(&self) -> Result<Option<Value>, Error> {
29
+ if self.index >= self.array.len() {
30
+ return Ok(None);
31
+ }
32
+
33
+ let index = isize::try_from(self.index).map_err(|_| {
34
+ Error::from(magnus::Error::new(
35
+ self.ruby.exception_range_error(),
36
+ "array index out of range",
37
+ ))
38
+ })?;
39
+ self.array.entry(index).map(Some).map_err(Into::into)
40
+ }
41
+ }
42
+
43
+ impl Iterator for ArrayEnumerator<'_> {
44
+ type Item = Result<Value, Error>;
45
+
46
+ fn next(&mut self) -> Option<Self::Item> {
47
+ match self.current() {
48
+ Ok(Some(value)) => {
49
+ self.index = match self.index.checked_add(1) {
50
+ Some(index) => index,
51
+ None => return Some(Err(Error::message("array index overflow"))),
52
+ };
53
+ Some(Ok(value))
54
+ }
55
+ Ok(None) => None,
56
+ Err(error) => Some(Err(error)),
57
+ }
58
+ }
59
+ }