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.
- checksums.yaml +4 -4
- data/Cargo.lock +20 -13
- data/Cargo.toml +10 -2
- data/crates/wreq-util/src/emulate/macros.rs +12 -10
- data/crates/wreq-util/src/emulate/profile/chrome/header.rs +18 -3
- data/crates/wreq-util/src/emulate/profile/firefox/header.rs +16 -0
- data/crates/wreq-util/src/emulate/profile/opera/header.rs +6 -7
- data/crates/wreq-util/tests/client.rs +103 -1
- data/lib/wreq.rb +70 -45
- data/lib/wreq_ruby/body.rb +29 -11
- data/lib/wreq_ruby/client.rb +88 -55
- data/lib/wreq_ruby/cookie.rb +79 -19
- data/lib/wreq_ruby/emulate.rb +74 -6
- data/lib/wreq_ruby/error.rb +5 -7
- data/lib/wreq_ruby/http.rb +74 -0
- data/lib/wreq_ruby/response.rb +3 -0
- data/script/build_windows_gnu.ps1 +6 -0
- data/src/arch.rs +22 -0
- data/src/client/body/form.rs +2 -0
- data/src/client/body/json.rs +47 -14
- data/src/client/body/stream.rs +147 -43
- data/src/client/body.rs +11 -15
- data/src/client/param.rs +7 -7
- data/src/client/req.rs +87 -47
- data/src/client/resp.rs +23 -24
- data/src/client.rs +310 -230
- data/src/cookie.rs +280 -87
- data/src/emulate.rs +85 -50
- data/src/error.rs +198 -41
- data/src/extractor.rs +16 -76
- data/src/header.rs +146 -23
- data/src/http.rs +62 -33
- data/src/lib.rs +26 -20
- data/src/macros.rs +71 -46
- data/src/options.rs +284 -0
- data/src/rt.rs +22 -11
- data/src/serde/de/array_deserializer.rs +48 -0
- data/src/serde/de/array_enumerator.rs +59 -0
- data/src/serde/de/deserializer.rs +307 -0
- data/src/serde/de/enum_deserializer.rs +47 -0
- data/src/serde/de/hash_deserializer.rs +89 -0
- data/src/serde/de/number_deserializer.rs +51 -0
- data/src/serde/de/variant_deserializer.rs +101 -0
- data/src/serde/de.rs +33 -0
- data/src/serde/error.rs +146 -0
- data/src/serde/ser/enums.rs +14 -0
- data/src/serde/ser/map_serializer.rs +56 -0
- data/src/serde/ser/seq_serializer.rs +71 -0
- data/src/serde/ser/serializer.rs +194 -0
- data/src/serde/ser/struct_serializer.rs +106 -0
- data/src/serde/ser/struct_variant_serializer.rs +44 -0
- data/src/serde/ser/tuple_variant_serializer.rs +41 -0
- data/src/serde/ser.rs +18 -0
- data/src/serde/tests.rs +237 -0
- data/src/serde.rs +202 -0
- data/test/body_sender_test.rb +246 -0
- data/test/cookie_test.rb +211 -10
- data/test/json_precision_test.rb +209 -0
- data/test/option_validation_test.rb +311 -0
- data/test/value_semantics_test.rb +238 -0
- metadata +25 -2
- data/src/header/helper.rs +0 -112
data/src/serde/tests.rs
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
use std::{collections::BTreeMap, fmt};
|
|
2
|
+
|
|
3
|
+
use ::serde::{Deserialize, Serialize, de::Visitor};
|
|
4
|
+
use magnus::{RArray, RHash, RString, Ruby, Value, encoding::EncodingCapable, value::ReprValue};
|
|
5
|
+
|
|
6
|
+
use super::{deserialize_json, deserialize_ruby, serialize};
|
|
7
|
+
|
|
8
|
+
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
|
9
|
+
struct Record {
|
|
10
|
+
count: u64,
|
|
11
|
+
enabled: bool,
|
|
12
|
+
tags: Vec<String>,
|
|
13
|
+
note: Option<String>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
|
17
|
+
struct UnitRecord;
|
|
18
|
+
|
|
19
|
+
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
|
20
|
+
struct NewtypeRecord(u64);
|
|
21
|
+
|
|
22
|
+
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
|
23
|
+
struct TupleRecord(u64, bool, String);
|
|
24
|
+
|
|
25
|
+
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
|
26
|
+
enum State {
|
|
27
|
+
Ready,
|
|
28
|
+
Count(u64),
|
|
29
|
+
Progress(u64, bool),
|
|
30
|
+
Failed { message: String },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Byte sequence that exercises Serde's owned byte-buffer entry points.
|
|
34
|
+
#[derive(Debug, PartialEq)]
|
|
35
|
+
struct ByteBuffer(Vec<u8>);
|
|
36
|
+
|
|
37
|
+
impl Serialize for ByteBuffer {
|
|
38
|
+
fn serialize<Serializer>(
|
|
39
|
+
&self,
|
|
40
|
+
serializer: Serializer,
|
|
41
|
+
) -> Result<Serializer::Ok, Serializer::Error>
|
|
42
|
+
where
|
|
43
|
+
Serializer: ::serde::Serializer,
|
|
44
|
+
{
|
|
45
|
+
serializer.serialize_bytes(&self.0)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
impl<'de> Deserialize<'de> for ByteBuffer {
|
|
50
|
+
fn deserialize<Deserializer>(deserializer: Deserializer) -> Result<Self, Deserializer::Error>
|
|
51
|
+
where
|
|
52
|
+
Deserializer: ::serde::Deserializer<'de>,
|
|
53
|
+
{
|
|
54
|
+
struct ByteBufferVisitor;
|
|
55
|
+
|
|
56
|
+
impl<'de> Visitor<'de> for ByteBufferVisitor {
|
|
57
|
+
type Value = ByteBuffer;
|
|
58
|
+
|
|
59
|
+
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
60
|
+
formatter.write_str("an owned byte buffer")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fn visit_bytes<Error>(self, value: &[u8]) -> Result<Self::Value, Error>
|
|
64
|
+
where
|
|
65
|
+
Error: ::serde::de::Error,
|
|
66
|
+
{
|
|
67
|
+
Ok(ByteBuffer(value.to_vec()))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn visit_byte_buf<Error>(self, value: Vec<u8>) -> Result<Self::Value, Error>
|
|
71
|
+
where
|
|
72
|
+
Error: ::serde::de::Error,
|
|
73
|
+
{
|
|
74
|
+
Ok(ByteBuffer(value))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
deserializer.deserialize_byte_buf(ByteBufferVisitor)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Assert that a supported Serde value survives conversion through Ruby.
|
|
83
|
+
fn assert_ruby_round_trip<Input>(ruby: &Ruby, input: Input) -> Result<(), magnus::Error>
|
|
84
|
+
where
|
|
85
|
+
Input: fmt::Debug + PartialEq + Serialize + for<'de> Deserialize<'de>,
|
|
86
|
+
{
|
|
87
|
+
let value: Value = serialize(ruby, &input)?;
|
|
88
|
+
let output: Input = deserialize_ruby(ruby, value)?;
|
|
89
|
+
assert_eq!(input, output);
|
|
90
|
+
Ok(())
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Assert that a failed conversion preserves Ruby's `TypeError` classification.
|
|
94
|
+
fn assert_type_error(error: magnus::Error, message: &str) {
|
|
95
|
+
let error = error.to_string();
|
|
96
|
+
assert!(error.starts_with("TypeError: "), "{error}");
|
|
97
|
+
assert!(error.contains(message), "{error}");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Verify scalar, option, result, and byte-string conversion behavior.
|
|
101
|
+
fn assert_scalar_conversions(ruby: &Ruby) -> Result<(), magnus::Error> {
|
|
102
|
+
assert_ruby_round_trip(ruby, true)?;
|
|
103
|
+
assert_ruby_round_trip(ruby, 1.25_f32)?;
|
|
104
|
+
assert_ruby_round_trip(ruby, 1.25_f64)?;
|
|
105
|
+
assert_ruby_round_trip(ruby, Option::<u64>::None)?;
|
|
106
|
+
assert_ruby_round_trip(ruby, Some(123_u64))?;
|
|
107
|
+
assert_ruby_round_trip(ruby, Result::<u64, String>::Ok(1234))?;
|
|
108
|
+
assert_ruby_round_trip(ruby, Result::<u64, String>::Err("failed".to_owned()))?;
|
|
109
|
+
|
|
110
|
+
let character: RString = serialize(ruby, &'\u{2603}')?;
|
|
111
|
+
assert_eq!("\u{2603}", character.to_string()?);
|
|
112
|
+
assert!(character.enc_get() == ruby.utf8_encindex());
|
|
113
|
+
let output: char = deserialize_ruby(ruby, character)?;
|
|
114
|
+
assert_eq!('\u{2603}', output);
|
|
115
|
+
|
|
116
|
+
let string: RString = serialize(ruby, &"Hello, world!")?;
|
|
117
|
+
assert_eq!("Hello, world!", string.to_string()?);
|
|
118
|
+
assert!(string.enc_get() == ruby.utf8_encindex());
|
|
119
|
+
assert_eq!(
|
|
120
|
+
"Hello, world!",
|
|
121
|
+
deserialize_ruby::<_, String>(ruby, string)?
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
let bytes = ByteBuffer(b"\0binary\xff".to_vec());
|
|
125
|
+
let value: RString = serialize(ruby, &bytes)?;
|
|
126
|
+
assert_eq!(bytes.0.as_slice(), value.to_bytes().as_ref());
|
|
127
|
+
assert!(value.enc_get() == ruby.ascii8bit_encindex());
|
|
128
|
+
assert_eq!(bytes, deserialize_ruby(ruby, value)?);
|
|
129
|
+
|
|
130
|
+
let none: Value = serialize(ruby, &Option::<u64>::None)?;
|
|
131
|
+
assert!(none.is_nil());
|
|
132
|
+
|
|
133
|
+
let ok: RHash = serialize(ruby, &Result::<u64, String>::Ok(1234))?;
|
|
134
|
+
let value: u64 = ok.aref("Ok")?;
|
|
135
|
+
assert_eq!(1234, value);
|
|
136
|
+
|
|
137
|
+
let error: RHash = serialize(ruby, &Result::<u64, String>::Err("failed".to_owned()))?;
|
|
138
|
+
assert_eq!("failed", error.aref::<_, String>("Err")?);
|
|
139
|
+
Ok(())
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Verify collection, tuple, struct, and enum conversion behavior.
|
|
143
|
+
fn assert_composite_conversions(ruby: &Ruby) -> Result<(), magnus::Error> {
|
|
144
|
+
assert_ruby_round_trip(ruby, ())?;
|
|
145
|
+
assert_ruby_round_trip(ruby, [1_i64, 2, 3])?;
|
|
146
|
+
assert_ruby_round_trip(ruby, (123_i64, true, "tuple".to_owned()))?;
|
|
147
|
+
assert_ruby_round_trip(ruby, UnitRecord)?;
|
|
148
|
+
assert_ruby_round_trip(ruby, NewtypeRecord(123))?;
|
|
149
|
+
assert_ruby_round_trip(ruby, TupleRecord(123, true, "tuple struct".to_owned()))?;
|
|
150
|
+
|
|
151
|
+
let record = Record {
|
|
152
|
+
count: 42,
|
|
153
|
+
enabled: true,
|
|
154
|
+
tags: vec!["ruby".into(), "rust".into()],
|
|
155
|
+
note: Some("present".into()),
|
|
156
|
+
};
|
|
157
|
+
let value: RHash = serialize(ruby, &record)?;
|
|
158
|
+
let count: u64 = value.aref(ruby.to_symbol("count"))?;
|
|
159
|
+
assert_eq!(record.count, count);
|
|
160
|
+
assert_eq!(record, deserialize_ruby(ruby, value)?);
|
|
161
|
+
|
|
162
|
+
let map = BTreeMap::from([("first".to_owned(), 1_u64), ("second".to_owned(), 2)]);
|
|
163
|
+
assert_ruby_round_trip(ruby, map)?;
|
|
164
|
+
|
|
165
|
+
let tuple = (123_i64, true, "tuple".to_owned());
|
|
166
|
+
let value: RArray = serialize(ruby, &tuple)?;
|
|
167
|
+
assert_eq!(3, value.len());
|
|
168
|
+
let first: i64 = value.entry(0)?;
|
|
169
|
+
assert_eq!(123, first);
|
|
170
|
+
|
|
171
|
+
for state in [
|
|
172
|
+
State::Ready,
|
|
173
|
+
State::Count(2),
|
|
174
|
+
State::Progress(3, false),
|
|
175
|
+
State::Failed {
|
|
176
|
+
message: "failed".into(),
|
|
177
|
+
},
|
|
178
|
+
] {
|
|
179
|
+
assert_ruby_round_trip(ruby, state)?;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let count: RHash = serialize(ruby, &State::Count(7))?;
|
|
183
|
+
let value: u64 = count.aref("Count")?;
|
|
184
|
+
assert_eq!(7, value);
|
|
185
|
+
Ok(())
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// Verify exact typed integer conversion in both deserialization modes.
|
|
189
|
+
fn assert_integer_conversions(ruby: &Ruby) -> Result<(), magnus::Error> {
|
|
190
|
+
let value: Value = serialize(ruby, &i128::MIN)?;
|
|
191
|
+
let decimal: String = value.funcall("to_s", ())?;
|
|
192
|
+
assert_eq!(i128::MIN.to_string(), decimal);
|
|
193
|
+
assert_eq!(i128::MIN, deserialize_ruby::<_, i128>(ruby, value)?);
|
|
194
|
+
assert_eq!(i128::MIN, deserialize_json::<_, i128>(ruby, value)?);
|
|
195
|
+
|
|
196
|
+
let value: Value = serialize(ruby, &u128::MAX)?;
|
|
197
|
+
let decimal: String = value.funcall("to_s", ())?;
|
|
198
|
+
assert_eq!(u128::MAX.to_string(), decimal);
|
|
199
|
+
assert_eq!(u128::MAX, deserialize_ruby::<_, u128>(ruby, value)?);
|
|
200
|
+
assert_eq!(u128::MAX, deserialize_json::<_, u128>(ruby, value)?);
|
|
201
|
+
|
|
202
|
+
let value: Value = serialize(ruby, &u64::MAX)?;
|
|
203
|
+
assert_eq!(u64::MAX, deserialize_ruby::<_, u64>(ruby, value)?);
|
|
204
|
+
assert_eq!(u64::MAX, deserialize_json::<_, u64>(ruby, value)?);
|
|
205
|
+
Ok(())
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// Verify unsupported borrowed values and malformed enum shapes return errors.
|
|
209
|
+
fn assert_conversion_errors(ruby: &Ruby) -> Result<(), magnus::Error> {
|
|
210
|
+
let error = deserialize_ruby::<_, &str>(ruby, ruby.str_new("borrowed"))
|
|
211
|
+
.expect_err("borrowed strings must not outlive their Ruby value");
|
|
212
|
+
assert_type_error(error, "expected a borrowed string");
|
|
213
|
+
|
|
214
|
+
let error = deserialize_ruby::<_, &[u8]>(ruby, ruby.str_new("borrowed"))
|
|
215
|
+
.expect_err("borrowed byte slices must not outlive their Ruby value");
|
|
216
|
+
assert_type_error(error, "can't deserialize into byte slice");
|
|
217
|
+
|
|
218
|
+
let variants = ruby.hash_new();
|
|
219
|
+
variants.aset("Ready", ruby.qnil())?;
|
|
220
|
+
variants.aset("Count", 1)?;
|
|
221
|
+
let error = deserialize_ruby::<_, State>(ruby, variants)
|
|
222
|
+
.expect_err("an enum hash must contain exactly one variant");
|
|
223
|
+
assert_type_error(error, "Hash of length 2");
|
|
224
|
+
Ok(())
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[test]
|
|
228
|
+
fn retains_ruby_serde_conversion_surface() -> Result<(), magnus::Error> {
|
|
229
|
+
// SAFETY: this is the only test that initializes the embedded Ruby VM.
|
|
230
|
+
let ruby = unsafe { magnus::embed::init() };
|
|
231
|
+
|
|
232
|
+
assert_scalar_conversions(&ruby)?;
|
|
233
|
+
assert_composite_conversions(&ruby)?;
|
|
234
|
+
assert_integer_conversions(&ruby)?;
|
|
235
|
+
assert_conversion_errors(&ruby)?;
|
|
236
|
+
Ok(())
|
|
237
|
+
}
|
data/src/serde.rs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2022 George Claghorn
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
8
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
9
|
+
so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
//! Serde integration for Magnus with JSON-specific conversion behavior.
|
|
24
|
+
//!
|
|
25
|
+
//! This module is adapted from `serde_magnus` 0.11.0 and retains its generic
|
|
26
|
+
//! Ruby serialization and deserialization surface. Local changes add a JSON
|
|
27
|
+
//! mode with arbitrary-size integer support, finite-float and object-key
|
|
28
|
+
//! validation, insertion-order preservation, and bounded container nesting.
|
|
29
|
+
//! The bridge also avoids the upstream `Ruby::get().unwrap()` error path,
|
|
30
|
+
//! checks iterator and map state explicitly, reports unknown option fields and
|
|
31
|
+
//! conversion paths, and supports `i128` and `u128` in both directions for
|
|
32
|
+
//! typed Rust values.
|
|
33
|
+
#![allow(unsafe_code)]
|
|
34
|
+
|
|
35
|
+
mod de;
|
|
36
|
+
mod error;
|
|
37
|
+
mod ser;
|
|
38
|
+
#[cfg(test)]
|
|
39
|
+
mod tests;
|
|
40
|
+
|
|
41
|
+
use ::serde::{Deserialize, Serialize, de::DeserializeOwned};
|
|
42
|
+
use indexmap::IndexSet;
|
|
43
|
+
use magnus::{IntoValue, Ruby, TryConvert};
|
|
44
|
+
use serde_ignored::Path;
|
|
45
|
+
use serde_path_to_error::{Deserializer as PathDeserializer, Track};
|
|
46
|
+
|
|
47
|
+
use crate::error::argument_error;
|
|
48
|
+
|
|
49
|
+
pub(super) use error::Error;
|
|
50
|
+
|
|
51
|
+
/// Private map key used to carry arbitrary-precision numbers through Serde.
|
|
52
|
+
///
|
|
53
|
+
/// This mirrors the representation used by `serde_json` when its
|
|
54
|
+
/// `arbitrary_precision` feature is enabled. The precision tests protect this
|
|
55
|
+
/// integration point when `serde_json` is updated.
|
|
56
|
+
pub(super) const JSON_NUMBER_TOKEN: &str = "$serde_json::private::Number";
|
|
57
|
+
|
|
58
|
+
/// Maximum nesting accepted while converting a Ruby request value.
|
|
59
|
+
pub(super) const MAX_JSON_NESTING: usize = 100;
|
|
60
|
+
|
|
61
|
+
/// Deserialize a Ruby value using native Ruby data model semantics.
|
|
62
|
+
///
|
|
63
|
+
/// This preserves the public conversion behavior provided by the upstream
|
|
64
|
+
/// `serde_magnus::deserialize` function. JSON callers should use
|
|
65
|
+
/// [`deserialize_json`] when arbitrary-size integer precision is required.
|
|
66
|
+
///
|
|
67
|
+
/// # Errors
|
|
68
|
+
///
|
|
69
|
+
/// Returns the Ruby exception produced when the input cannot be represented by
|
|
70
|
+
/// `Output`.
|
|
71
|
+
#[allow(dead_code)] // Retained as part of the upstream-compatible Serde surface.
|
|
72
|
+
pub(crate) fn deserialize_ruby<'de, Input, Output>(
|
|
73
|
+
ruby: &Ruby,
|
|
74
|
+
input: Input,
|
|
75
|
+
) -> Result<Output, magnus::Error>
|
|
76
|
+
where
|
|
77
|
+
Input: IntoValue,
|
|
78
|
+
Output: Deserialize<'de>,
|
|
79
|
+
{
|
|
80
|
+
de::deserialize_ruby(ruby, input.into_value_with(ruby)).map_err(|error| error.into_magnus(ruby))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Serialize any Serde value into a Ruby value.
|
|
84
|
+
///
|
|
85
|
+
/// This preserves the public conversion behavior provided by the upstream
|
|
86
|
+
/// `serde_magnus::serialize` function.
|
|
87
|
+
///
|
|
88
|
+
/// # Errors
|
|
89
|
+
///
|
|
90
|
+
/// Returns the Ruby exception produced while creating or converting the output
|
|
91
|
+
/// value.
|
|
92
|
+
pub(crate) fn serialize<Input, Output>(ruby: &Ruby, input: &Input) -> Result<Output, magnus::Error>
|
|
93
|
+
where
|
|
94
|
+
Input: Serialize + ?Sized,
|
|
95
|
+
Output: TryConvert,
|
|
96
|
+
{
|
|
97
|
+
let value = ser::serialize(ruby, input).map_err(|error| error.into_magnus(ruby))?;
|
|
98
|
+
Output::try_convert(value)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Deserialize a Ruby value using JSON-specific conversion rules.
|
|
102
|
+
///
|
|
103
|
+
/// Unlike [`deserialize_ruby`], this preserves arbitrary-size integers and
|
|
104
|
+
/// rejects non-finite floats, unsupported object keys, and excessive nesting.
|
|
105
|
+
///
|
|
106
|
+
/// # Errors
|
|
107
|
+
///
|
|
108
|
+
/// Returns the Ruby exception produced when the input cannot be represented by
|
|
109
|
+
/// `Output`.
|
|
110
|
+
pub(crate) fn deserialize_json<'de, Input, Output>(
|
|
111
|
+
ruby: &Ruby,
|
|
112
|
+
input: Input,
|
|
113
|
+
) -> Result<Output, magnus::Error>
|
|
114
|
+
where
|
|
115
|
+
Input: IntoValue,
|
|
116
|
+
Output: Deserialize<'de>,
|
|
117
|
+
{
|
|
118
|
+
de::deserialize_json(ruby, input.into_value_with(ruby)).map_err(|error| error.into_magnus(ruby))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Validate Ruby option keys without inspecting their values.
|
|
122
|
+
///
|
|
123
|
+
/// The derived struct is the only list of accepted keys. Every option field is
|
|
124
|
+
/// optional, so this pass can collect unknown keys and duplicates without
|
|
125
|
+
/// converting or retaining any Ruby value.
|
|
126
|
+
///
|
|
127
|
+
/// # Errors
|
|
128
|
+
///
|
|
129
|
+
/// Returns `ArgumentError` for unknown or duplicate keys and `TypeError` for
|
|
130
|
+
/// keys that are neither Ruby Symbols nor Strings.
|
|
131
|
+
pub(crate) fn validate_option_keys<Input, Output>(
|
|
132
|
+
ruby: &Ruby,
|
|
133
|
+
input: Input,
|
|
134
|
+
) -> Result<(), magnus::Error>
|
|
135
|
+
where
|
|
136
|
+
Input: IntoValue,
|
|
137
|
+
Output: DeserializeOwned,
|
|
138
|
+
{
|
|
139
|
+
let value: magnus::Value = input.into_value_with(ruby);
|
|
140
|
+
let mut unknown = IndexSet::new();
|
|
141
|
+
{
|
|
142
|
+
let mut callback = |path: Path<'_>| {
|
|
143
|
+
if let Path::Map { key, .. } = path {
|
|
144
|
+
unknown.insert(key);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
serde_ignored::deserialize::<_, _, Output>(
|
|
148
|
+
de::Deserializer::new_option_keys(ruby, value),
|
|
149
|
+
&mut callback,
|
|
150
|
+
)
|
|
151
|
+
.map_err(|error| error.into_option_magnus(ruby, None))?;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if unknown.is_empty() {
|
|
155
|
+
return Ok(());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let label = if unknown.len() == 1 {
|
|
159
|
+
"unknown option"
|
|
160
|
+
} else {
|
|
161
|
+
"unknown options"
|
|
162
|
+
};
|
|
163
|
+
let mut message = String::from(label);
|
|
164
|
+
message.push(':');
|
|
165
|
+
for (index, name) in unknown.into_iter().enumerate() {
|
|
166
|
+
if index > 0 {
|
|
167
|
+
message.push(',');
|
|
168
|
+
}
|
|
169
|
+
message.push(' ');
|
|
170
|
+
message.push(':');
|
|
171
|
+
message.push_str(&name);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
Err(argument_error(ruby, message))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Deserialize validated Ruby options and retain the failing field path.
|
|
178
|
+
///
|
|
179
|
+
/// # Errors
|
|
180
|
+
///
|
|
181
|
+
/// Returns the Ruby conversion error with the failing option path attached.
|
|
182
|
+
pub(crate) fn deserialize_options<Input, Output>(
|
|
183
|
+
ruby: &Ruby,
|
|
184
|
+
input: Input,
|
|
185
|
+
) -> Result<Output, magnus::Error>
|
|
186
|
+
where
|
|
187
|
+
Input: IntoValue,
|
|
188
|
+
Output: DeserializeOwned,
|
|
189
|
+
{
|
|
190
|
+
let value = input.into_value_with(ruby);
|
|
191
|
+
let mut track = Track::new();
|
|
192
|
+
let result = Output::deserialize(PathDeserializer::new(
|
|
193
|
+
de::Deserializer::new_ruby(ruby, value),
|
|
194
|
+
&mut track,
|
|
195
|
+
));
|
|
196
|
+
|
|
197
|
+
result.map_err(|error| {
|
|
198
|
+
let path = track.path();
|
|
199
|
+
let path = path.iter().next().is_some().then_some(&path);
|
|
200
|
+
error.into_option_magnus(ruby, path)
|
|
201
|
+
})
|
|
202
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
class BodySenderTest < Minitest::Test
|
|
7
|
+
def test_valid_capacity_creates_open_sender
|
|
8
|
+
sender = Wreq::BodySender.new(2)
|
|
9
|
+
|
|
10
|
+
refute_predicate sender, :closed?
|
|
11
|
+
ensure
|
|
12
|
+
sender&.close
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_omitted_capacity_defaults_to_eight
|
|
16
|
+
sender = Wreq::BodySender.new
|
|
17
|
+
progress = Queue.new
|
|
18
|
+
producer = Thread.new do
|
|
19
|
+
9.times do |index|
|
|
20
|
+
sender.push(index.to_s)
|
|
21
|
+
progress << index
|
|
22
|
+
end
|
|
23
|
+
sender.close
|
|
24
|
+
end
|
|
25
|
+
producer.report_on_exception = false
|
|
26
|
+
|
|
27
|
+
assert wait_until { progress.size >= 8 }, "the default channel should buffer eight chunks"
|
|
28
|
+
assert_equal 8, progress.size
|
|
29
|
+
assert_predicate producer, :alive?, "the ninth push should wait for channel capacity"
|
|
30
|
+
|
|
31
|
+
with_request_body_server do |url, request_thread|
|
|
32
|
+
response = Wreq.post(url, body: sender)
|
|
33
|
+
|
|
34
|
+
assert_equal 200, response.code
|
|
35
|
+
assert producer.join(5), "the ninth push should finish once the request drains"
|
|
36
|
+
assert_equal "012345678", request_thread.value
|
|
37
|
+
end
|
|
38
|
+
ensure
|
|
39
|
+
producer&.kill if producer&.alive?
|
|
40
|
+
producer&.join(5)
|
|
41
|
+
sender&.close
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_non_positive_and_excessive_capacities_raise_argument_error
|
|
45
|
+
[0, -1, 2**256].each do |capacity|
|
|
46
|
+
error = assert_raises(ArgumentError) { Wreq::BodySender.new(capacity) }
|
|
47
|
+
assert_match(/capacity/, error.message)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_non_integer_capacities_raise_type_error
|
|
52
|
+
[1.0, "not an integer"].each do |capacity|
|
|
53
|
+
error = assert_raises(TypeError) { Wreq::BodySender.new(capacity) }
|
|
54
|
+
assert_match(/capacity/, error.message)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_too_many_constructor_arguments_raise_argument_error
|
|
59
|
+
assert_raises(ArgumentError) { Wreq::BodySender.new(1, 2) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_close_is_idempotent_and_updates_closed_state
|
|
63
|
+
sender = Wreq::BodySender.new
|
|
64
|
+
|
|
65
|
+
refute_predicate sender, :closed?
|
|
66
|
+
assert_nil sender.close
|
|
67
|
+
assert_predicate sender, :closed?
|
|
68
|
+
assert_nil sender.close
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_push_after_close_raises_io_error
|
|
72
|
+
sender = Wreq::BodySender.new
|
|
73
|
+
sender.close
|
|
74
|
+
|
|
75
|
+
error = assert_raises(IOError) { sender.push("data") }
|
|
76
|
+
assert_equal "closed body sender", error.message
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_queued_chunks_survive_close_before_request_attachment
|
|
80
|
+
sender = Wreq::BodySender.new(2)
|
|
81
|
+
sender.push("queued-")
|
|
82
|
+
sender.push("body")
|
|
83
|
+
sender.close
|
|
84
|
+
|
|
85
|
+
with_request_body_server do |url, request_thread|
|
|
86
|
+
response = Wreq.post(url, body: sender)
|
|
87
|
+
|
|
88
|
+
assert_equal 200, response.code
|
|
89
|
+
assert_equal "queued-body", request_thread.value
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_valid_capacity_keeps_backpressure_until_request_drains
|
|
94
|
+
sender = Wreq::BodySender.new(1)
|
|
95
|
+
sender.push("first-")
|
|
96
|
+
producer = Thread.new do
|
|
97
|
+
sender.push("second")
|
|
98
|
+
sender.close
|
|
99
|
+
end
|
|
100
|
+
producer.report_on_exception = false
|
|
101
|
+
|
|
102
|
+
sleep 0.05
|
|
103
|
+
assert_predicate producer, :alive?, "the second push should wait for channel capacity"
|
|
104
|
+
|
|
105
|
+
with_request_body_server do |url, request_thread|
|
|
106
|
+
response = Wreq.post(url, body: sender)
|
|
107
|
+
|
|
108
|
+
assert_equal 200, response.code
|
|
109
|
+
assert producer.join(5), "the blocked producer should finish once the request drains"
|
|
110
|
+
assert_equal "first-second", request_thread.value
|
|
111
|
+
end
|
|
112
|
+
ensure
|
|
113
|
+
producer&.kill if producer&.alive?
|
|
114
|
+
producer&.join(5)
|
|
115
|
+
sender&.close
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_receiver_termination_closes_sender
|
|
119
|
+
sender = Wreq::BodySender.new(1)
|
|
120
|
+
sender.push("data")
|
|
121
|
+
|
|
122
|
+
with_reset_server do |url|
|
|
123
|
+
assert_raises(StandardError) { Wreq.post(url, body: sender, timeout: 2) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
assert wait_until { sender.closed? }, "sender should close after the request drops its receiver"
|
|
127
|
+
assert_raises(IOError) { sender.push("more") }
|
|
128
|
+
ensure
|
|
129
|
+
sender&.close
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def test_zero_capacity_regression_exits_subprocess_normally
|
|
133
|
+
lib_dir = File.expand_path("../lib", __dir__)
|
|
134
|
+
script = <<~RUBY
|
|
135
|
+
require "wreq"
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
Wreq::BodySender.new(0)
|
|
139
|
+
rescue ArgumentError => error
|
|
140
|
+
warn "\#{error.class}: \#{error.message}"
|
|
141
|
+
exit 0
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
warn "zero capacity did not raise"
|
|
145
|
+
exit 2
|
|
146
|
+
RUBY
|
|
147
|
+
|
|
148
|
+
_stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", lib_dir, "-e", script)
|
|
149
|
+
|
|
150
|
+
assert status.success?, "subprocess failed with #{status.inspect}: #{stderr}"
|
|
151
|
+
assert_match(/ArgumentError:.*capacity/, stderr)
|
|
152
|
+
refute_match(/panicked|mpsc bounded channel/i, stderr)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def wait_until(timeout: 2)
|
|
158
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
159
|
+
loop do
|
|
160
|
+
return true if yield
|
|
161
|
+
return false if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
162
|
+
|
|
163
|
+
sleep 0.01
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def with_request_body_server
|
|
168
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
169
|
+
port = server.addr[1]
|
|
170
|
+
request_thread = Thread.new do
|
|
171
|
+
socket = server.accept
|
|
172
|
+
begin
|
|
173
|
+
socket.gets
|
|
174
|
+
headers = read_headers(socket)
|
|
175
|
+
body = read_request_body(socket, headers)
|
|
176
|
+
socket.write "HTTP/1.1 200 OK\r\n"
|
|
177
|
+
socket.write "Content-Length: 0\r\n"
|
|
178
|
+
socket.write "Connection: close\r\n\r\n"
|
|
179
|
+
body
|
|
180
|
+
ensure
|
|
181
|
+
socket.close unless socket.closed?
|
|
182
|
+
end
|
|
183
|
+
ensure
|
|
184
|
+
server.close unless server.closed?
|
|
185
|
+
end
|
|
186
|
+
request_thread.report_on_exception = false
|
|
187
|
+
|
|
188
|
+
yield "http://127.0.0.1:#{port}/", request_thread
|
|
189
|
+
ensure
|
|
190
|
+
server&.close unless server&.closed?
|
|
191
|
+
request_thread&.join(5)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def with_reset_server
|
|
195
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
196
|
+
port = server.addr[1]
|
|
197
|
+
thread = Thread.new do
|
|
198
|
+
socket = server.accept
|
|
199
|
+
socket.close
|
|
200
|
+
ensure
|
|
201
|
+
server.close unless server.closed?
|
|
202
|
+
end
|
|
203
|
+
thread.report_on_exception = false
|
|
204
|
+
|
|
205
|
+
yield "http://127.0.0.1:#{port}/"
|
|
206
|
+
ensure
|
|
207
|
+
server&.close unless server&.closed?
|
|
208
|
+
thread&.join(5)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def read_headers(socket)
|
|
212
|
+
headers = {}
|
|
213
|
+
while (line = socket.gets)
|
|
214
|
+
break if line == "\r\n"
|
|
215
|
+
|
|
216
|
+
name, value = line.split(":", 2)
|
|
217
|
+
headers[name.downcase] = value.strip
|
|
218
|
+
end
|
|
219
|
+
headers
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def read_request_body(socket, headers)
|
|
223
|
+
if headers.fetch("transfer-encoding", "").downcase.include?("chunked")
|
|
224
|
+
read_chunked_body(socket)
|
|
225
|
+
else
|
|
226
|
+
socket.read(headers.fetch("content-length", "0").to_i)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def read_chunked_body(socket)
|
|
231
|
+
body = "".b
|
|
232
|
+
loop do
|
|
233
|
+
size_line = socket.gets or raise EOFError, "missing chunk size"
|
|
234
|
+
size = Integer(size_line.split(";", 2).first, 16)
|
|
235
|
+
break if size.zero?
|
|
236
|
+
|
|
237
|
+
body << socket.read(size)
|
|
238
|
+
raise IOError, "missing chunk terminator" unless socket.read(2) == "\r\n"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
while (line = socket.gets)
|
|
242
|
+
break if line == "\r\n"
|
|
243
|
+
end
|
|
244
|
+
body
|
|
245
|
+
end
|
|
246
|
+
end
|