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/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::{
|
|
6
|
+
use crate::{
|
|
7
|
+
error::{interrupt_error, runtime_initialization_error},
|
|
8
|
+
gvl,
|
|
9
|
+
};
|
|
6
10
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|