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,1932 @@
1
+ use crate::memory::HeapSize;
2
+ use apache_avro::{
3
+ Decimal, Duration, Schema,
4
+ schema::{Aliases, FixedSchema, InnerDecimalSchema, Name, NamesRef, UuidSchema},
5
+ types::Value,
6
+ };
7
+ use serde_json::Value as Json;
8
+ use std::collections::{HashMap, HashSet};
9
+ use std::sync::Arc;
10
+
11
+ pub struct Resolution {
12
+ nodes: Vec<Node>,
13
+ root: usize,
14
+ limits: Limits,
15
+ memory_size: usize,
16
+ }
17
+
18
+ #[derive(Debug)]
19
+ pub struct Resolved {
20
+ pub value: Value,
21
+ pub defaults: Vec<(usize, Arc<Json>)>,
22
+ pub adapters: Vec<(usize, usize)>,
23
+ pub failure: Option<(usize, String)>,
24
+ }
25
+
26
+ enum Node {
27
+ Error(String),
28
+ Scalar {
29
+ writer: Logical,
30
+ reader: Logical,
31
+ from: Scalar,
32
+ to: Scalar,
33
+ },
34
+ Record(Vec<Field>),
35
+ Enum {
36
+ indices: Vec<Option<u32>>,
37
+ symbols: Vec<String>,
38
+ },
39
+ Array(usize),
40
+ Map(usize),
41
+ WriterUnion(Vec<usize>),
42
+ ReaderUnion(u32, usize),
43
+ }
44
+
45
+ struct Field {
46
+ name: String,
47
+ source: FieldSource,
48
+ }
49
+
50
+ enum FieldSource {
51
+ Writer {
52
+ index: usize,
53
+ name: String,
54
+ node: usize,
55
+ },
56
+ Default {
57
+ value: Arc<Json>,
58
+ usage: Usage,
59
+ },
60
+ Missing,
61
+ }
62
+
63
+ impl HeapSize for Field {
64
+ fn heap_size(&self) -> usize {
65
+ self.name.heap_size()
66
+ + match &self.source {
67
+ FieldSource::Writer { name, .. } => name.heap_size(),
68
+ FieldSource::Default { value, .. } => {
69
+ size_of::<Json>() + 2 * size_of::<usize>() + Json::heap_size(value)
70
+ }
71
+ FieldSource::Missing => 0,
72
+ }
73
+ }
74
+ }
75
+
76
+ impl HeapSize for Node {
77
+ fn heap_size(&self) -> usize {
78
+ match self {
79
+ Self::Error(error) => error.heap_size(),
80
+ Self::Record(fields) => fields.heap_size(),
81
+ Self::Enum { indices, symbols } => {
82
+ indices.capacity() * size_of::<Option<u32>>() + symbols.heap_size()
83
+ }
84
+ Self::WriterUnion(branches) => branches.heap_size(),
85
+ _ => 0,
86
+ }
87
+ }
88
+ }
89
+
90
+ #[derive(Clone, Copy, PartialEq, Eq)]
91
+ enum Scalar {
92
+ Null,
93
+ Boolean,
94
+ Int,
95
+ Long,
96
+ Float,
97
+ Double,
98
+ Bytes,
99
+ String,
100
+ Fixed(usize),
101
+ }
102
+
103
+ #[derive(Clone, Copy, PartialEq, Eq)]
104
+ enum Logical {
105
+ Plain,
106
+ Decimal { precision: usize },
107
+ BigDecimal,
108
+ Uuid,
109
+ Date,
110
+ TimeMillis,
111
+ TimeMicros,
112
+ TimestampMillis,
113
+ TimestampMicros,
114
+ TimestampNanos,
115
+ LocalTimestampMillis,
116
+ LocalTimestampMicros,
117
+ LocalTimestampNanos,
118
+ Duration,
119
+ }
120
+
121
+ #[derive(Clone, Copy)]
122
+ struct Limits {
123
+ depth: usize,
124
+ items: usize,
125
+ bytes: usize,
126
+ }
127
+
128
+ #[derive(Clone, Copy, Default)]
129
+ struct Usage {
130
+ depth: usize,
131
+ items: usize,
132
+ bytes: usize,
133
+ }
134
+
135
+ struct Budget {
136
+ limits: Limits,
137
+ usage: Usage,
138
+ exhausted: bool,
139
+ defaults: Vec<(usize, Arc<Json>)>,
140
+ adapters: Vec<(usize, usize)>,
141
+ field_position: usize,
142
+ read_position: usize,
143
+ pending_adapters: usize,
144
+ failure: Option<(usize, String)>,
145
+ }
146
+
147
+ fn field_default(field: &apache_avro::schema::RecordField) -> Option<&Json> {
148
+ field.default.as_ref().or_else(|| match &field.schema {
149
+ Schema::Null => Some(&Json::Null),
150
+ Schema::Union(union) if union.variants().iter().any(|s| matches!(s, Schema::Null)) => {
151
+ Some(&Json::Null)
152
+ }
153
+ _ => None,
154
+ })
155
+ }
156
+
157
+ pub(crate) fn validate_defaults<'a>(
158
+ schemas: &'a [Schema],
159
+ names: &NamesRef<'a>,
160
+ limits: crate::guard::Limits,
161
+ ) -> Result<(), String> {
162
+ let mut budget = Budget::new(Limits {
163
+ depth: limits.max_depth,
164
+ items: limits.max_items,
165
+ bytes: limits.max_bytes,
166
+ });
167
+ let mut pending: Vec<_> = schemas.iter().collect();
168
+ while let Some(schema) = pending.pop() {
169
+ match schema {
170
+ Schema::Record(record) => {
171
+ for field in &record.fields {
172
+ if let Some(default) = field.default.as_ref() {
173
+ let path = format!("$.{}", field.name);
174
+ let mut field_budget = Budget::new(budget.limits);
175
+ default_value(default, &field.schema, names, 0, &path, &mut field_budget)?;
176
+ budget.add(field_budget.usage, &path)?;
177
+ }
178
+ pending.push(&field.schema);
179
+ }
180
+ }
181
+ Schema::Array(array) => pending.push(&array.items),
182
+ Schema::Map(map) => pending.push(&map.types),
183
+ Schema::Union(union) => pending.extend(union.variants()),
184
+ _ => {}
185
+ }
186
+ }
187
+ Ok(())
188
+ }
189
+
190
+ impl Budget {
191
+ fn new(limits: Limits) -> Self {
192
+ Self {
193
+ limits,
194
+ usage: Usage::default(),
195
+ exhausted: false,
196
+ defaults: Vec::new(),
197
+ adapters: Vec::new(),
198
+ field_position: 0,
199
+ read_position: 0,
200
+ pending_adapters: 0,
201
+ failure: None,
202
+ }
203
+ }
204
+
205
+ fn adapters(&mut self, count: usize) {
206
+ let count = count + std::mem::take(&mut self.pending_adapters);
207
+ if count != 1 {
208
+ self.adapters.push((self.read_position, count));
209
+ }
210
+ self.read_position += 1;
211
+ }
212
+
213
+ fn node(&mut self, depth: usize, path: &str) -> Result<(), String> {
214
+ self.add(
215
+ Usage {
216
+ depth,
217
+ items: 1,
218
+ bytes: 0,
219
+ },
220
+ path,
221
+ )
222
+ }
223
+
224
+ fn bytes(&mut self, bytes: usize, path: &str) -> Result<(), String> {
225
+ self.add(
226
+ Usage {
227
+ bytes,
228
+ ..Usage::default()
229
+ },
230
+ path,
231
+ )
232
+ }
233
+
234
+ fn add(&mut self, usage: Usage, path: &str) -> Result<(), String> {
235
+ if usage.depth > self.limits.depth {
236
+ self.exhausted = true;
237
+ return Err(format!("{path}: resolution exceeds maximum depth"));
238
+ }
239
+ self.usage.depth = self.usage.depth.max(usage.depth);
240
+ self.usage.items = self
241
+ .usage
242
+ .items
243
+ .checked_add(usage.items)
244
+ .filter(|&n| n <= self.limits.items)
245
+ .ok_or_else(|| {
246
+ self.exhausted = true;
247
+ format!("{path}: resolution exceeds maximum item count")
248
+ })?;
249
+ self.usage.bytes = self
250
+ .usage
251
+ .bytes
252
+ .checked_add(usage.bytes)
253
+ .filter(|&n| n <= self.limits.bytes)
254
+ .ok_or_else(|| {
255
+ self.exhausted = true;
256
+ format!("{path}: resolution exceeds maximum byte count")
257
+ })?;
258
+ Ok(())
259
+ }
260
+ }
261
+
262
+ impl Resolution {
263
+ pub fn new(
264
+ writer: &Schema,
265
+ writer_names: &NamesRef<'_>,
266
+ reader: &Schema,
267
+ reader_names: &NamesRef<'_>,
268
+ max_depth: usize,
269
+ max_items: usize,
270
+ max_bytes: usize,
271
+ ) -> Result<Self, String> {
272
+ let limits = Limits {
273
+ depth: max_depth,
274
+ items: max_items,
275
+ bytes: max_bytes,
276
+ };
277
+ let mut builder = Builder {
278
+ writer_names,
279
+ reader_names,
280
+ nodes: Vec::new(),
281
+ pairs: HashMap::new(),
282
+ limits,
283
+ defaults: Budget::new(limits),
284
+ };
285
+ let root = builder.compile(writer, reader, 0)?;
286
+ let memory_size = size_of::<Self>() + 2 * size_of::<usize>() + builder.nodes.heap_size();
287
+ Ok(Self {
288
+ nodes: builder.nodes,
289
+ root,
290
+ limits,
291
+ memory_size,
292
+ })
293
+ }
294
+
295
+ pub fn memory_size(&self) -> usize {
296
+ self.memory_size
297
+ }
298
+
299
+ pub fn apply(&self, value: Value) -> Result<Resolved, String> {
300
+ let mut budget = Budget::new(self.limits);
301
+ let value = self.resolve(self.root, value, 0, "$", &mut budget)?;
302
+ Ok(Resolved {
303
+ value,
304
+ defaults: budget.defaults,
305
+ adapters: budget.adapters,
306
+ failure: budget.failure,
307
+ })
308
+ }
309
+
310
+ fn resolve(
311
+ &self,
312
+ node: usize,
313
+ value: Value,
314
+ depth: usize,
315
+ path: &str,
316
+ budget: &mut Budget,
317
+ ) -> Result<Value, String> {
318
+ if budget.failure.is_some() {
319
+ return Ok(Value::Null);
320
+ }
321
+ let position = budget.read_position;
322
+ match self.resolve_value(node, value, depth, path, budget) {
323
+ Err(error) if position > 0 && !budget.exhausted => {
324
+ budget.failure = Some((position, error));
325
+ Ok(Value::Null)
326
+ }
327
+ result => result,
328
+ }
329
+ }
330
+
331
+ fn resolve_value(
332
+ &self,
333
+ node: usize,
334
+ value: Value,
335
+ depth: usize,
336
+ path: &str,
337
+ budget: &mut Budget,
338
+ ) -> Result<Value, String> {
339
+ match &self.nodes[node] {
340
+ Node::Error(error) => return Err(format!("{path}: {error}")),
341
+ Node::WriterUnion(branches) => {
342
+ let Value::Union(index, value) = value else {
343
+ return Err(format!("{path}: expected writer union"));
344
+ };
345
+ let node = branches
346
+ .get(index as usize)
347
+ .ok_or_else(|| format!("{path}: invalid writer union index {index}"))?;
348
+ budget.pending_adapters += 1;
349
+ return self.resolve(*node, *value, depth, path, budget);
350
+ }
351
+ Node::ReaderUnion(index, child) => {
352
+ budget.node(depth, path)?;
353
+ budget.adapters(0);
354
+ return self
355
+ .resolve(*child, value, depth, path, budget)
356
+ .map(|value| Value::Union(*index, Box::new(value)));
357
+ }
358
+ _ => {}
359
+ }
360
+ budget.node(depth, path)?;
361
+ budget.adapters(1);
362
+ match (&self.nodes[node], value) {
363
+ (
364
+ Node::Scalar {
365
+ writer,
366
+ reader,
367
+ from,
368
+ to,
369
+ },
370
+ value,
371
+ ) => {
372
+ let plain = strip_logical(value, *writer, *from)
373
+ .map_err(|error| format!("{path}: {error}"))?;
374
+ let promoted = if *reader == Logical::Plain
375
+ && matches!(
376
+ (*from, *to),
377
+ (Scalar::Int | Scalar::Long, Scalar::Float | Scalar::Double)
378
+ | (Scalar::String, Scalar::Bytes)
379
+ | (Scalar::Bytes, Scalar::String)
380
+ ) {
381
+ plain
382
+ } else {
383
+ promote(plain, *from, *to).map_err(|error| format!("{path}: {error}"))?
384
+ };
385
+ budget.bytes(value_bytes(&promoted), path)?;
386
+ apply_logical(promoted, *reader).map_err(|error| format!("{path}: {error}"))
387
+ }
388
+ (Node::Record(fields), Value::Record(mut values)) => {
389
+ if fields.len() > budget.limits.items.saturating_sub(budget.usage.items) {
390
+ return Err(format!("{path}: resolution exceeds maximum item count"));
391
+ }
392
+ let mut output = Vec::with_capacity(fields.len());
393
+ for field in fields {
394
+ let position = budget.field_position;
395
+ budget.field_position += 1;
396
+ let field_path = format!("{path}.{}", field.name);
397
+ budget.bytes(field.name.len(), &field_path)?;
398
+ let value = match &field.source {
399
+ FieldSource::Writer { index, name, node } => {
400
+ let (actual, value) = values
401
+ .get_mut(*index)
402
+ .ok_or_else(|| format!("{field_path}: missing writer field"))?;
403
+ if actual != name {
404
+ return Err(format!(
405
+ "{field_path}: unexpected writer field {actual}"
406
+ ));
407
+ }
408
+ self.resolve(
409
+ *node,
410
+ std::mem::replace(value, Value::Null),
411
+ depth + 1,
412
+ &field_path,
413
+ budget,
414
+ )?
415
+ }
416
+ FieldSource::Default { value, usage } => {
417
+ let mut usage = *usage;
418
+ usage.depth = usage
419
+ .depth
420
+ .checked_add(depth + 1)
421
+ .ok_or_else(|| format!("{field_path}: default depth overflow"))?;
422
+ budget.add(usage, &field_path)?;
423
+ budget.defaults.push((position, Arc::clone(value)));
424
+ Value::Null
425
+ }
426
+ FieldSource::Missing => {
427
+ budget.failure.get_or_insert_with(|| (budget.read_position, format!(
428
+ "{field_path}: reader field is absent from writer schema and has no default"
429
+ )));
430
+ Value::Null
431
+ }
432
+ };
433
+ output.push((field.name.clone(), value));
434
+ }
435
+ Ok(Value::Record(output))
436
+ }
437
+ (Node::Enum { indices, symbols }, Value::Enum(index, symbol)) => {
438
+ let resolved = indices
439
+ .get(index as usize)
440
+ .copied()
441
+ .ok_or_else(|| format!("{path}: invalid writer enum index"))?;
442
+ let (index, symbol) = match resolved {
443
+ Some(index) => (index, symbols[index as usize].clone()),
444
+ None => (index, symbol),
445
+ };
446
+ budget.bytes(symbol.len(), path)?;
447
+ Ok(Value::Enum(index, symbol))
448
+ }
449
+ (Node::Array(child), Value::Array(values)) => {
450
+ if values.len() > budget.limits.items.saturating_sub(budget.usage.items) {
451
+ return Err(format!("{path}: resolution exceeds maximum item count"));
452
+ }
453
+ values
454
+ .into_iter()
455
+ .enumerate()
456
+ .map(|(index, value)| {
457
+ self.resolve(
458
+ *child,
459
+ value,
460
+ depth + 1,
461
+ &format!("{path}[{index}]"),
462
+ budget,
463
+ )
464
+ })
465
+ .collect::<Result<Vec<_>, _>>()
466
+ .map(Value::Array)
467
+ }
468
+ (Node::Map(child), Value::Record(values)) => {
469
+ if values.len() > budget.limits.items.saturating_sub(budget.usage.items) {
470
+ return Err(format!("{path}: resolution exceeds maximum item count"));
471
+ }
472
+ values
473
+ .into_iter()
474
+ .map(|(key, value)| {
475
+ budget.bytes(key.len(), path)?;
476
+ let value = self.resolve(
477
+ *child,
478
+ value,
479
+ depth + 1,
480
+ &format!("{path}[{key:?}]"),
481
+ budget,
482
+ )?;
483
+ Ok((key, value))
484
+ })
485
+ .collect::<Result<Vec<_>, String>>()
486
+ .map(Value::Record)
487
+ }
488
+ _ => Err(format!(
489
+ "{path}: decoded value does not match writer schema"
490
+ )),
491
+ }
492
+ }
493
+ }
494
+
495
+ struct Builder<'a, 'b> {
496
+ writer_names: &'a NamesRef<'b>,
497
+ reader_names: &'a NamesRef<'b>,
498
+ nodes: Vec<Node>,
499
+ pairs: HashMap<(usize, usize), usize>,
500
+ limits: Limits,
501
+ defaults: Budget,
502
+ }
503
+
504
+ impl<'b> Builder<'_, 'b> {
505
+ fn compile(
506
+ &mut self,
507
+ writer: &'b Schema,
508
+ reader: &'b Schema,
509
+ depth: usize,
510
+ ) -> Result<usize, String> {
511
+ let writer = dereference(writer, self.writer_names)?;
512
+ let reader = dereference(reader, self.reader_names)?;
513
+ let pair = (
514
+ std::ptr::from_ref(writer) as usize,
515
+ std::ptr::from_ref(reader) as usize,
516
+ );
517
+ if let Some(&node) = self.pairs.get(&pair) {
518
+ return Ok(node);
519
+ }
520
+ if depth > self.limits.depth || self.nodes.len() >= self.limits.items {
521
+ return Err("$: schema resolution plan exceeds configured limits".into());
522
+ }
523
+ let index = self.nodes.len();
524
+ self.nodes
525
+ .push(Node::Error("incomplete resolution plan".into()));
526
+ self.pairs.insert(pair, index);
527
+ let node = self.build(writer, reader, depth)?;
528
+ self.nodes[index] = node;
529
+ Ok(index)
530
+ }
531
+
532
+ fn build(
533
+ &mut self,
534
+ writer: &'b Schema,
535
+ reader: &'b Schema,
536
+ depth: usize,
537
+ ) -> Result<Node, String> {
538
+ if let Schema::Union(union) = writer {
539
+ if union.variants().len() > self.limits.items {
540
+ return Err("$: writer union exceeds maximum item count".into());
541
+ }
542
+ return union
543
+ .variants()
544
+ .iter()
545
+ .map(|branch| self.compile(branch, reader, depth + 1))
546
+ .collect::<Result<Vec<_>, _>>()
547
+ .map(Node::WriterUnion);
548
+ }
549
+ if let Schema::Union(union) = reader {
550
+ for (index, branch) in union.variants().iter().enumerate() {
551
+ if self.matches(writer, branch, depth + 1)? {
552
+ return Ok(Node::ReaderUnion(
553
+ index as u32,
554
+ self.compile(writer, branch, depth + 1)?,
555
+ ));
556
+ }
557
+ }
558
+ return Ok(Node::Error(format!(
559
+ "writer {writer} has no matching reader union branch"
560
+ )));
561
+ }
562
+ if !self.matches(writer, reader, depth)? {
563
+ return Ok(Node::Error(format!(
564
+ "writer {writer} cannot resolve to reader {reader}"
565
+ )));
566
+ }
567
+ match (writer, reader) {
568
+ (Schema::Record(writer), Schema::Record(reader)) => {
569
+ if reader.fields.len() > self.limits.items {
570
+ return Err("$: reader record exceeds maximum item count".into());
571
+ }
572
+ let mut used = HashSet::new();
573
+ let aliases: HashMap<_, _> = reader
574
+ .fields
575
+ .iter()
576
+ .flat_map(|field| {
577
+ field
578
+ .aliases
579
+ .iter()
580
+ .map(move |alias| (alias.as_str(), field))
581
+ })
582
+ .collect();
583
+ let reader_fields: HashMap<_, _> = reader
584
+ .fields
585
+ .iter()
586
+ .map(|field| (field.name.as_str(), field))
587
+ .collect();
588
+ let mut fields = Vec::with_capacity(reader.fields.len());
589
+ for (index, written) in writer.fields.iter().enumerate() {
590
+ let Some(field) = reader_fields
591
+ .get(written.name.as_str())
592
+ .or_else(|| aliases.get(written.name.as_str()))
593
+ else {
594
+ continue;
595
+ };
596
+ used.insert(field.name.as_str());
597
+ self.defaults.bytes(written.name.len(), &field.name)?;
598
+ self.defaults.bytes(field.name.len(), &field.name)?;
599
+ fields.push(Field {
600
+ name: field.name.clone(),
601
+ source: FieldSource::Writer {
602
+ index,
603
+ name: written.name.clone(),
604
+ node: self.compile(&written.schema, &field.schema, depth + 1)?,
605
+ },
606
+ });
607
+ }
608
+ for field in &reader.fields {
609
+ if used.contains(field.name.as_str()) {
610
+ continue;
611
+ }
612
+ let source = if let Some(default) = field.default.as_ref() {
613
+ let path = format!("$.{}", field.name);
614
+ let mut budget = Budget::new(self.limits);
615
+ default_value(
616
+ default,
617
+ &field.schema,
618
+ self.reader_names,
619
+ 0,
620
+ &path,
621
+ &mut budget,
622
+ )?;
623
+ self.defaults.add(budget.usage, &path)?;
624
+ FieldSource::Default {
625
+ value: Arc::new(default.clone()),
626
+ usage: budget.usage,
627
+ }
628
+ } else {
629
+ FieldSource::Missing
630
+ };
631
+ self.defaults.bytes(field.name.len(), &field.name)?;
632
+ fields.push(Field {
633
+ name: field.name.clone(),
634
+ source,
635
+ });
636
+ }
637
+ Ok(Node::Record(fields))
638
+ }
639
+ (Schema::Enum(writer), Schema::Enum(reader)) => {
640
+ if writer.symbols.len() > self.limits.items
641
+ || reader.symbols.len() > self.limits.items
642
+ {
643
+ return Err("$: enum exceeds maximum item count".into());
644
+ }
645
+ for symbol in &reader.symbols {
646
+ self.defaults.bytes(symbol.len(), "$")?;
647
+ }
648
+ let reader_symbols: HashMap<_, _> = reader
649
+ .symbols
650
+ .iter()
651
+ .enumerate()
652
+ .map(|(index, symbol)| (symbol.as_str(), index as u32))
653
+ .collect();
654
+ let default = reader
655
+ .default
656
+ .as_ref()
657
+ .and_then(|symbol| reader_symbols.get(symbol.as_str()))
658
+ .copied();
659
+ let indices = writer
660
+ .symbols
661
+ .iter()
662
+ .map(|symbol| reader_symbols.get(symbol.as_str()).copied().or(default))
663
+ .collect();
664
+ Ok(Node::Enum {
665
+ indices,
666
+ symbols: reader.symbols.clone(),
667
+ })
668
+ }
669
+ (Schema::Array(writer), Schema::Array(reader)) => Ok(Node::Array(self.compile(
670
+ &writer.items,
671
+ &reader.items,
672
+ depth + 1,
673
+ )?)),
674
+ (Schema::Map(writer), Schema::Map(reader)) => Ok(Node::Map(self.compile(
675
+ &writer.types,
676
+ &reader.types,
677
+ depth + 1,
678
+ )?)),
679
+ _ => {
680
+ let Some((from, writer)) = scalar(writer) else {
681
+ return Ok(Node::Error("unsupported writer logical type".into()));
682
+ };
683
+ let Some((to, reader)) = scalar(reader) else {
684
+ return Ok(Node::Error("unsupported reader logical type".into()));
685
+ };
686
+ Ok(Node::Scalar {
687
+ writer,
688
+ reader,
689
+ from,
690
+ to,
691
+ })
692
+ }
693
+ }
694
+ }
695
+
696
+ fn matches(
697
+ &self,
698
+ writer: &'b Schema,
699
+ reader: &'b Schema,
700
+ depth: usize,
701
+ ) -> Result<bool, String> {
702
+ if depth > self.limits.depth {
703
+ return Err("$: schema matching exceeds maximum depth".into());
704
+ }
705
+ let writer = dereference(writer, self.writer_names)?;
706
+ let reader = dereference(reader, self.reader_names)?;
707
+ Ok(match (writer, reader) {
708
+ (Schema::Union(_), _) | (_, Schema::Union(_)) => true,
709
+ (Schema::Record(writer), Schema::Record(reader)) => {
710
+ writer.attributes.get("type") == reader.attributes.get("type")
711
+ && names_match(&writer.name, &reader.name, &reader.aliases)
712
+ }
713
+ (Schema::Enum(writer), Schema::Enum(reader)) => {
714
+ names_match(&writer.name, &reader.name, &reader.aliases)
715
+ }
716
+ (Schema::Array(writer), Schema::Array(reader)) => {
717
+ self.matches(&writer.items, &reader.items, depth + 1)?
718
+ }
719
+ (Schema::Map(writer), Schema::Map(reader)) => {
720
+ self.matches(&writer.types, &reader.types, depth + 1)?
721
+ }
722
+ _ => {
723
+ if let (Schema::Decimal(writer), Schema::Decimal(reader)) = (writer, reader)
724
+ && (writer.scale != reader.scale || writer.precision != reader.precision)
725
+ {
726
+ return Ok(false);
727
+ }
728
+ if let (Some(writer), Some(reader)) = (fixed(writer), fixed(reader)) {
729
+ return Ok(writer.size == reader.size
730
+ && names_match(&writer.name, &reader.name, &reader.aliases));
731
+ }
732
+ match (scalar(writer), scalar(reader)) {
733
+ (Some((from, _)), Some((to, _))) => scalar_matches(from, to),
734
+ _ => false,
735
+ }
736
+ }
737
+ })
738
+ }
739
+ }
740
+
741
+ pub(crate) fn dereference<'a>(
742
+ mut schema: &'a Schema,
743
+ names: &NamesRef<'a>,
744
+ ) -> Result<&'a Schema, String> {
745
+ let mut hops = 0;
746
+ while let Schema::Ref { name } = schema {
747
+ if hops > names.len() {
748
+ return Err(format!("$: cyclic unresolved schema reference {name}"));
749
+ }
750
+ schema = names
751
+ .get(name)
752
+ .copied()
753
+ .ok_or_else(|| format!("$: unresolved schema reference {name}"))?;
754
+ hops += 1;
755
+ }
756
+ Ok(schema)
757
+ }
758
+
759
+ fn names_match(writer: &Name, reader: &Name, aliases: &Aliases) -> bool {
760
+ writer == reader
761
+ || aliases.as_ref().is_some_and(|aliases| {
762
+ aliases
763
+ .iter()
764
+ .any(|alias| alias.fully_qualified_name(reader.namespace()).as_ref() == writer)
765
+ })
766
+ }
767
+
768
+ fn fixed(schema: &Schema) -> Option<&FixedSchema> {
769
+ match schema {
770
+ Schema::Fixed(fixed) | Schema::Duration(fixed) | Schema::Uuid(UuidSchema::Fixed(fixed)) => {
771
+ Some(fixed)
772
+ }
773
+ Schema::Decimal(decimal) => match &decimal.inner {
774
+ InnerDecimalSchema::Fixed(fixed) => Some(fixed),
775
+ InnerDecimalSchema::Bytes => None,
776
+ },
777
+ _ => None,
778
+ }
779
+ }
780
+
781
+ fn scalar(schema: &Schema) -> Option<(Scalar, Logical)> {
782
+ Some(match schema {
783
+ Schema::Null => (Scalar::Null, Logical::Plain),
784
+ Schema::Boolean => (Scalar::Boolean, Logical::Plain),
785
+ Schema::Int => (Scalar::Int, Logical::Plain),
786
+ Schema::Long => (Scalar::Long, Logical::Plain),
787
+ Schema::Float => (Scalar::Float, Logical::Plain),
788
+ Schema::Double => (Scalar::Double, Logical::Plain),
789
+ Schema::Bytes => (Scalar::Bytes, Logical::Plain),
790
+ Schema::String => (Scalar::String, Logical::Plain),
791
+ Schema::Fixed(fixed) => (Scalar::Fixed(fixed.size), Logical::Plain),
792
+ Schema::Decimal(decimal) => (
793
+ match &decimal.inner {
794
+ InnerDecimalSchema::Bytes => Scalar::Bytes,
795
+ InnerDecimalSchema::Fixed(fixed) => Scalar::Fixed(fixed.size),
796
+ },
797
+ Logical::Decimal {
798
+ precision: decimal.precision,
799
+ },
800
+ ),
801
+ Schema::BigDecimal => (Scalar::Bytes, Logical::BigDecimal),
802
+ Schema::Uuid(inner) => (
803
+ match inner {
804
+ UuidSchema::String => Scalar::String,
805
+ UuidSchema::Bytes => Scalar::Bytes,
806
+ UuidSchema::Fixed(fixed) => Scalar::Fixed(fixed.size),
807
+ },
808
+ Logical::Uuid,
809
+ ),
810
+ Schema::Date => (Scalar::Int, Logical::Date),
811
+ Schema::TimeMillis => (Scalar::Int, Logical::TimeMillis),
812
+ Schema::TimeMicros => (Scalar::Long, Logical::TimeMicros),
813
+ Schema::TimestampMillis => (Scalar::Long, Logical::TimestampMillis),
814
+ Schema::TimestampMicros => (Scalar::Long, Logical::TimestampMicros),
815
+ Schema::TimestampNanos => (Scalar::Long, Logical::TimestampNanos),
816
+ Schema::LocalTimestampMillis => (Scalar::Long, Logical::LocalTimestampMillis),
817
+ Schema::LocalTimestampMicros => (Scalar::Long, Logical::LocalTimestampMicros),
818
+ Schema::LocalTimestampNanos => (Scalar::Long, Logical::LocalTimestampNanos),
819
+ Schema::Duration(fixed) => (Scalar::Fixed(fixed.size), Logical::Duration),
820
+ _ => return None,
821
+ })
822
+ }
823
+
824
+ fn scalar_matches(from: Scalar, to: Scalar) -> bool {
825
+ if matches!(from, Scalar::Fixed(_)) || matches!(to, Scalar::Fixed(_)) {
826
+ return false; // Named fixed matching is handled before primitive matching.
827
+ }
828
+ from == to
829
+ || matches!(
830
+ (from, to),
831
+ (Scalar::Int, Scalar::Long | Scalar::Float | Scalar::Double)
832
+ | (Scalar::Long, Scalar::Float | Scalar::Double)
833
+ | (Scalar::Float, Scalar::Double)
834
+ | (Scalar::String, Scalar::Bytes)
835
+ | (Scalar::Bytes, Scalar::String)
836
+ )
837
+ }
838
+
839
+ fn promote(value: Value, from: Scalar, to: Scalar) -> Result<Value, String> {
840
+ Ok(match (from, to, value) {
841
+ (Scalar::Null, Scalar::Null, Value::Null) => Value::Null,
842
+ (Scalar::Boolean, Scalar::Boolean, value @ Value::Boolean(_)) => value,
843
+ (Scalar::Int, Scalar::Int, value @ Value::Int(_)) => value,
844
+ (Scalar::Int, Scalar::Long, Value::Int(value)) => Value::Long(i64::from(value)),
845
+ (Scalar::Int, Scalar::Float, Value::Int(value)) => Value::Float(value as f32),
846
+ (Scalar::Int, Scalar::Double, Value::Int(value)) => Value::Double(f64::from(value)),
847
+ (Scalar::Long, Scalar::Long, value @ Value::Long(_)) => value,
848
+ (Scalar::Long, Scalar::Float, Value::Long(value)) => Value::Float(value as f32),
849
+ (Scalar::Long, Scalar::Double, Value::Long(value)) => Value::Double(value as f64),
850
+ (Scalar::Float, Scalar::Float, value @ Value::Float(_)) => value,
851
+ (Scalar::Float, Scalar::Double, Value::Float(value)) => Value::Double(f64::from(value)),
852
+ (Scalar::Double, Scalar::Double, value @ Value::Double(_)) => value,
853
+ (Scalar::Bytes, Scalar::Bytes, value @ Value::Bytes(_)) => value,
854
+ (Scalar::String, Scalar::String, value @ Value::String(_)) => value,
855
+ (Scalar::String, Scalar::Bytes, Value::String(value)) => Value::Bytes(value.into_bytes()),
856
+ (Scalar::Bytes, Scalar::String, Value::Bytes(value)) => Value::String(
857
+ String::from_utf8(value).map_err(|_| "bytes promoted to string are not valid UTF-8")?,
858
+ ),
859
+ (Scalar::Fixed(writer), Scalar::Fixed(reader), Value::Fixed(size, bytes))
860
+ if writer == reader && size == writer && bytes.len() == size =>
861
+ {
862
+ Value::Fixed(size, bytes)
863
+ }
864
+ _ => return Err("decoded value does not match writer scalar".into()),
865
+ })
866
+ }
867
+
868
+ fn strip_logical(value: Value, logical: Logical, physical: Scalar) -> Result<Value, String> {
869
+ Ok(match (logical, value) {
870
+ (Logical::Plain, value) => value,
871
+ (Logical::Decimal { precision }, Value::Decimal(decimal)) => {
872
+ let bytes = Vec::<u8>::try_from(decimal).map_err(|error| error.to_string())?;
873
+ validate_decimal(&bytes, precision)?;
874
+ match physical {
875
+ Scalar::Fixed(size) => Value::Fixed(size, bytes),
876
+ _ => Value::Bytes(bytes),
877
+ }
878
+ }
879
+ (Logical::BigDecimal, Value::BigDecimal(decimal)) => {
880
+ Value::Bytes(crate::big_decimal::encode(&decimal)?)
881
+ }
882
+ (Logical::Uuid, Value::Uuid(uuid)) => match physical {
883
+ Scalar::String => Value::String(uuid.hyphenated().to_string()),
884
+ Scalar::Fixed(size) => Value::Fixed(size, uuid.as_bytes().to_vec()),
885
+ _ => Value::Bytes(uuid.as_bytes().to_vec()),
886
+ },
887
+ (Logical::Date, Value::Date(value)) => Value::Int(value),
888
+ (Logical::TimeMillis, Value::TimeMillis(value)) => {
889
+ validate_time(i64::from(value), 86_400_000)?;
890
+ Value::Int(value)
891
+ }
892
+ (Logical::TimeMicros, Value::TimeMicros(value)) => {
893
+ validate_time(value, 86_400_000_000)?;
894
+ Value::Long(value)
895
+ }
896
+ (Logical::TimestampMillis, Value::TimestampMillis(value))
897
+ | (Logical::TimestampMicros, Value::TimestampMicros(value))
898
+ | (Logical::TimestampNanos, Value::TimestampNanos(value))
899
+ | (Logical::LocalTimestampMillis, Value::LocalTimestampMillis(value))
900
+ | (Logical::LocalTimestampMicros, Value::LocalTimestampMicros(value))
901
+ | (Logical::LocalTimestampNanos, Value::LocalTimestampNanos(value)) => Value::Long(value),
902
+ (Logical::Duration, Value::Duration(duration)) => {
903
+ Value::Fixed(12, <[u8; 12]>::from(duration).to_vec())
904
+ }
905
+ _ => return Err("decoded value does not match writer logical type".into()),
906
+ })
907
+ }
908
+
909
+ fn apply_logical(value: Value, logical: Logical) -> Result<Value, String> {
910
+ Ok(match (logical, value) {
911
+ (Logical::Plain, value) => value,
912
+ (Logical::Decimal { precision }, Value::Bytes(bytes) | Value::Fixed(_, bytes)) => {
913
+ validate_decimal(&bytes, precision)?;
914
+ Value::Decimal(Decimal::from(bytes))
915
+ }
916
+ (Logical::BigDecimal, Value::Bytes(bytes)) => {
917
+ Value::BigDecimal(crate::big_decimal::decode(&bytes)?)
918
+ }
919
+ (Logical::Uuid, Value::String(string)) => Value::Uuid(
920
+ uuid::Uuid::parse_str(&string).map_err(|error| format!("invalid UUID: {error}"))?,
921
+ ),
922
+ (Logical::Uuid, Value::Bytes(bytes) | Value::Fixed(_, bytes)) => Value::Uuid(
923
+ uuid::Uuid::from_slice(&bytes).map_err(|error| format!("invalid UUID: {error}"))?,
924
+ ),
925
+ (Logical::Date, Value::Int(value)) => Value::Date(value),
926
+ (Logical::TimeMillis, Value::Int(value)) => {
927
+ validate_time(i64::from(value), 86_400_000)?;
928
+ Value::TimeMillis(value)
929
+ }
930
+ (Logical::TimeMicros, Value::Long(value)) => {
931
+ validate_time(value, 86_400_000_000)?;
932
+ Value::TimeMicros(value)
933
+ }
934
+ (Logical::TimestampMillis, Value::Long(value)) => Value::TimestampMillis(value),
935
+ (Logical::TimestampMicros, Value::Long(value)) => Value::TimestampMicros(value),
936
+ (Logical::TimestampNanos, Value::Long(value)) => Value::TimestampNanos(value),
937
+ (Logical::LocalTimestampMillis, Value::Long(value)) => Value::LocalTimestampMillis(value),
938
+ (Logical::LocalTimestampMicros, Value::Long(value)) => Value::LocalTimestampMicros(value),
939
+ (Logical::LocalTimestampNanos, Value::Long(value)) => Value::LocalTimestampNanos(value),
940
+ (Logical::Duration, Value::Fixed(12, bytes)) => {
941
+ let bytes: [u8; 12] = bytes.try_into().map_err(|_| "duration requires 12 bytes")?;
942
+ Value::Duration(Duration::from(bytes))
943
+ }
944
+ _ => return Err("value cannot represent reader logical type".into()),
945
+ })
946
+ }
947
+
948
+ pub(crate) fn logical_value(value: Value, schema: &Schema) -> Result<Value, String> {
949
+ let (physical, logical) = scalar(schema).ok_or("unexpected wire schema")?;
950
+ let value = match (physical, value) {
951
+ (Scalar::Fixed(size), Value::Bytes(bytes)) => Value::Fixed(size, bytes),
952
+ (_, value) => value,
953
+ };
954
+ apply_logical(value, logical)
955
+ }
956
+
957
+ fn validate_decimal(bytes: &[u8], precision: usize) -> Result<(), String> {
958
+ crate::guard::decimal_precision(bytes, precision)
959
+ .map_err(|error| error.replace("schema precision", "reader precision"))
960
+ }
961
+
962
+ fn validate_time(value: i64, units_per_day: i64) -> Result<(), String> {
963
+ if !(0..units_per_day).contains(&value) {
964
+ return Err("time logical value is outside one day".into());
965
+ }
966
+ Ok(())
967
+ }
968
+
969
+ fn value_bytes(value: &Value) -> usize {
970
+ match value {
971
+ Value::String(value) | Value::Enum(_, value) => value.len(),
972
+ Value::Bytes(value) | Value::Fixed(_, value) => value.len(),
973
+ _ => 0,
974
+ }
975
+ }
976
+
977
+ pub(crate) fn default_datum<'a>(
978
+ json: &Json,
979
+ schema: &'a Schema,
980
+ names: &NamesRef<'a>,
981
+ limits: crate::guard::Limits,
982
+ ) -> Result<Value, String> {
983
+ default_value(
984
+ json,
985
+ schema,
986
+ names,
987
+ 0,
988
+ "$",
989
+ &mut Budget::new(Limits {
990
+ depth: limits.max_depth,
991
+ items: limits.max_items,
992
+ bytes: limits.max_bytes,
993
+ }),
994
+ )
995
+ }
996
+
997
+ fn default_value<'a>(
998
+ json: &Json,
999
+ schema: &'a Schema,
1000
+ names: &NamesRef<'a>,
1001
+ depth: usize,
1002
+ path: &str,
1003
+ budget: &mut Budget,
1004
+ ) -> Result<Value, String> {
1005
+ budget.node(depth, path)?;
1006
+ let schema = dereference(schema, names)?;
1007
+ let invalid = || format!("{path}: invalid default for {schema}");
1008
+ let value = match schema {
1009
+ Schema::Union(union) => {
1010
+ for (index, branch) in union.variants().iter().enumerate() {
1011
+ // Failed alternatives also consume work and allocation budgets.
1012
+ match default_value(json, branch, names, depth + 1, path, budget) {
1013
+ Ok(value) => return Ok(Value::Union(index as u32, Box::new(value))),
1014
+ Err(error) if budget.exhausted => return Err(error),
1015
+ Err(_) => {}
1016
+ }
1017
+ }
1018
+ return Err(invalid());
1019
+ }
1020
+ Schema::Record(record) => {
1021
+ let object = json.as_object().ok_or_else(invalid)?;
1022
+ if record.fields.len() > budget.limits.items.saturating_sub(budget.usage.items) {
1023
+ budget.exhausted = true;
1024
+ return Err(format!("{path}: default exceeds maximum item count"));
1025
+ }
1026
+ let mut fields = Vec::with_capacity(record.fields.len());
1027
+ for field in &record.fields {
1028
+ let path = format!("{path}.{}", field.name);
1029
+ let json = object
1030
+ .get(&field.name)
1031
+ .filter(|value| {
1032
+ field.default.is_none() || !matches!(value, Json::Null | Json::Bool(false))
1033
+ })
1034
+ .or_else(|| field_default(field))
1035
+ .ok_or_else(|| format!("{path}: missing field in record default"))?;
1036
+ budget.bytes(field.name.len(), &path)?;
1037
+ let value = default_value(json, &field.schema, names, depth + 1, &path, budget)?;
1038
+ fields.push((field.name.clone(), value));
1039
+ }
1040
+ return Ok(Value::Record(fields));
1041
+ }
1042
+ Schema::Array(array) => {
1043
+ let values = json.as_array().ok_or_else(invalid)?;
1044
+ if values.len() > budget.limits.items.saturating_sub(budget.usage.items) {
1045
+ budget.exhausted = true;
1046
+ return Err(format!("{path}: default exceeds maximum item count"));
1047
+ }
1048
+ return values
1049
+ .iter()
1050
+ .enumerate()
1051
+ .map(|(index, json)| {
1052
+ default_value(
1053
+ json,
1054
+ &array.items,
1055
+ names,
1056
+ depth + 1,
1057
+ &format!("{path}[{index}]"),
1058
+ budget,
1059
+ )
1060
+ })
1061
+ .collect::<Result<Vec<_>, _>>()
1062
+ .map(Value::Array);
1063
+ }
1064
+ Schema::Map(map) => {
1065
+ let values = json.as_object().ok_or_else(invalid)?;
1066
+ if values.len() > budget.limits.items.saturating_sub(budget.usage.items) {
1067
+ budget.exhausted = true;
1068
+ return Err(format!("{path}: default exceeds maximum item count"));
1069
+ }
1070
+ return values
1071
+ .iter()
1072
+ .map(|(key, json)| {
1073
+ budget.bytes(key.len(), path)?;
1074
+ let value = default_value(
1075
+ json,
1076
+ &map.types,
1077
+ names,
1078
+ depth + 1,
1079
+ &format!("{path}[{key:?}]"),
1080
+ budget,
1081
+ )?;
1082
+ Ok((key.clone(), value))
1083
+ })
1084
+ .collect::<Result<Vec<_>, String>>()
1085
+ .map(Value::Record);
1086
+ }
1087
+ Schema::Enum(enumeration) => {
1088
+ let symbol = json.as_str().ok_or_else(invalid)?;
1089
+ let index = enumeration
1090
+ .symbols
1091
+ .iter()
1092
+ .position(|candidate| candidate == symbol)
1093
+ .ok_or_else(invalid)?;
1094
+ budget.bytes(symbol.len(), path)?;
1095
+ return Ok(Value::Enum(index as u32, symbol.into()));
1096
+ }
1097
+ _ => {
1098
+ let (physical, _) = scalar(schema).ok_or_else(invalid)?;
1099
+ match physical {
1100
+ Scalar::Null if json.is_null() => Value::Null,
1101
+ Scalar::Boolean => Value::Boolean(json.as_bool().ok_or_else(invalid)?),
1102
+ Scalar::Int => Value::Int(
1103
+ i32::try_from(json.as_i64().ok_or_else(invalid)?).map_err(|_| invalid())?,
1104
+ ),
1105
+ Scalar::Long => Value::Long(json.as_i64().ok_or_else(invalid)?),
1106
+ Scalar::Float => Value::Double(json.as_f64().ok_or_else(invalid)?),
1107
+ Scalar::Double => Value::Double(json.as_f64().ok_or_else(invalid)?),
1108
+ Scalar::String => {
1109
+ let value = json.as_str().ok_or_else(invalid)?;
1110
+ budget.bytes(value.len(), path)?;
1111
+ Value::String(value.into())
1112
+ }
1113
+ Scalar::Bytes | Scalar::Fixed(_) => {
1114
+ let value = json.as_str().ok_or_else(invalid)?;
1115
+ let logical = scalar(schema).ok_or_else(invalid)?.1;
1116
+ let length = if logical == Logical::Plain {
1117
+ value.len()
1118
+ } else {
1119
+ value.chars().count()
1120
+ };
1121
+ budget.bytes(length, path)?;
1122
+ if let Scalar::Fixed(size) = physical
1123
+ && size != length
1124
+ {
1125
+ return Err(invalid());
1126
+ }
1127
+ match logical {
1128
+ Logical::Plain => Value::String(value.into()),
1129
+ _ => {
1130
+ let bytes = value
1131
+ .chars()
1132
+ .map(|character| {
1133
+ u8::try_from(u32::from(character)).map_err(|_| invalid())
1134
+ })
1135
+ .collect::<Result<Vec<_>, _>>()?;
1136
+ match physical {
1137
+ Scalar::Fixed(size) => Value::Fixed(size, bytes),
1138
+ _ => Value::Bytes(bytes),
1139
+ }
1140
+ }
1141
+ }
1142
+ }
1143
+ _ => return Err(invalid()),
1144
+ }
1145
+ }
1146
+ };
1147
+ let (_, logical) = scalar(schema).ok_or_else(invalid)?;
1148
+ apply_logical(value, logical).map_err(|error| format!("{path}: invalid default: {error}"))
1149
+ }
1150
+
1151
+ #[cfg(test)]
1152
+ mod tests {
1153
+ use super::*;
1154
+ use apache_avro::schema::ResolvedSchema;
1155
+ use serde_json::json;
1156
+
1157
+ fn parse(json: &str) -> apache_avro::AvroResult<Schema> {
1158
+ crate::validation::initialize().unwrap();
1159
+ Schema::parse_str(json)
1160
+ }
1161
+
1162
+ fn prepare(writer: &Schema, reader: &Schema, limits: Limits) -> Resolution {
1163
+ crate::validation::initialize().unwrap();
1164
+ let writer_names = ResolvedSchema::new(writer).unwrap();
1165
+ let reader_names = ResolvedSchema::new(reader).unwrap();
1166
+ Resolution::new(
1167
+ writer,
1168
+ writer_names.get_names(),
1169
+ reader,
1170
+ reader_names.get_names(),
1171
+ limits.depth,
1172
+ limits.items,
1173
+ limits.bytes,
1174
+ )
1175
+ .unwrap()
1176
+ }
1177
+
1178
+ fn limits() -> Limits {
1179
+ crate::validation::initialize().unwrap();
1180
+ Limits {
1181
+ depth: 64,
1182
+ items: 10_000,
1183
+ bytes: 1_000_000,
1184
+ }
1185
+ }
1186
+
1187
+ fn resolve(writer: &str, reader: &str, value: Value) -> Result<Value, String> {
1188
+ let writer = parse(writer).unwrap();
1189
+ let reader = parse(reader).unwrap();
1190
+ prepare(&writer, &reader, limits())
1191
+ .apply(value)
1192
+ .and_then(|result| match result.failure {
1193
+ Some((_, error)) => Err(error),
1194
+ None => Ok(result.value),
1195
+ })
1196
+ }
1197
+
1198
+ #[test]
1199
+ fn resolution_is_owned_send_and_sync() {
1200
+ fn send_sync<T: Send + Sync>() {}
1201
+ send_sync::<Resolution>();
1202
+ let plan = {
1203
+ let writer = Schema::Int;
1204
+ let reader = Schema::Long;
1205
+ prepare(&writer, &reader, limits())
1206
+ };
1207
+ assert_eq!(plan.apply(Value::Int(7)).unwrap().value, Value::Long(7));
1208
+ }
1209
+
1210
+ #[test]
1211
+ fn named_dependency_plans_survive_dropping_all_schemas() {
1212
+ let plan = {
1213
+ let writer =
1214
+ parse(r#"{"type":"record","name":"old.R","fields":[{"name":"v","type":"int"}]}"#)
1215
+ .unwrap();
1216
+ let reader = parse(r#"{"type":"record","name":"new.R","aliases":["old.R"],"fields":[{"name":"v","type":"long"}]}"#).unwrap();
1217
+ let writer_ref = Schema::Ref {
1218
+ name: Name::new("old.R").unwrap(),
1219
+ };
1220
+ let reader_ref = Schema::Ref {
1221
+ name: Name::new("new.R").unwrap(),
1222
+ };
1223
+ let writer_names = ResolvedSchema::new(&writer).unwrap();
1224
+ let reader_names = ResolvedSchema::new(&reader).unwrap();
1225
+ Resolution::new(
1226
+ &writer_ref,
1227
+ writer_names.get_names(),
1228
+ &reader_ref,
1229
+ reader_names.get_names(),
1230
+ 64,
1231
+ 1_000,
1232
+ 10_000,
1233
+ )
1234
+ .unwrap()
1235
+ };
1236
+ assert_eq!(
1237
+ plan.apply(Value::Record(vec![("v".into(), Value::Int(8))]))
1238
+ .unwrap()
1239
+ .value,
1240
+ Value::Record(vec![("v".into(), Value::Long(8))])
1241
+ );
1242
+ }
1243
+
1244
+ #[test]
1245
+ fn numeric_promotions_are_directional_even_when_the_value_fits() {
1246
+ let schemas = [Schema::Int, Schema::Long, Schema::Float, Schema::Double];
1247
+ let values = [
1248
+ Value::Int(1),
1249
+ Value::Long(1),
1250
+ Value::Float(1.0),
1251
+ Value::Double(1.0),
1252
+ ];
1253
+ for (writer_index, writer) in schemas.iter().enumerate() {
1254
+ for (reader_index, reader) in schemas.iter().enumerate() {
1255
+ let result = prepare(writer, reader, limits())
1256
+ .apply(values[writer_index].clone())
1257
+ .map(|result| result.value);
1258
+ if writer_index <= reader_index {
1259
+ let output = if writer_index < 2 && reader_index >= 2 {
1260
+ writer_index
1261
+ } else {
1262
+ reader_index
1263
+ };
1264
+ assert_eq!(result.unwrap(), values[output]);
1265
+ } else {
1266
+ assert!(result.unwrap_err().contains("cannot resolve"));
1267
+ }
1268
+ }
1269
+ }
1270
+ }
1271
+
1272
+ #[test]
1273
+ fn selected_writer_union_branch_alone_controls_compatibility() {
1274
+ assert_eq!(
1275
+ resolve(
1276
+ r#"["int","string"]"#,
1277
+ r#""long""#,
1278
+ Value::Union(0, Box::new(Value::Int(42)))
1279
+ )
1280
+ .unwrap(),
1281
+ Value::Long(42)
1282
+ );
1283
+ assert!(
1284
+ resolve(
1285
+ r#"["int","string"]"#,
1286
+ r#""long""#,
1287
+ Value::Union(1, Box::new(Value::String("42".into())))
1288
+ )
1289
+ .is_err()
1290
+ );
1291
+ let writer = r#"["null",{"type":"record","name":"R","fields":[]}]"#;
1292
+ let reader =
1293
+ r#"["null",{"type":"record","name":"R","fields":[{"name":"v","type":"int"}]}]"#;
1294
+ assert_eq!(
1295
+ resolve(writer, reader, Value::Union(0, Box::new(Value::Null))).unwrap(),
1296
+ Value::Union(0, Box::new(Value::Null))
1297
+ );
1298
+ assert!(
1299
+ resolve(
1300
+ writer,
1301
+ reader,
1302
+ Value::Union(1, Box::new(Value::Record(vec![])))
1303
+ )
1304
+ .is_err()
1305
+ );
1306
+ }
1307
+
1308
+ #[test]
1309
+ fn reader_union_uses_first_matching_branch_and_remaps_index() {
1310
+ assert_eq!(
1311
+ resolve(
1312
+ r#"["null","int"]"#,
1313
+ r#"["double","long","null"]"#,
1314
+ Value::Union(1, Box::new(Value::Int(9)))
1315
+ )
1316
+ .unwrap(),
1317
+ Value::Union(0, Box::new(Value::Int(9)))
1318
+ );
1319
+ assert_eq!(
1320
+ resolve(
1321
+ r#"["null","int"]"#,
1322
+ r#"["double","long","null"]"#,
1323
+ Value::Union(0, Box::new(Value::Null))
1324
+ )
1325
+ .unwrap(),
1326
+ Value::Union(2, Box::new(Value::Null))
1327
+ );
1328
+ }
1329
+
1330
+ #[test]
1331
+ fn record_aliases_field_aliases_reordering_and_defaults() {
1332
+ let writer = r#"{"type":"record","name":"old.R","fields":[
1333
+ {"name":"old_name","type":"int"},{"name":"ignored","type":"string"}]}"#;
1334
+ let reader = r#"{"type":"record","name":"new.R","aliases":["old.R"],"fields":[
1335
+ {"name":"flag","type":"boolean","default":false},
1336
+ {"name":"new_name","aliases":["old_name"],"type":"long"},
1337
+ {"name":"optional","type":["null","string"],"default":null}]}"#;
1338
+ let plan = prepare(&parse(writer).unwrap(), &parse(reader).unwrap(), limits());
1339
+ let result = plan
1340
+ .apply(Value::Record(vec![
1341
+ ("old_name".into(), Value::Int(12)),
1342
+ ("ignored".into(), Value::String("x".into())),
1343
+ ]))
1344
+ .unwrap();
1345
+ assert_eq!(
1346
+ result.value,
1347
+ Value::Record(vec![
1348
+ ("new_name".into(), Value::Long(12)),
1349
+ ("flag".into(), Value::Null),
1350
+ ("optional".into(), Value::Null)
1351
+ ])
1352
+ );
1353
+ assert_eq!(
1354
+ result
1355
+ .defaults
1356
+ .iter()
1357
+ .map(|(position, value)| (*position, value.as_ref()))
1358
+ .collect::<Vec<_>>(),
1359
+ vec![(1, &json!(false)), (2, &Json::Null)]
1360
+ );
1361
+ }
1362
+
1363
+ #[test]
1364
+ fn repeated_reads_share_owned_default_definitions() {
1365
+ let plan = {
1366
+ let writer = parse(r#"{"type":"record","name":"R","fields":[]}"#).unwrap();
1367
+ let reader = parse(
1368
+ r#"{"type":"record","name":"R","fields":[
1369
+ {"name":"values","type":{"type":"array","items":"long"},"default":[1,2,3]}]}"#,
1370
+ )
1371
+ .unwrap();
1372
+ prepare(&writer, &reader, limits())
1373
+ };
1374
+ let first = plan.apply(Value::Record(vec![])).unwrap();
1375
+ let second = plan.apply(Value::Record(vec![])).unwrap();
1376
+ assert!(Arc::ptr_eq(&first.defaults[0].1, &second.defaults[0].1));
1377
+ assert_eq!(first.defaults[0].1.as_ref(), &json!([1, 2, 3]));
1378
+ assert_eq!(
1379
+ first.value,
1380
+ Value::Record(vec![("values".into(), Value::Null)])
1381
+ );
1382
+ }
1383
+
1384
+ #[test]
1385
+ fn reader_defaults_do_not_replace_incompatible_writer_fields() {
1386
+ let writer = r#"{"type":"record","name":"R","fields":[{"name":"v","type":"string"}]}"#;
1387
+ let reader =
1388
+ r#"{"type":"record","name":"R","fields":[{"name":"v","type":"int","default":7}]}"#;
1389
+ let error = resolve(
1390
+ writer,
1391
+ reader,
1392
+ Value::Record(vec![("v".into(), Value::String("7".into()))]),
1393
+ )
1394
+ .unwrap_err();
1395
+ assert!(error.starts_with("$.v:"));
1396
+ assert!(error.contains("cannot resolve"));
1397
+ }
1398
+
1399
+ #[test]
1400
+ fn names_include_namespace_and_only_reader_aliases_apply() {
1401
+ let writer = r#"{"type":"record","name":"a.R","aliases":["b.R"],"fields":[]}"#;
1402
+ let reader = r#"{"type":"record","name":"b.R","fields":[]}"#;
1403
+ assert!(resolve(writer, reader, Value::Record(vec![])).is_err());
1404
+ assert!(resolve(reader, writer, Value::Record(vec![])).is_ok());
1405
+ }
1406
+
1407
+ #[test]
1408
+ fn recursive_names_compile_once_and_promote_recursively() {
1409
+ let writer = parse(
1410
+ r#"{"type":"record","name":"N","fields":[
1411
+ {"name":"v","type":"int"},{"name":"next","type":["null","N"]}]}"#,
1412
+ )
1413
+ .unwrap();
1414
+ let reader = parse(
1415
+ r#"{"type":"record","name":"N","fields":[
1416
+ {"name":"v","type":"long"},{"name":"next","type":["null","N"]}]}"#,
1417
+ )
1418
+ .unwrap();
1419
+ let tail = Value::Record(vec![
1420
+ ("v".into(), Value::Int(2)),
1421
+ ("next".into(), Value::Union(0, Box::new(Value::Null))),
1422
+ ]);
1423
+ let value = Value::Record(vec![
1424
+ ("v".into(), Value::Int(1)),
1425
+ ("next".into(), Value::Union(1, Box::new(tail))),
1426
+ ]);
1427
+ let plan = prepare(&writer, &reader, limits());
1428
+ assert!(plan.nodes.len() < 10);
1429
+ let Value::Record(output) = plan.apply(value).unwrap().value else {
1430
+ panic!("expected record");
1431
+ };
1432
+ assert_eq!(output[0].1, Value::Long(1));
1433
+ let Value::Union(1, tail) = &output[1].1 else {
1434
+ panic!("expected record union");
1435
+ };
1436
+ let Value::Record(tail) = tail.as_ref() else {
1437
+ panic!("expected tail");
1438
+ };
1439
+ assert_eq!(tail[0].1, Value::Long(2));
1440
+ }
1441
+
1442
+ #[test]
1443
+ fn enum_resolution_remaps_symbols_and_uses_reader_default() {
1444
+ let writer = r#"{"type":"enum","name":"E","symbols":["A","B","C"]}"#;
1445
+ let reader = r#"{"type":"enum","name":"E","symbols":["B","A"],"default":"B"}"#;
1446
+ assert_eq!(
1447
+ resolve(writer, reader, Value::Enum(0, "A".into())).unwrap(),
1448
+ Value::Enum(1, "A".into())
1449
+ );
1450
+ assert_eq!(
1451
+ resolve(writer, reader, Value::Enum(2, "C".into())).unwrap(),
1452
+ Value::Enum(0, "B".into())
1453
+ );
1454
+ assert_eq!(
1455
+ resolve(
1456
+ writer,
1457
+ r#"{"type":"enum","name":"E","symbols":["A"]}"#,
1458
+ Value::Enum(2, "C".into())
1459
+ )
1460
+ .unwrap(),
1461
+ Value::Enum(2, "C".into())
1462
+ );
1463
+ assert!(
1464
+ resolve(
1465
+ writer,
1466
+ r#"{"type":"enum","name":"Other","symbols":["A"]}"#,
1467
+ Value::Enum(0, "A".into())
1468
+ )
1469
+ .is_err()
1470
+ );
1471
+ }
1472
+
1473
+ #[test]
1474
+ fn fixed_requires_name_and_size_with_reader_aliases() {
1475
+ let writer = r#"{"type":"fixed","name":"F","size":2}"#;
1476
+ let reader = r#"{"type":"fixed","name":"G","aliases":["F"],"size":2}"#;
1477
+ assert_eq!(
1478
+ resolve(writer, reader, Value::Fixed(2, vec![0, 255])).unwrap(),
1479
+ Value::Fixed(2, vec![0, 255])
1480
+ );
1481
+ assert!(
1482
+ resolve(
1483
+ writer,
1484
+ r#"{"type":"fixed","name":"G","size":2}"#,
1485
+ Value::Fixed(2, vec![0, 255])
1486
+ )
1487
+ .is_err()
1488
+ );
1489
+ assert!(
1490
+ resolve(
1491
+ writer,
1492
+ r#"{"type":"fixed","name":"F","size":3}"#,
1493
+ Value::Fixed(2, vec![0, 255])
1494
+ )
1495
+ .is_err()
1496
+ );
1497
+ }
1498
+
1499
+ #[test]
1500
+ fn strings_and_bytes_preserve_the_writer_representation() {
1501
+ assert_eq!(
1502
+ resolve(r#""string""#, r#""bytes""#, Value::String("é".into())).unwrap(),
1503
+ Value::String("é".into())
1504
+ );
1505
+ assert_eq!(
1506
+ resolve(r#""bytes""#, r#""string""#, Value::Bytes(vec![0xc3, 0xa9])).unwrap(),
1507
+ Value::Bytes(vec![0xc3, 0xa9])
1508
+ );
1509
+ assert_eq!(
1510
+ resolve(r#""bytes""#, r#""string""#, Value::Bytes(vec![255])).unwrap(),
1511
+ Value::Bytes(vec![255])
1512
+ );
1513
+ }
1514
+
1515
+ #[test]
1516
+ fn logical_values_use_reader_annotations_and_underlying_promotions() {
1517
+ assert_eq!(
1518
+ prepare(&Schema::Date, &Schema::Long, limits())
1519
+ .apply(Value::Date(-1))
1520
+ .unwrap()
1521
+ .value,
1522
+ Value::Long(-1)
1523
+ );
1524
+ assert_eq!(
1525
+ prepare(&Schema::Int, &Schema::Date, limits())
1526
+ .apply(Value::Int(42))
1527
+ .unwrap()
1528
+ .value,
1529
+ Value::Date(42)
1530
+ );
1531
+ assert_eq!(
1532
+ prepare(&Schema::TimestampMillis, &Schema::TimestampMicros, limits())
1533
+ .apply(Value::TimestampMillis(123))
1534
+ .unwrap()
1535
+ .value,
1536
+ Value::TimestampMicros(123)
1537
+ );
1538
+ assert!(
1539
+ prepare(&Schema::Long, &Schema::Date, limits())
1540
+ .apply(Value::Long(1))
1541
+ .map(|result| result.value)
1542
+ .is_err()
1543
+ );
1544
+ let uuid = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
1545
+ assert_eq!(
1546
+ prepare(&Schema::String, &Schema::Uuid(UuidSchema::String), limits())
1547
+ .apply(Value::String(uuid.to_string()))
1548
+ .unwrap()
1549
+ .value,
1550
+ Value::Uuid(uuid)
1551
+ );
1552
+ assert!(
1553
+ prepare(&Schema::TimeMillis, &Schema::Int, limits())
1554
+ .apply(Value::TimeMillis(-1))
1555
+ .map(|result| result.value)
1556
+ .is_err()
1557
+ );
1558
+ assert!(
1559
+ prepare(&Schema::Long, &Schema::TimeMicros, limits())
1560
+ .apply(Value::Long(86_400_000_000))
1561
+ .map(|result| result.value)
1562
+ .is_err()
1563
+ );
1564
+ }
1565
+
1566
+ #[test]
1567
+ fn decimal_resolution_preserves_metadata_and_rejects_overflow() {
1568
+ let decimal = r#"{"type":"bytes","logicalType":"decimal","precision":2,"scale":1}"#;
1569
+ assert_eq!(
1570
+ resolve(r#""bytes""#, decimal, Value::Bytes(vec![99])).unwrap(),
1571
+ Value::Decimal(Decimal::from(vec![99]))
1572
+ );
1573
+ assert!(resolve(r#""bytes""#, decimal, Value::Bytes(vec![100])).is_err());
1574
+ assert!(resolve(r#""bytes""#, decimal, Value::Bytes(vec![156])).is_err());
1575
+ assert_eq!(
1576
+ resolve(
1577
+ decimal,
1578
+ r#""bytes""#,
1579
+ Value::Decimal(Decimal::from(vec![157]))
1580
+ )
1581
+ .unwrap(),
1582
+ Value::Bytes(vec![157])
1583
+ );
1584
+ assert!(
1585
+ resolve(
1586
+ decimal,
1587
+ r#""bytes""#,
1588
+ Value::Decimal(Decimal::from(vec![100]))
1589
+ )
1590
+ .is_err()
1591
+ );
1592
+ assert!(
1593
+ resolve(
1594
+ decimal,
1595
+ r#"{"type":"bytes","logicalType":"decimal","precision":2,"scale":0}"#,
1596
+ Value::Decimal(Decimal::from(vec![99]))
1597
+ )
1598
+ .is_err()
1599
+ );
1600
+ assert!(
1601
+ resolve(
1602
+ decimal,
1603
+ r#"{"type":"bytes","logicalType":"decimal","precision":3,"scale":1}"#,
1604
+ Value::Decimal(Decimal::from(vec![99]))
1605
+ )
1606
+ .is_err()
1607
+ );
1608
+ }
1609
+
1610
+ #[test]
1611
+ fn big_decimal_resolves_bytes_without_expanding_extreme_scales() {
1612
+ for coefficient in [0, 127, 128, -1, -128, -129] {
1613
+ for scale in [0, 2, -3, 5000, -5000, i64::MIN, i64::MAX] {
1614
+ let number = apache_avro::BigDecimal::new(coefficient.into(), scale);
1615
+ let bytes = crate::big_decimal::encode(&number).unwrap();
1616
+ assert_eq!(
1617
+ prepare(&Schema::BigDecimal, &Schema::Bytes, limits())
1618
+ .apply(Value::BigDecimal(number.clone()))
1619
+ .unwrap()
1620
+ .value,
1621
+ Value::Bytes(bytes.clone())
1622
+ );
1623
+ for (writer, value) in [
1624
+ (Schema::Bytes, Value::Bytes(bytes)),
1625
+ (Schema::BigDecimal, Value::BigDecimal(number)),
1626
+ ] {
1627
+ let Value::BigDecimal(decoded) =
1628
+ prepare(&writer, &Schema::BigDecimal, limits())
1629
+ .apply(value)
1630
+ .unwrap()
1631
+ .value
1632
+ else {
1633
+ panic!("expected big-decimal");
1634
+ };
1635
+ assert_eq!(
1636
+ decoded.as_bigint_and_exponent(),
1637
+ (coefficient.into(), scale)
1638
+ );
1639
+ }
1640
+ }
1641
+ }
1642
+ }
1643
+
1644
+ #[test]
1645
+ fn big_decimal_string_promotions_preserve_bytes_and_selected_union_branch() {
1646
+ let bytes = vec![2, 7, 4];
1647
+ let number = apache_avro::BigDecimal::new(7.into(), 2);
1648
+ assert_eq!(
1649
+ prepare(&Schema::BigDecimal, &Schema::String, limits())
1650
+ .apply(Value::BigDecimal(number.clone()))
1651
+ .unwrap()
1652
+ .value,
1653
+ Value::Bytes(bytes.clone())
1654
+ );
1655
+ assert_eq!(
1656
+ prepare(&Schema::String, &Schema::BigDecimal, limits())
1657
+ .apply(Value::String(String::from_utf8(bytes).unwrap()))
1658
+ .unwrap()
1659
+ .value,
1660
+ Value::BigDecimal(number.clone())
1661
+ );
1662
+ assert_eq!(
1663
+ prepare(&Schema::BigDecimal, &Schema::String, limits())
1664
+ .apply(Value::BigDecimal(apache_avro::BigDecimal::from(-1)))
1665
+ .unwrap()
1666
+ .value,
1667
+ Value::Bytes(crate::big_decimal::encode(&apache_avro::BigDecimal::from(-1)).unwrap())
1668
+ );
1669
+ let writer = parse(r#"["long",{"type":"bytes","logicalType":"big-decimal"}]"#).unwrap();
1670
+ let reader =
1671
+ parse(r#"["null",{"type":"bytes","logicalType":"big-decimal"},"string"]"#).unwrap();
1672
+ let plan = prepare(&writer, &reader, limits());
1673
+ assert_eq!(
1674
+ plan.apply(Value::Union(1, Box::new(Value::BigDecimal(number.clone()))))
1675
+ .unwrap()
1676
+ .value,
1677
+ Value::Union(1, Box::new(Value::BigDecimal(number)))
1678
+ );
1679
+ assert!(
1680
+ plan.apply(Value::Union(0, Box::new(Value::Long(7))))
1681
+ .map(|result| result.value)
1682
+ .is_err()
1683
+ );
1684
+ }
1685
+
1686
+ #[test]
1687
+ fn big_decimal_resolution_rejects_malformed_inner_bytes_and_enforces_budget() {
1688
+ let plan = prepare(&Schema::Bytes, &Schema::BigDecimal, limits());
1689
+ for bytes in [
1690
+ vec![],
1691
+ vec![0, 0],
1692
+ vec![2],
1693
+ vec![2, 7],
1694
+ vec![2, 7, 4, 0],
1695
+ vec![1, 0],
1696
+ ] {
1697
+ assert!(
1698
+ plan.apply(Value::Bytes(bytes))
1699
+ .map(|result| result.value)
1700
+ .is_err()
1701
+ );
1702
+ }
1703
+ let mut limits = limits();
1704
+ limits.bytes = 2;
1705
+ assert!(
1706
+ prepare(&Schema::Bytes, &Schema::BigDecimal, limits)
1707
+ .apply(Value::Bytes(vec![2, 7, 4]))
1708
+ .unwrap_err()
1709
+ .contains("byte count")
1710
+ );
1711
+ assert!(
1712
+ prepare(&Schema::BigDecimal, &Schema::Bytes, limits)
1713
+ .apply(Value::BigDecimal(apache_avro::BigDecimal::from(7)))
1714
+ .unwrap_err()
1715
+ .contains("byte count")
1716
+ );
1717
+ }
1718
+
1719
+ #[test]
1720
+ fn defaults_preserve_json_floats_and_utf8_bytes() {
1721
+ let mut budget = Budget::new(limits());
1722
+ let names = NamesRef::new();
1723
+ assert_eq!(
1724
+ default_value(
1725
+ &json!("ÿ\u{0}"),
1726
+ &Schema::Bytes,
1727
+ &names,
1728
+ 0,
1729
+ "$",
1730
+ &mut budget
1731
+ )
1732
+ .unwrap(),
1733
+ Value::String("ÿ\u{0}".into())
1734
+ );
1735
+ assert_eq!(
1736
+ default_value(&json!("Ā"), &Schema::Bytes, &names, 0, "$", &mut budget).unwrap(),
1737
+ Value::String("Ā".into())
1738
+ );
1739
+ assert_eq!(
1740
+ default_value(&json!(0.1), &Schema::Float, &names, 0, "$", &mut budget).unwrap(),
1741
+ Value::Double(0.1)
1742
+ );
1743
+ assert!(default_value(&json!(1.0), &Schema::Int, &names, 0, "$", &mut budget).is_err());
1744
+ assert!(
1745
+ default_value(
1746
+ &json!(2_147_483_648_i64),
1747
+ &Schema::Int,
1748
+ &names,
1749
+ 0,
1750
+ "$",
1751
+ &mut budget
1752
+ )
1753
+ .is_err()
1754
+ );
1755
+ assert!(
1756
+ default_value(
1757
+ &json!("false"),
1758
+ &Schema::Boolean,
1759
+ &names,
1760
+ 0,
1761
+ "$",
1762
+ &mut budget
1763
+ )
1764
+ .is_err()
1765
+ );
1766
+ assert_eq!(
1767
+ default_value(&json!(-1), &Schema::Date, &names, 0, "$", &mut budget).unwrap(),
1768
+ Value::Date(-1)
1769
+ );
1770
+ }
1771
+
1772
+ #[test]
1773
+ fn union_default_uses_first_matching_branch_under_avro_1_12() {
1774
+ let schema = parse(r#"["null","int","long"]"#).unwrap();
1775
+ assert_eq!(
1776
+ default_value(
1777
+ &json!(42),
1778
+ &schema,
1779
+ &NamesRef::new(),
1780
+ 0,
1781
+ "$",
1782
+ &mut Budget::new(limits())
1783
+ )
1784
+ .unwrap(),
1785
+ Value::Union(1, Box::new(Value::Int(42)))
1786
+ );
1787
+ }
1788
+
1789
+ #[test]
1790
+ fn failed_union_defaults_cannot_reset_the_preparation_budget() {
1791
+ let schema = parse(r#"["null","boolean","int","string"]"#).unwrap();
1792
+ let mut limits = limits();
1793
+ limits.items = 3;
1794
+ let error = default_value(
1795
+ &json!("value"),
1796
+ &schema,
1797
+ &NamesRef::new(),
1798
+ 0,
1799
+ "$",
1800
+ &mut Budget::new(limits),
1801
+ )
1802
+ .unwrap_err();
1803
+ assert!(error.contains("item count"));
1804
+ }
1805
+
1806
+ #[test]
1807
+ fn default_expansion_counts_against_whole_result_budget() {
1808
+ let writer = parse(r#"{"type":"record","name":"R","fields":[]}"#).unwrap();
1809
+ let reader = parse(
1810
+ r#"{"type":"record","name":"R","fields":[
1811
+ {"name":"v","type":{"type":"array","items":"int"},"default":[1,2]}]}"#,
1812
+ )
1813
+ .unwrap();
1814
+ let mut limits = limits();
1815
+ limits.items = 3;
1816
+ assert!(
1817
+ prepare(&writer, &reader, limits)
1818
+ .apply(Value::Record(vec![]))
1819
+ .unwrap_err()
1820
+ .contains("item count")
1821
+ );
1822
+ }
1823
+
1824
+ #[test]
1825
+ fn implicit_defaults_cannot_expand_an_exponential_schema_graph() {
1826
+ crate::validation::initialize().unwrap();
1827
+ use apache_avro::schema::{RecordField, RecordSchema};
1828
+
1829
+ let mut dependencies = vec![Schema::Record(
1830
+ RecordSchema::builder()
1831
+ .name(Name::new("Tree0").unwrap())
1832
+ .build(),
1833
+ )];
1834
+ for level in 1..24 {
1835
+ let fields = ["left", "right"].map(|name| {
1836
+ RecordField::builder()
1837
+ .name(name)
1838
+ .schema(Schema::Ref {
1839
+ name: Name::new(format!("Tree{}", level - 1)).unwrap(),
1840
+ })
1841
+ .default(json!({}))
1842
+ .build()
1843
+ });
1844
+ dependencies.push(Schema::Record(
1845
+ RecordSchema::builder()
1846
+ .name(Name::new(format!("Tree{level}")).unwrap())
1847
+ .fields(fields.to_vec())
1848
+ .build(),
1849
+ ));
1850
+ }
1851
+ let writer = Schema::Record(
1852
+ RecordSchema::builder()
1853
+ .name(Name::new("Root").unwrap())
1854
+ .build(),
1855
+ );
1856
+ let reader = Schema::Record(
1857
+ RecordSchema::builder()
1858
+ .name(Name::new("Root").unwrap())
1859
+ .fields(vec![
1860
+ RecordField::builder()
1861
+ .name("tree")
1862
+ .schema(Schema::Ref {
1863
+ name: Name::new("Tree23").unwrap(),
1864
+ })
1865
+ .default(json!({}))
1866
+ .build(),
1867
+ ])
1868
+ .build(),
1869
+ );
1870
+ let writer_names = ResolvedSchema::new(&writer).unwrap();
1871
+ let reader_names =
1872
+ ResolvedSchema::new_with_schemata(dependencies.iter().chain([&reader]).collect())
1873
+ .unwrap();
1874
+ let error = Resolution::new(
1875
+ &writer,
1876
+ writer_names.get_names(),
1877
+ &reader,
1878
+ reader_names.get_names(),
1879
+ 64,
1880
+ 1_000,
1881
+ 1_000_000,
1882
+ )
1883
+ .err()
1884
+ .expect("bounded expansion must fail");
1885
+ assert!(error.contains("item count"));
1886
+ }
1887
+
1888
+ #[test]
1889
+ fn depth_limit_includes_defaults_at_their_result_position() {
1890
+ let writer = parse(
1891
+ r#"{"type":"record","name":"R","fields":[
1892
+ {"name":"child","type":{"type":"record","name":"C","fields":[]}}]}"#,
1893
+ )
1894
+ .unwrap();
1895
+ let reader = parse(
1896
+ r#"{"type":"record","name":"R","fields":[
1897
+ {"name":"child","type":{"type":"record","name":"C","fields":[
1898
+ {"name":"v","type":{"type":"array","items":"int"},"default":[1]}]}}]}"#,
1899
+ )
1900
+ .unwrap();
1901
+ let mut limits = limits();
1902
+ limits.depth = 2;
1903
+ let error = prepare(&writer, &reader, limits)
1904
+ .apply(Value::Record(vec![("child".into(), Value::Record(vec![]))]))
1905
+ .unwrap_err();
1906
+ assert!(error.starts_with("$.child.v:"));
1907
+ assert!(error.contains("depth"));
1908
+ }
1909
+
1910
+ #[test]
1911
+ fn array_and_map_contents_are_resolved_recursively() {
1912
+ let writer = r#"{"type":"map","values":{"type":"array","items":"int"}}"#;
1913
+ let reader = r#"{"type":"map","values":{"type":"array","items":"double"}}"#;
1914
+ assert_eq!(
1915
+ resolve(
1916
+ writer,
1917
+ reader,
1918
+ Value::Record(Vec::from([("a".into(), Value::Array(vec![Value::Int(1)]))]))
1919
+ )
1920
+ .unwrap(),
1921
+ Value::Record(Vec::from([("a".into(), Value::Array(vec![Value::Int(1)]))]))
1922
+ );
1923
+ assert!(
1924
+ resolve(
1925
+ r#"{"type":"array","items":"string"}"#,
1926
+ r#"{"type":"array","items":"int"}"#,
1927
+ Value::Array(vec![])
1928
+ )
1929
+ .is_err()
1930
+ );
1931
+ }
1932
+ }