avrocadabra 0.0.1

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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/Cargo.lock +923 -0
  4. data/Cargo.toml +8 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +131 -0
  7. data/docs/avro_turf.md +61 -0
  8. data/docs/licenses/crates/quad-rand-0.2.3/DECLARED-LICENSE.txt +32 -0
  9. data/docs/licenses/gcc-16.2.0/COPYING +340 -0
  10. data/docs/licenses/gcc-16.2.0/COPYING.RUNTIME +73 -0
  11. data/docs/licenses/mingw-w64-14.0.0/AUTHORS +73 -0
  12. data/docs/licenses/mingw-w64-14.0.0/COPYING +43 -0
  13. data/docs/licenses/rust-1.99.0/COMPILER-BUILTINS.txt +275 -0
  14. data/docs/licenses/rust-1.99.0/COPYRIGHT-library.html +29357 -0
  15. data/docs/licenses/rust-1.99.0/LIBM.txt +258 -0
  16. data/docs/licenses/rust-1.99.0/LLVM-LIBUNWIND.txt +311 -0
  17. data/docs/licenses/rust-1.99.0/RUST-LICENSE-APACHE.txt +176 -0
  18. data/docs/licenses/rust-1.99.0/RUST-LICENSE-MIT.txt +25 -0
  19. data/docs/licenses/rust-1.99.0/STDLIB-BACKTRACE-MIT.txt +25 -0
  20. data/docs/licenses/rust-1.99.0/STDLIB-PORTABLE-SIMD-MIT.txt +19 -0
  21. data/docs/licenses/rust-1.99.0/STDLIB-STDARCH-MIT.txt +25 -0
  22. data/docs/licenses/rust-1.99.0/licenses/Apache-2.0.txt +73 -0
  23. data/docs/licenses/rust-1.99.0/licenses/BSD-2-Clause.txt +9 -0
  24. data/docs/licenses/rust-1.99.0/licenses/CC-BY-SA-4.0.txt +427 -0
  25. data/docs/licenses/rust-1.99.0/licenses/GCC-exception-3.1.txt +30 -0
  26. data/docs/licenses/rust-1.99.0/licenses/GPL-2.0-only.txt +133 -0
  27. data/docs/licenses/rust-1.99.0/licenses/GPL-3.0-or-later.txt +202 -0
  28. data/docs/licenses/rust-1.99.0/licenses/ISC.txt +7 -0
  29. data/docs/licenses/rust-1.99.0/licenses/LLVM-exception.txt +15 -0
  30. data/docs/licenses/rust-1.99.0/licenses/MIT.txt +9 -0
  31. data/docs/licenses/rust-1.99.0/licenses/NCSA.txt +28 -0
  32. data/docs/licenses/rust-1.99.0/licenses/OFL-1.1.txt +43 -0
  33. data/docs/licenses/rust-1.99.0/licenses/Unicode-3.0.txt +39 -0
  34. data/docs/releasing.md +71 -0
  35. data/docs/third_party.md +5922 -0
  36. data/ext/avrocadabra/Cargo.toml +29 -0
  37. data/ext/avrocadabra/build.rs +4 -0
  38. data/ext/avrocadabra/extconf.rb +9 -0
  39. data/ext/avrocadabra/src/big_decimal.rs +107 -0
  40. data/ext/avrocadabra/src/callback.rs +106 -0
  41. data/ext/avrocadabra/src/convert.rs +617 -0
  42. data/ext/avrocadabra/src/decode_value.rs +473 -0
  43. data/ext/avrocadabra/src/guard.rs +680 -0
  44. data/ext/avrocadabra/src/lib.rs +670 -0
  45. data/ext/avrocadabra/src/mapping.rs +63 -0
  46. data/ext/avrocadabra/src/memory.rs +194 -0
  47. data/ext/avrocadabra/src/prepare.rs +819 -0
  48. data/ext/avrocadabra/src/resolution.rs +1932 -0
  49. data/ext/avrocadabra/src/schema_state.rs +60 -0
  50. data/ext/avrocadabra/src/validation.rs +174 -0
  51. data/ext/avrocadabra/src/wire.rs +269 -0
  52. data/lib/avrocadabra/avro_turf/cache.rb +28 -0
  53. data/lib/avrocadabra/avro_turf/codec.rb +69 -0
  54. data/lib/avrocadabra/avro_turf/datum_reader.rb +17 -0
  55. data/lib/avrocadabra/avro_turf/datum_writer.rb +14 -0
  56. data/lib/avrocadabra/avro_turf/mapping.rb +48 -0
  57. data/lib/avrocadabra/avro_turf/messaging.rb +12 -0
  58. data/lib/avrocadabra/avro_turf/ractor_support.rb +154 -0
  59. data/lib/avrocadabra/avro_turf/routing.rb +15 -0
  60. data/lib/avrocadabra/avro_turf/schema_state.rb +70 -0
  61. data/lib/avrocadabra/avro_turf/validation.rb +33 -0
  62. data/lib/avrocadabra/avro_turf.rb +25 -0
  63. data/lib/avrocadabra/logical.rb +71 -0
  64. data/lib/avrocadabra/schema.rb +48 -0
  65. data/lib/avrocadabra/version.rb +5 -0
  66. data/lib/avrocadabra.rb +34 -0
  67. metadata +183 -0
@@ -0,0 +1,617 @@
1
+ use crate::{
2
+ Core, error,
3
+ guard::Limits,
4
+ mapping::{self, Mapping},
5
+ namespace,
6
+ };
7
+ use apache_avro::{
8
+ Decimal, Duration, Schema,
9
+ schema::{InnerDecimalSchema, NamesRef, NamespaceRef},
10
+ types::Value as AvroValue,
11
+ };
12
+ use magnus::{
13
+ Error, RArray, RClass, RHash, RModule, RString, Ruby, TryConvert, Value, prelude::*, value::Id,
14
+ };
15
+ use num_bigint::BigInt;
16
+ use std::collections::HashMap;
17
+
18
+ pub fn encode(
19
+ ruby: &Ruby,
20
+ core: &Core,
21
+ value: Value,
22
+ keys: RArray,
23
+ mapping: Option<Mapping>,
24
+ ) -> Result<AvroValue, Error> {
25
+ let module = namespace(ruby)?;
26
+ let codec = core.prepared.borrow_codec();
27
+ let mut ctx = Encoder {
28
+ ruby,
29
+ names: codec.resolved.get_names(),
30
+ keys,
31
+ fields: &core.fields,
32
+ limits: core.limits,
33
+ path: "$".into(),
34
+ items: 0,
35
+ work: 0,
36
+ bytes: 0,
37
+ logical: module.const_get("Logical")?,
38
+ union: module.const_get("Union")?,
39
+ duration: module.const_get("Duration")?,
40
+ decimal: ruby.class_object().const_get("BigDecimal")?,
41
+ date: ruby.class_object().const_get("Date")?,
42
+ error_class: module.const_get("EncodeError")?,
43
+ hash_key: ruby.intern("key?"),
44
+ hash_read: ruby.intern("[]"),
45
+ };
46
+ ctx.value(codec.schema, None, value, 0, mapping)
47
+ }
48
+
49
+ struct Encoder<'a, 's> {
50
+ ruby: &'a Ruby,
51
+ names: &'a NamesRef<'s>,
52
+ keys: RArray,
53
+ fields: &'a HashMap<String, usize>,
54
+ limits: Limits,
55
+ path: String,
56
+ items: usize,
57
+ work: usize,
58
+ bytes: usize,
59
+ logical: RModule,
60
+ union: RClass,
61
+ duration: RClass,
62
+ decimal: RClass,
63
+ date: RClass,
64
+ error_class: magnus::ExceptionClass,
65
+ hash_key: Id,
66
+ hash_read: Id,
67
+ }
68
+
69
+ impl Encoder<'_, '_> {
70
+ fn field_value(&self, data: RHash, key: Value, symbol: Value) -> Result<Value, Error> {
71
+ let present: bool = data.funcall_public(self.hash_key, (key,))?;
72
+ data.funcall_public(self.hash_read, (if present { key } else { symbol },))
73
+ }
74
+
75
+ fn fail(&self, message: impl AsRef<str>) -> Error {
76
+ error(
77
+ self.ruby,
78
+ "EncodeError",
79
+ format!("{}: {}", self.path, message.as_ref()),
80
+ )
81
+ }
82
+
83
+ fn ruby_result<T>(&self, result: Result<T, Error>) -> Result<T, Error> {
84
+ result.map_err(|e| self.fail(e.to_string()))
85
+ }
86
+
87
+ fn count_bytes(&mut self, n: usize) -> Result<(), Error> {
88
+ self.bytes = self
89
+ .bytes
90
+ .checked_add(n)
91
+ .filter(|&n| n <= self.limits.max_bytes)
92
+ .ok_or_else(|| self.fail("value exceeds max_bytes"))?;
93
+ Ok(())
94
+ }
95
+
96
+ fn integer(&self, value: Value) -> Result<i64, Error> {
97
+ if !value.is_kind_of(self.ruby.class_integer()) {
98
+ return Err(self.fail("expected Integer"));
99
+ }
100
+ self.ruby_result(i64::try_convert(value))
101
+ }
102
+
103
+ fn int32(&self, value: Value) -> Result<i32, Error> {
104
+ i32::try_from(self.integer(value)?).map_err(|_| self.fail("Avro int out of range"))
105
+ }
106
+
107
+ fn float(&self, value: Value) -> Result<f64, Error> {
108
+ let integer = value.is_kind_of(self.ruby.class_integer());
109
+ let decimal = value.is_kind_of(self.decimal);
110
+ if !value.is_kind_of(self.ruby.class_float()) && !integer && !decimal {
111
+ return Err(self.fail("expected Float, Integer or BigDecimal"));
112
+ }
113
+ self.ruby_result(f64::try_convert(value))
114
+ }
115
+
116
+ fn string(&mut self, value: Value) -> Result<String, Error> {
117
+ let value = RString::from_value(value).ok_or_else(|| self.fail("expected UTF-8 String"))?;
118
+ let value: RString = value.funcall_public("encode", ("UTF-8",))?;
119
+ self.count_bytes(value.len())?;
120
+ self.ruby_result(unsafe { value.as_str() }.map(str::to_owned))
121
+ }
122
+
123
+ fn bytes(&mut self, value: Value) -> Result<Vec<u8>, Error> {
124
+ let value =
125
+ RString::from_value(value).ok_or_else(|| self.fail("expected binary String"))?;
126
+ self.count_bytes(value.len())?;
127
+ // Copy before any Ruby allocation or callback can move the source string.
128
+ Ok(unsafe { value.as_slice() }.to_vec())
129
+ }
130
+
131
+ fn resolved<'a>(&self, schema: &'a Schema, ns: NamespaceRef<'_>) -> Result<&'a Schema, Error>
132
+ where
133
+ Self: 'a,
134
+ {
135
+ if let Schema::Ref { name } = schema {
136
+ self.names
137
+ .get(&name.fully_qualified_name(ns))
138
+ .copied()
139
+ .ok_or_else(|| self.fail(format!("unresolved name {}", name.fullname(ns))))
140
+ } else {
141
+ Ok(schema)
142
+ }
143
+ }
144
+
145
+ fn label(&self, schema: &Schema, ns: NamespaceRef<'_>) -> String {
146
+ if let Some(name) = schema.name() {
147
+ return name.fullname(ns);
148
+ }
149
+ match schema {
150
+ Schema::Null => "null",
151
+ Schema::Boolean => "boolean",
152
+ Schema::Int => "int",
153
+ Schema::Long => "long",
154
+ Schema::Float => "float",
155
+ Schema::Double => "double",
156
+ Schema::Bytes | Schema::Decimal(_) | Schema::BigDecimal => "bytes",
157
+ Schema::String | Schema::Uuid(_) => "string",
158
+ Schema::Array(_) => "array",
159
+ Schema::Map(_) => "map",
160
+ Schema::Date | Schema::TimeMillis => "int",
161
+ _ => "long",
162
+ }
163
+ .into()
164
+ }
165
+
166
+ fn accepts(&self, schema: &Schema, ns: NamespaceRef<'_>, value: Value) -> Result<bool, Error> {
167
+ let schema = self.resolved(schema, ns)?;
168
+ Ok(match schema {
169
+ Schema::Null => value.is_nil(),
170
+ Schema::Boolean => {
171
+ value.is_kind_of(self.ruby.class_true_class())
172
+ || value.is_kind_of(self.ruby.class_false_class())
173
+ }
174
+ Schema::Int
175
+ | Schema::Long
176
+ | Schema::TimeMillis
177
+ | Schema::TimeMicros
178
+ | Schema::LocalTimestampMillis
179
+ | Schema::LocalTimestampMicros
180
+ | Schema::LocalTimestampNanos => value.is_kind_of(self.ruby.class_integer()),
181
+ Schema::Float | Schema::Double => {
182
+ value.is_kind_of(self.ruby.class_float())
183
+ || value.is_kind_of(self.ruby.class_integer())
184
+ || value.is_kind_of(self.decimal)
185
+ }
186
+ Schema::String
187
+ | Schema::Bytes
188
+ | Schema::Enum(_)
189
+ | Schema::Fixed(_)
190
+ | Schema::Uuid(_) => RString::from_value(value).is_some(),
191
+ Schema::Array(_) => RArray::from_value(value).is_some(),
192
+ Schema::Map(_) | Schema::Record(_) => RHash::from_value(value).is_some(),
193
+ Schema::Decimal(_) | Schema::BigDecimal => {
194
+ value.is_kind_of(self.decimal)
195
+ || value.is_kind_of(self.ruby.class_integer())
196
+ || value.is_kind_of(self.ruby.class_float())
197
+ }
198
+ Schema::Date => {
199
+ value.is_kind_of(self.date) || value.is_kind_of(self.ruby.class_numeric())
200
+ }
201
+ Schema::TimestampMillis | Schema::TimestampMicros | Schema::TimestampNanos => {
202
+ value.is_kind_of(self.ruby.class_time())
203
+ || value.is_kind_of(self.date)
204
+ || value.is_kind_of(self.ruby.class_numeric())
205
+ }
206
+ Schema::Duration(_) => value.is_kind_of(self.duration),
207
+ _ => false,
208
+ })
209
+ }
210
+
211
+ fn union_index(
212
+ &mut self,
213
+ variants: &[Schema],
214
+ value: Value,
215
+ mapping: Mapping,
216
+ depth: usize,
217
+ ) -> Result<usize, Error> {
218
+ for (index, schema) in variants.iter().enumerate() {
219
+ if !matches!(
220
+ schema,
221
+ Schema::Null
222
+ | Schema::Boolean
223
+ | Schema::Int
224
+ | Schema::Long
225
+ | Schema::Float
226
+ | Schema::Double
227
+ | Schema::String
228
+ | Schema::Bytes
229
+ ) || !mapping.child(index)?.identity()?
230
+ {
231
+ let budget = self.ruby.ary_from_vec(vec![
232
+ self.limits.max_items.saturating_sub(self.work),
233
+ self.limits.max_depth.saturating_sub(depth) + 1,
234
+ ]);
235
+ let index = mapping.union_index(value, budget)?;
236
+ self.work = self.limits.max_items - budget.entry::<usize>(0)?;
237
+ return Ok(index);
238
+ }
239
+ }
240
+ for (index, schema) in variants.iter().enumerate() {
241
+ if self.accepts(schema, None, value)? {
242
+ let valid = match schema {
243
+ Schema::Int => self
244
+ .integer(value)
245
+ .is_ok_and(|value| i32::try_from(value).is_ok()),
246
+ Schema::Long => self.integer(value).is_ok(),
247
+ _ => true,
248
+ };
249
+ if valid {
250
+ return Ok(index);
251
+ }
252
+ }
253
+ }
254
+ Err(mapping.encoding_error(value)?)
255
+ }
256
+
257
+ fn value(
258
+ &mut self,
259
+ schema: &Schema,
260
+ ns: NamespaceRef<'_>,
261
+ value: Value,
262
+ depth: usize,
263
+ mapping: Option<Mapping>,
264
+ ) -> Result<AvroValue, Error> {
265
+ if let Schema::Ref { name } = schema {
266
+ let full = name.fully_qualified_name(ns);
267
+ let schema = self
268
+ .names
269
+ .get(&full)
270
+ .copied()
271
+ .ok_or_else(|| self.fail("unresolved named schema"))?;
272
+ return self.value(schema, full.namespace(), value, depth, mapping);
273
+ }
274
+ let value = match mapping {
275
+ Some(mapping) => mapping.convert("encode", value)?,
276
+ None => value,
277
+ };
278
+ match self.physical(schema, ns, value, depth, mapping) {
279
+ Err(error) if error.is_kind_of(self.error_class) && mapping.is_some() => {
280
+ Err(mapping.unwrap().encoding_error(value)?)
281
+ }
282
+ result => result,
283
+ }
284
+ }
285
+
286
+ fn physical(
287
+ &mut self,
288
+ schema: &Schema,
289
+ ns: NamespaceRef<'_>,
290
+ value: Value,
291
+ depth: usize,
292
+ mapping: Option<Mapping>,
293
+ ) -> Result<AvroValue, Error> {
294
+ if depth > self.limits.max_depth {
295
+ return Err(self.fail("value exceeds maximum depth"));
296
+ }
297
+ self.items += 1;
298
+ if self.items > self.limits.max_items {
299
+ return Err(self.fail("value exceeds maximum item count"));
300
+ }
301
+ self.work += 1;
302
+ if self.work > self.limits.max_items {
303
+ return Err(self.fail("union search exceeds maximum item count"));
304
+ }
305
+ match schema {
306
+ Schema::Null if value.is_nil() => Ok(AvroValue::Null),
307
+ Schema::Boolean
308
+ if value.is_kind_of(self.ruby.class_true_class())
309
+ || value.is_kind_of(self.ruby.class_false_class()) =>
310
+ {
311
+ Ok(AvroValue::Boolean(bool::try_convert(value)?))
312
+ }
313
+ Schema::Int => Ok(AvroValue::Int(self.int32(value)?)),
314
+ Schema::Long => Ok(AvroValue::Long(self.integer(value)?)),
315
+ Schema::Float => Ok(AvroValue::Float(self.float(value)? as f32)),
316
+ Schema::Double => Ok(AvroValue::Double(self.float(value)?)),
317
+ Schema::String => Ok(AvroValue::String(self.string(value)?)),
318
+ Schema::Bytes => Ok(AvroValue::Bytes(self.bytes(value)?)),
319
+ Schema::Fixed(fixed) => {
320
+ let bytes = self.bytes(value)?;
321
+ if bytes.len() != fixed.size {
322
+ return Err(self.fail(format!("fixed requires {} bytes", fixed.size)));
323
+ }
324
+ Ok(AvroValue::Fixed(fixed.size, bytes))
325
+ }
326
+ Schema::Enum(enumeration) => {
327
+ let index = match mapping {
328
+ Some(mapping) => mapping.enum_index(value)?,
329
+ None => {
330
+ let symbol = RString::from_value(value)
331
+ .ok_or_else(|| self.fail("expected enum String"))?;
332
+ if symbol.enc_coderange_scan() != magnus::encoding::Coderange::SevenBit {
333
+ return Err(self.fail("unknown enum symbol"));
334
+ }
335
+ enumeration.symbols.iter().position(|candidate| {
336
+ (unsafe { symbol.as_slice() }) == candidate.as_bytes()
337
+ })
338
+ }
339
+ }
340
+ .ok_or_else(|| self.fail("unknown enum symbol"))?;
341
+ let symbol = enumeration
342
+ .symbols
343
+ .get(index)
344
+ .ok_or_else(|| self.fail("enum index out of range"))?;
345
+ self.count_bytes(symbol.len())?;
346
+ Ok(AvroValue::Enum(index as u32, symbol.clone()))
347
+ }
348
+ Schema::Array(array) => {
349
+ let values =
350
+ RArray::from_value(value).ok_or_else(|| self.fail("expected Array"))?;
351
+ let length: usize = values.funcall_public("size", ())?;
352
+ if length > self.limits.max_items - self.items {
353
+ return Err(self.fail("array exceeds maximum item count"));
354
+ }
355
+ let mut output = Vec::with_capacity(length);
356
+ let child = mapping::child(mapping, 0)?;
357
+ if length == 0 {
358
+ return Ok(AvroValue::Array(output));
359
+ }
360
+ crate::callback::each(self.ruby, value, |args| {
361
+ let n = self.path.len();
362
+ crate::push_index(&mut self.path, output.len());
363
+ output.push(self.value(
364
+ &array.items,
365
+ ns,
366
+ args.first().copied().unwrap_or(self.ruby.qnil().as_value()),
367
+ depth + 1,
368
+ child,
369
+ )?);
370
+ self.path.truncate(n);
371
+ Ok(())
372
+ })?;
373
+ Ok(AvroValue::Array(output))
374
+ }
375
+ Schema::Map(map) => {
376
+ let values = RHash::from_value(value).ok_or_else(|| self.fail("expected Hash"))?;
377
+ let length: usize = values.funcall_public("size", ())?;
378
+ if length > self.limits.max_items - self.items {
379
+ return Err(self.fail("map exceeds maximum item count"));
380
+ }
381
+ let mut output = Vec::with_capacity(length);
382
+ let child = mapping::child(mapping, 0)?;
383
+ if length == 0 {
384
+ return Ok(AvroValue::Record(output));
385
+ }
386
+ crate::callback::each(self.ruby, value, |args| {
387
+ let pair;
388
+ let args = if args.len() == 1 {
389
+ pair = RArray::try_convert(args[0])?;
390
+ &[pair.entry(0)?, pair.entry(1)?]
391
+ } else {
392
+ args
393
+ };
394
+ let key = args.first().copied().unwrap_or(self.ruby.qnil().as_value());
395
+ let value = args.get(1).copied().unwrap_or(self.ruby.qnil().as_value());
396
+ let key = self.string(key)?;
397
+ let n = self.path.len();
398
+ self.path.push_str(&format!("[{key:?}]"));
399
+ output.push((key, self.value(&map.types, ns, value, depth + 1, child)?));
400
+ self.path.truncate(n);
401
+ Ok(())
402
+ })?;
403
+ Ok(AvroValue::Record(output))
404
+ }
405
+ Schema::Record(record) => {
406
+ let data =
407
+ RHash::from_value(value).ok_or_else(|| self.fail("expected record Hash"))?;
408
+ let full = record.name.fully_qualified_name(ns);
409
+ if record.fields.len() > self.limits.max_items - self.items {
410
+ return Err(self.fail("record exceeds maximum item count"));
411
+ }
412
+ let mut output = Vec::with_capacity(record.fields.len());
413
+ for (field_index, field) in record.fields.iter().enumerate() {
414
+ self.count_bytes(field.name.len())?;
415
+ let n = self.path.len();
416
+ self.path.push('.');
417
+ self.path.push_str(&field.name);
418
+ let index = self
419
+ .fields
420
+ .get(&field.name)
421
+ .ok_or_else(|| self.fail("missing prepared field"))?
422
+ * 2;
423
+ let key: Value = self.keys.entry(index as isize)?;
424
+ let symbol: Value = self.keys.entry(index as isize + 1)?;
425
+ let value = self.field_value(data, key, symbol)?;
426
+ let value = self.value(
427
+ &field.schema,
428
+ full.namespace(),
429
+ value,
430
+ depth + 1,
431
+ mapping::child(mapping, field_index)?,
432
+ )?;
433
+ output.push((field.name.clone(), value));
434
+ self.path.truncate(n);
435
+ }
436
+ Ok(AvroValue::Record(output))
437
+ }
438
+ Schema::Union(union) => {
439
+ let datum;
440
+ let index = if value.is_kind_of(self.union) {
441
+ let branch: Value = value.funcall("branch", ())?;
442
+ datum = value.funcall("value", ())?;
443
+ if branch.is_kind_of(self.ruby.class_integer()) {
444
+ self.ruby_result(usize::try_convert(branch))?
445
+ } else {
446
+ let branch: String = self.ruby_result(branch.funcall("to_s", ()))?;
447
+ union
448
+ .variants()
449
+ .iter()
450
+ .position(|s| self.label(s, ns) == branch)
451
+ .ok_or_else(|| self.fail(format!("unknown union branch {branch}")))?
452
+ }
453
+ } else if let Some(mapping) = mapping {
454
+ datum = value;
455
+ self.union_index(union.variants(), value, mapping, depth)?
456
+ } else {
457
+ let (items, bytes, path_len) = (self.items, self.bytes, self.path.len());
458
+ let mut failure = None;
459
+ for (i, schema) in union.variants().iter().enumerate() {
460
+ if self.accepts(schema, ns, value)? {
461
+ match self.value(schema, ns, value, depth, None) {
462
+ Ok(value) => {
463
+ return Ok(AvroValue::Union(i as u32, Box::new(value)));
464
+ }
465
+ Err(error) => {
466
+ if self.work > self.limits.max_items {
467
+ return Err(error);
468
+ }
469
+ failure = Some(error.to_string());
470
+ }
471
+ }
472
+ self.items = items;
473
+ self.bytes = bytes;
474
+ self.path.truncate(path_len);
475
+ }
476
+ }
477
+ return Err(self.fail(
478
+ failure
479
+ .as_deref()
480
+ .unwrap_or("no union branch matches this Ruby value"),
481
+ ));
482
+ };
483
+ let schema = union
484
+ .variants()
485
+ .get(index)
486
+ .ok_or_else(|| self.fail("union branch index out of range"))?;
487
+ Ok(AvroValue::Union(
488
+ index as u32,
489
+ Box::new(self.value(
490
+ schema,
491
+ ns,
492
+ datum,
493
+ depth,
494
+ mapping::child(mapping, index)?,
495
+ )?),
496
+ ))
497
+ }
498
+ Schema::Decimal(decimal) => {
499
+ let unscaled: String = self.ruby_result(self.logical.funcall(
500
+ "decimal_unscaled",
501
+ (value, decimal.precision, decimal.scale),
502
+ ))?;
503
+ let integer = BigInt::parse_bytes(unscaled.as_bytes(), 10)
504
+ .ok_or_else(|| self.fail("invalid decimal"))?;
505
+ let mut bytes = integer.to_signed_bytes_be();
506
+ if let InnerDecimalSchema::Fixed(fixed) = &decimal.inner {
507
+ if bytes.len() > fixed.size {
508
+ return Err(self.fail("decimal overflows fixed size"));
509
+ }
510
+ self.count_bytes(fixed.size)?;
511
+ let mut padded = vec![
512
+ if integer.sign() == num_bigint::Sign::Minus {
513
+ 255
514
+ } else {
515
+ 0
516
+ };
517
+ fixed.size - bytes.len()
518
+ ];
519
+ padded.append(&mut bytes);
520
+ bytes = padded;
521
+ } else {
522
+ self.count_bytes(bytes.len())?;
523
+ }
524
+ Ok(AvroValue::Decimal(Decimal::from(bytes)))
525
+ }
526
+ Schema::BigDecimal => {
527
+ let parts: RArray =
528
+ self.ruby_result(self.logical.funcall("big_decimal_parts", (value,)))?;
529
+ let coefficient: RString = parts.entry(0)?;
530
+ if coefficient.len() > self.limits.max_bytes.saturating_mul(3) {
531
+ return Err(self.fail("big-decimal exceeds max_bytes"));
532
+ }
533
+ let scale: i64 = self.ruby_result(parts.entry(1))?;
534
+ let coefficient = self.ruby_result(unsafe { coefficient.as_str() })?;
535
+ let coefficient = apache_avro::BigDecimal::parse_bytes(coefficient.as_bytes(), 10)
536
+ .ok_or_else(|| self.fail("invalid big-decimal coefficient"))?
537
+ .into_bigint_and_scale()
538
+ .0;
539
+ self.count_bytes((coefficient.bits() / 8 + 1) as usize)?;
540
+ Ok(AvroValue::BigDecimal(apache_avro::BigDecimal::new(
541
+ coefficient,
542
+ scale,
543
+ )))
544
+ }
545
+ Schema::Date => {
546
+ let days: Value = self.ruby_result(self.logical.funcall("date_days", (value,)))?;
547
+ Ok(AvroValue::Date(self.int32(days)?))
548
+ }
549
+ Schema::TimeMillis => {
550
+ let ticks = self.int32(value)?;
551
+ if !(0..86_400_000).contains(&ticks) {
552
+ return Err(self.fail("time-millis must be within a day"));
553
+ }
554
+ Ok(AvroValue::TimeMillis(ticks))
555
+ }
556
+ Schema::TimeMicros => {
557
+ let ticks = self.integer(value)?;
558
+ if !(0..86_400_000_000).contains(&ticks) {
559
+ return Err(self.fail("time-micros must be within a day"));
560
+ }
561
+ Ok(AvroValue::TimeMicros(ticks))
562
+ }
563
+ Schema::TimestampMillis | Schema::TimestampMicros | Schema::TimestampNanos => {
564
+ let units = match schema {
565
+ Schema::TimestampMillis => 1000,
566
+ Schema::TimestampMicros => 1_000_000,
567
+ _ => 1_000_000_000,
568
+ };
569
+ let ticks: Value =
570
+ self.ruby_result(self.logical.funcall("timestamp_ticks", (value, units)))?;
571
+ let ticks = self.integer(ticks)?;
572
+ Ok(match schema {
573
+ Schema::TimestampMillis => AvroValue::TimestampMillis(ticks),
574
+ Schema::TimestampMicros => AvroValue::TimestampMicros(ticks),
575
+ _ => AvroValue::TimestampNanos(ticks),
576
+ })
577
+ }
578
+ Schema::LocalTimestampMillis => {
579
+ Ok(AvroValue::LocalTimestampMillis(self.integer(value)?))
580
+ }
581
+ Schema::LocalTimestampMicros => {
582
+ Ok(AvroValue::LocalTimestampMicros(self.integer(value)?))
583
+ }
584
+ Schema::LocalTimestampNanos => Ok(AvroValue::LocalTimestampNanos(self.integer(value)?)),
585
+ Schema::Uuid(inner) => {
586
+ let text = self.string(value)?;
587
+ let uuid = uuid::Uuid::parse_str(&text).map_err(|_| self.fail("invalid UUID"))?;
588
+ Ok(match inner {
589
+ apache_avro::schema::UuidSchema::String => AvroValue::String(uuid.to_string()),
590
+ apache_avro::schema::UuidSchema::Bytes => {
591
+ AvroValue::Bytes(uuid.as_bytes().to_vec())
592
+ }
593
+ apache_avro::schema::UuidSchema::Fixed(fixed) => {
594
+ AvroValue::Fixed(fixed.size, uuid.as_bytes().to_vec())
595
+ }
596
+ })
597
+ }
598
+ Schema::Duration(_) => {
599
+ if !value.is_kind_of(self.duration) {
600
+ return Err(self.fail("expected Avrocadabra::Duration"));
601
+ }
602
+ let mut parts = [0u32; 3];
603
+ for (i, name) in ["months", "days", "milliseconds"].iter().enumerate() {
604
+ let part: Value = self.ruby_result(value.funcall(*name, ()))?;
605
+ parts[i] = u32::try_from(self.integer(part)?)
606
+ .map_err(|_| self.fail("duration components must be uint32"))?;
607
+ }
608
+ Ok(AvroValue::Duration(Duration::new(
609
+ apache_avro::Months::new(parts[0]),
610
+ apache_avro::Days::new(parts[1]),
611
+ apache_avro::Millis::new(parts[2]),
612
+ )))
613
+ }
614
+ _ => Err(self.fail("value does not match schema")),
615
+ }
616
+ }
617
+ }