dry-validation-rust 0.1.0.pre5

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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +74 -0
  3. data/LICENSE +21 -0
  4. data/NOTICE.md +28 -0
  5. data/README.md +459 -0
  6. data/docs/ARCHITECTURE.md +256 -0
  7. data/docs/COMPATIBILITY.md +198 -0
  8. data/docs/FEASIBILITY.md +207 -0
  9. data/docs/SUPPORT_MATRIX.md +66 -0
  10. data/docs/VERIFICATION.md +128 -0
  11. data/dry-validation-rust.gemspec +58 -0
  12. data/ext/dry_validation_rust/Cargo.lock +809 -0
  13. data/ext/dry_validation_rust/Cargo.toml +44 -0
  14. data/ext/dry_validation_rust/benches/coercion.rs +77 -0
  15. data/ext/dry_validation_rust/benches/full_schema.rs +189 -0
  16. data/ext/dry_validation_rust/benches/plan_compile.rs +37 -0
  17. data/ext/dry_validation_rust/benches/predicates.rs +105 -0
  18. data/ext/dry_validation_rust/extconf.rb +29 -0
  19. data/ext/dry_validation_rust/src/coercion.rs +515 -0
  20. data/ext/dry_validation_rust/src/engine.rs +416 -0
  21. data/ext/dry_validation_rust/src/error.rs +82 -0
  22. data/ext/dry_validation_rust/src/extract_primitive.rs +23 -0
  23. data/ext/dry_validation_rust/src/generated_predicates.rs +33 -0
  24. data/ext/dry_validation_rust/src/lib.rs +228 -0
  25. data/ext/dry_validation_rust/src/plan.rs +611 -0
  26. data/ext/dry_validation_rust/src/predicates.rs +449 -0
  27. data/ext/dry_validation_rust/src/ruby_bridge.rs +78 -0
  28. data/lib/dry/schema.rb +6 -0
  29. data/lib/dry/validation/rust/block_keyword_parameters.rb +20 -0
  30. data/lib/dry/validation/rust/config.rb +74 -0
  31. data/lib/dry/validation/rust/contract/result.rb +180 -0
  32. data/lib/dry/validation/rust/contract/values.rb +73 -0
  33. data/lib/dry/validation/rust/contract.rb +400 -0
  34. data/lib/dry/validation/rust/errors.rb +14 -0
  35. data/lib/dry/validation/rust/evaluator.rb +295 -0
  36. data/lib/dry/validation/rust/failures.rb +57 -0
  37. data/lib/dry/validation/rust/generated_predicates.rb +14 -0
  38. data/lib/dry/validation/rust/macros.rb +45 -0
  39. data/lib/dry/validation/rust/message.rb +41 -0
  40. data/lib/dry/validation/rust/message_backend.rb +115 -0
  41. data/lib/dry/validation/rust/message_set.rb +159 -0
  42. data/lib/dry/validation/rust/native.rb +25 -0
  43. data/lib/dry/validation/rust/path.rb +65 -0
  44. data/lib/dry/validation/rust/path_trie.rb +57 -0
  45. data/lib/dry/validation/rust/result.rb +3 -0
  46. data/lib/dry/validation/rust/rule.rb +62 -0
  47. data/lib/dry/validation/rust/schema/dsl.rb +76 -0
  48. data/lib/dry/validation/rust/schema/field_builder.rb +156 -0
  49. data/lib/dry/validation/rust/schema/field_definition.rb +99 -0
  50. data/lib/dry/validation/rust/schema/predicate_block.rb +56 -0
  51. data/lib/dry/validation/rust/schema/processor_hooks.rb +46 -0
  52. data/lib/dry/validation/rust/schema/result.rb +67 -0
  53. data/lib/dry/validation/rust/schema/ruby_type_processor.rb +44 -0
  54. data/lib/dry/validation/rust/schema.rb +323 -0
  55. data/lib/dry/validation/rust/values.rb +3 -0
  56. data/lib/dry/validation/rust/version.rb +10 -0
  57. data/lib/dry/validation/rust.rb +55 -0
  58. data/lib/dry/validation.rb +66 -0
  59. data/lib/dry-schema.rb +3 -0
  60. data/lib/dry-validation.rb +3 -0
  61. data/lib/dry_validation_rust.rb +3 -0
  62. data/predicates.yml +67 -0
  63. data/rust-toolchain.toml +9 -0
  64. metadata +260 -0
@@ -0,0 +1,611 @@
1
+ use magnus::{Error, Ruby};
2
+ use serde::{
3
+ de::{DeserializeSeed, EnumAccess, Error as DeError, MapAccess, SeqAccess, Visitor},
4
+ Deserialize,
5
+ };
6
+ use std::{collections::HashSet, fmt};
7
+
8
+ const MAX_PLAN_JSON_NESTING: usize = 512;
9
+
10
+ #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
11
+ #[serde(rename_all = "snake_case")]
12
+ pub(crate) enum Mode {
13
+ Schema,
14
+ Params,
15
+ Json,
16
+ }
17
+
18
+ #[derive(Debug, Clone, PartialEq)]
19
+ pub(crate) enum PredicateArg {
20
+ Bool(bool),
21
+ Int(i64),
22
+ Float(f64),
23
+ Str(String),
24
+ List(Vec<PredicateArg>),
25
+ }
26
+
27
+ impl<'de> Deserialize<'de> for PredicateArg {
28
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
29
+ where
30
+ D: serde::Deserializer<'de>,
31
+ {
32
+ struct PredicateArgVisitor;
33
+
34
+ impl<'de> Visitor<'de> for PredicateArgVisitor {
35
+ type Value = PredicateArg;
36
+
37
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
38
+ formatter.write_str("a boolean, integer, float, string, or list predicate argument")
39
+ }
40
+
41
+ fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
42
+ Ok(PredicateArg::Bool(value))
43
+ }
44
+
45
+ fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
46
+ Ok(PredicateArg::Int(value))
47
+ }
48
+
49
+ fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
50
+ where
51
+ E: DeError,
52
+ {
53
+ i64::try_from(value)
54
+ .map(PredicateArg::Int)
55
+ .map_err(|_| E::custom("predicate integer exceeds i64 range"))
56
+ }
57
+
58
+ fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> {
59
+ Ok(PredicateArg::Float(value))
60
+ }
61
+
62
+ fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
63
+ Ok(PredicateArg::Str(value.to_owned()))
64
+ }
65
+
66
+ fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
67
+ Ok(PredicateArg::Str(value))
68
+ }
69
+
70
+ fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
71
+ where
72
+ A: SeqAccess<'de>,
73
+ {
74
+ let mut values = Vec::new();
75
+ while let Some(value) = sequence.next_element()? {
76
+ values.push(value);
77
+ }
78
+ Ok(PredicateArg::List(values))
79
+ }
80
+
81
+ fn visit_unit<E>(self) -> Result<Self::Value, E>
82
+ where
83
+ E: DeError,
84
+ {
85
+ // JSON null was previously an invalid native predicate operand; reject it at parse time.
86
+ Err(E::custom("predicate argument must not be null"))
87
+ }
88
+ }
89
+
90
+ deserializer.deserialize_any(PredicateArgVisitor)
91
+ }
92
+ }
93
+
94
+ include!("generated_predicates.rs");
95
+
96
+ #[derive(Debug)]
97
+ pub(crate) struct PredicatePlan {
98
+ pub(crate) name: String,
99
+ pub(crate) op: PredicateOp,
100
+ pub(crate) argument: PredicateArg,
101
+ }
102
+
103
+ impl<'de> Deserialize<'de> for PredicatePlan {
104
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105
+ where
106
+ D: serde::Deserializer<'de>,
107
+ {
108
+ #[derive(Deserialize)]
109
+ struct RawPredicatePlan {
110
+ name: String,
111
+ argument: PredicateArg,
112
+ }
113
+
114
+ let raw = RawPredicatePlan::deserialize(deserializer)?;
115
+ Ok(Self {
116
+ op: PredicateOp::from_name(&raw.name),
117
+ name: raw.name,
118
+ argument: raw.argument,
119
+ })
120
+ }
121
+ }
122
+
123
+ #[derive(Debug, Deserialize)]
124
+ pub(crate) struct FieldPlan {
125
+ pub(crate) name: Option<String>,
126
+ pub(crate) required: bool,
127
+ pub(crate) nullable: bool,
128
+ pub(crate) filled: bool,
129
+ #[serde(rename = "type")]
130
+ pub(crate) kind: String,
131
+ pub(crate) member: Option<Box<FieldPlan>>,
132
+ #[serde(default)]
133
+ pub(crate) children: Vec<FieldPlan>,
134
+ #[serde(default)]
135
+ pub(crate) predicates: Vec<PredicatePlan>,
136
+ }
137
+
138
+ #[derive(Debug, Deserialize)]
139
+ pub(crate) struct SchemaPlan {
140
+ pub(crate) engine_version: u32,
141
+ pub(crate) mode: Mode,
142
+ #[serde(default)]
143
+ pub(crate) validate_keys: bool,
144
+ pub(crate) fields: Vec<FieldPlan>,
145
+ #[serde(skip)]
146
+ pub(crate) used_kinds: HashSet<String>,
147
+ }
148
+
149
+ pub(crate) fn parse_plan(ruby: &Ruby, json: &str) -> Result<SchemaPlan, Error> {
150
+ deserialize_plan(json).map_err(|message| Error::new(ruby.exception_arg_error(), message))
151
+ }
152
+
153
+ pub(crate) fn deserialize_plan(json: &str) -> Result<SchemaPlan, String> {
154
+ let mut deserializer = serde_json::Deserializer::from_str(json);
155
+ deserializer.disable_recursion_limit();
156
+ let mut plan = SchemaPlan::deserialize(DepthLimitedDeserializer::new(&mut deserializer, 0))
157
+ .map_err(|error| format!("invalid native schema plan: {error}"))?;
158
+ if plan.engine_version != 1 {
159
+ return Err(format!(
160
+ "unsupported schema engine version {}; expected 1",
161
+ plan.engine_version
162
+ ));
163
+ }
164
+ plan.used_kinds = collect_used_kinds(&plan.fields);
165
+ Ok(plan)
166
+ }
167
+
168
+ struct DepthLimitedDeserializer<D> {
169
+ inner: D,
170
+ depth: usize,
171
+ }
172
+
173
+ impl<D> DepthLimitedDeserializer<D> {
174
+ fn new(inner: D, depth: usize) -> Self {
175
+ Self { inner, depth }
176
+ }
177
+ }
178
+
179
+ impl<'de, D> serde::Deserializer<'de> for DepthLimitedDeserializer<D>
180
+ where
181
+ D: serde::Deserializer<'de>,
182
+ {
183
+ type Error = D::Error;
184
+
185
+ serde::forward_to_deserialize_any! {
186
+ bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes byte_buf unit
187
+ unit_struct newtype_struct seq tuple tuple_struct map struct identifier ignored_any
188
+ }
189
+
190
+ fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
191
+ where
192
+ V: Visitor<'de>,
193
+ {
194
+ self.inner
195
+ .deserialize_any(DepthLimitedVisitor::new(visitor, self.depth))
196
+ }
197
+
198
+ fn deserialize_enum<V>(
199
+ self,
200
+ name: &'static str,
201
+ variants: &'static [&'static str],
202
+ visitor: V,
203
+ ) -> Result<V::Value, Self::Error>
204
+ where
205
+ V: Visitor<'de>,
206
+ {
207
+ self.inner.deserialize_enum(
208
+ name,
209
+ variants,
210
+ DepthLimitedVisitor::new(visitor, self.depth),
211
+ )
212
+ }
213
+
214
+ fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
215
+ where
216
+ V: Visitor<'de>,
217
+ {
218
+ self.inner
219
+ .deserialize_option(DepthLimitedVisitor::new(visitor, self.depth))
220
+ }
221
+ }
222
+
223
+ struct DepthLimitedVisitor<V> {
224
+ inner: V,
225
+ depth: usize,
226
+ }
227
+
228
+ impl<V> DepthLimitedVisitor<V> {
229
+ fn new(inner: V, depth: usize) -> Self {
230
+ Self { inner, depth }
231
+ }
232
+
233
+ fn nested_depth<E: DeError>(&self) -> Result<usize, E> {
234
+ let depth = self.depth + 1;
235
+ if depth > MAX_PLAN_JSON_NESTING {
236
+ Err(E::custom(format!(
237
+ "native schema plan nesting exceeds limit ({MAX_PLAN_JSON_NESTING})"
238
+ )))
239
+ } else {
240
+ Ok(depth)
241
+ }
242
+ }
243
+ }
244
+
245
+ impl<'de, V> Visitor<'de> for DepthLimitedVisitor<V>
246
+ where
247
+ V: Visitor<'de>,
248
+ {
249
+ type Value = V::Value;
250
+
251
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
252
+ self.inner.expecting(formatter)
253
+ }
254
+
255
+ fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
256
+ where
257
+ E: DeError,
258
+ {
259
+ self.inner.visit_bool(value)
260
+ }
261
+
262
+ fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
263
+ where
264
+ E: DeError,
265
+ {
266
+ self.inner.visit_i64(value)
267
+ }
268
+
269
+ fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
270
+ where
271
+ E: DeError,
272
+ {
273
+ self.inner.visit_u64(value)
274
+ }
275
+
276
+ fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
277
+ where
278
+ E: DeError,
279
+ {
280
+ self.inner.visit_f64(value)
281
+ }
282
+
283
+ fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
284
+ where
285
+ E: DeError,
286
+ {
287
+ self.inner.visit_str(value)
288
+ }
289
+
290
+ fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
291
+ where
292
+ E: DeError,
293
+ {
294
+ self.inner.visit_borrowed_str(value)
295
+ }
296
+
297
+ fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
298
+ where
299
+ E: DeError,
300
+ {
301
+ self.inner.visit_string(value)
302
+ }
303
+
304
+ fn visit_none<E>(self) -> Result<Self::Value, E>
305
+ where
306
+ E: DeError,
307
+ {
308
+ self.inner.visit_none()
309
+ }
310
+
311
+ fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
312
+ where
313
+ D: serde::Deserializer<'de>,
314
+ {
315
+ self.inner
316
+ .visit_some(DepthLimitedDeserializer::new(deserializer, self.depth))
317
+ }
318
+
319
+ fn visit_unit<E>(self) -> Result<Self::Value, E>
320
+ where
321
+ E: DeError,
322
+ {
323
+ self.inner.visit_unit()
324
+ }
325
+
326
+ fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
327
+ where
328
+ A: EnumAccess<'de>,
329
+ {
330
+ self.inner.visit_enum(data)
331
+ }
332
+
333
+ fn visit_seq<A>(self, sequence: A) -> Result<Self::Value, A::Error>
334
+ where
335
+ A: SeqAccess<'de>,
336
+ {
337
+ let depth = self.nested_depth()?;
338
+ self.inner
339
+ .visit_seq(DepthLimitedSeqAccess { sequence, depth })
340
+ }
341
+
342
+ fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
343
+ where
344
+ A: MapAccess<'de>,
345
+ {
346
+ let depth = self.nested_depth()?;
347
+ self.inner.visit_map(DepthLimitedMapAccess { map, depth })
348
+ }
349
+ }
350
+
351
+ struct DepthLimitedSeed<S> {
352
+ seed: S,
353
+ depth: usize,
354
+ }
355
+
356
+ impl<'de, S> DeserializeSeed<'de> for DepthLimitedSeed<S>
357
+ where
358
+ S: DeserializeSeed<'de>,
359
+ {
360
+ type Value = S::Value;
361
+
362
+ fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
363
+ where
364
+ D: serde::Deserializer<'de>,
365
+ {
366
+ self.seed
367
+ .deserialize(DepthLimitedDeserializer::new(deserializer, self.depth))
368
+ }
369
+ }
370
+
371
+ struct DepthLimitedSeqAccess<A> {
372
+ sequence: A,
373
+ depth: usize,
374
+ }
375
+
376
+ impl<'de, A> SeqAccess<'de> for DepthLimitedSeqAccess<A>
377
+ where
378
+ A: SeqAccess<'de>,
379
+ {
380
+ type Error = A::Error;
381
+
382
+ fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
383
+ where
384
+ T: DeserializeSeed<'de>,
385
+ {
386
+ self.sequence.next_element_seed(DepthLimitedSeed {
387
+ seed,
388
+ depth: self.depth,
389
+ })
390
+ }
391
+ }
392
+
393
+ struct DepthLimitedMapAccess<A> {
394
+ map: A,
395
+ depth: usize,
396
+ }
397
+
398
+ impl<'de, A> MapAccess<'de> for DepthLimitedMapAccess<A>
399
+ where
400
+ A: MapAccess<'de>,
401
+ {
402
+ type Error = A::Error;
403
+
404
+ fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
405
+ where
406
+ K: DeserializeSeed<'de>,
407
+ {
408
+ self.map.next_key_seed(seed)
409
+ }
410
+
411
+ fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
412
+ where
413
+ V: DeserializeSeed<'de>,
414
+ {
415
+ self.map.next_value_seed(DepthLimitedSeed {
416
+ seed,
417
+ depth: self.depth,
418
+ })
419
+ }
420
+ }
421
+
422
+ fn collect_used_kinds(fields: &[FieldPlan]) -> HashSet<String> {
423
+ fn collect(fields: &[FieldPlan], kinds: &mut HashSet<String>) {
424
+ for field in fields {
425
+ kinds.insert(field.kind.clone());
426
+ collect(&field.children, kinds);
427
+ if let Some(member) = &field.member {
428
+ collect(std::slice::from_ref(member.as_ref()), kinds);
429
+ }
430
+ }
431
+ }
432
+
433
+ let mut kinds = HashSet::new();
434
+ collect(fields, &mut kinds);
435
+ kinds
436
+ }
437
+
438
+ #[cfg(test)]
439
+ mod tests {
440
+ use super::*;
441
+
442
+ #[test]
443
+ fn plan_json_nesting_limit_rejects_the_513th_container_during_deserialization() {
444
+ let argument_nesting = MAX_PLAN_JSON_NESTING - 5 + 1;
445
+ let over_limit = format!(
446
+ r#"{{"engine_version":1,"mode":"params","fields":[{{"name":"value","required":true,"nullable":false,"filled":false,"type":"string","member":null,"predicates":[{{"name":"custom","argument":{}null{}}}]}}]}}"#,
447
+ "[".repeat(argument_nesting),
448
+ "]".repeat(argument_nesting)
449
+ );
450
+
451
+ assert!(deserialize_plan(&over_limit)
452
+ .unwrap_err()
453
+ .contains("nesting exceeds limit (512)"));
454
+ }
455
+
456
+ #[test]
457
+ fn plan_deserializes_into_typed_fields() {
458
+ let json = r#"{
459
+ "engine_version": 1,
460
+ "mode": "params",
461
+ "fields": [{
462
+ "name": "age",
463
+ "required": true,
464
+ "nullable": false,
465
+ "filled": true,
466
+ "type": "integer",
467
+ "member": null,
468
+ "children": [],
469
+ "predicates": [{"name": "gteq", "argument": 18}]
470
+ }]
471
+ }"#;
472
+ let plan: SchemaPlan = serde_json::from_str(json).expect("valid plan");
473
+
474
+ assert_eq!(plan.engine_version, 1);
475
+ assert_eq!(plan.mode, Mode::Params);
476
+ assert_eq!(plan.fields.len(), 1);
477
+ assert_eq!(plan.fields[0].name.as_deref(), Some("age"));
478
+ assert_eq!(plan.fields[0].predicates[0].name, "gteq");
479
+ assert_eq!(plan.fields[0].predicates[0].op, PredicateOp::Gteq);
480
+ assert_eq!(plan.fields[0].predicates[0].argument, PredicateArg::Int(18));
481
+ }
482
+
483
+ #[test]
484
+ fn plan_deserializes_all_supported_predicate_argument_shapes() {
485
+ let json = serde_json::json!({
486
+ "engine_version": 1,
487
+ "mode": "params",
488
+ "fields": [{
489
+ "name": "value",
490
+ "required": true,
491
+ "nullable": false,
492
+ "filled": false,
493
+ "type": "string",
494
+ "member": null,
495
+ "children": [],
496
+ "predicates": [
497
+ {"name": "bool", "argument": true},
498
+ {"name": "int", "argument": -1},
499
+ {"name": "float", "argument": 1.5},
500
+ {"name": "str", "argument": "example"},
501
+ {"name": "list", "argument": [false, 2, "three"]}
502
+ ]
503
+ }]
504
+ });
505
+ let plan: SchemaPlan = serde_json::from_str(&json.to_string()).expect("valid plan");
506
+ let arguments: Vec<_> = plan.fields[0]
507
+ .predicates
508
+ .iter()
509
+ .map(|predicate| predicate.argument.clone())
510
+ .collect();
511
+
512
+ assert_eq!(
513
+ arguments,
514
+ vec![
515
+ PredicateArg::Bool(true),
516
+ PredicateArg::Int(-1),
517
+ PredicateArg::Float(1.5),
518
+ PredicateArg::Str("example".to_owned()),
519
+ PredicateArg::List(vec![
520
+ PredicateArg::Bool(false),
521
+ PredicateArg::Int(2),
522
+ PredicateArg::Str("three".to_owned()),
523
+ ]),
524
+ ]
525
+ );
526
+ }
527
+
528
+ #[test]
529
+ fn plan_rejects_null_and_object_predicate_arguments() {
530
+ for argument in ["null", "{\"unexpected\": true}"] {
531
+ let json = format!(
532
+ r#"{{"engine_version":1,"mode":"params","fields":[{{"name":"value","required":true,"nullable":false,"filled":false,"type":"string","member":null,"children":[],"predicates":[{{"name":"gteq","argument":{argument}}}]}}]}}"#
533
+ );
534
+ assert!(serde_json::from_str::<SchemaPlan>(&json).is_err());
535
+ }
536
+ }
537
+
538
+ #[test]
539
+ fn predicate_operations_are_resolved_during_deserialization() {
540
+ let json = r#"{
541
+ "engine_version": 1,
542
+ "mode": "params",
543
+ "fields": [{
544
+ "name": "value",
545
+ "required": true,
546
+ "nullable": false,
547
+ "filled": false,
548
+ "type": "integer",
549
+ "member": null,
550
+ "children": [],
551
+ "predicates": [
552
+ {"name": "gt", "argument": 1},
553
+ {"name": "min_size", "argument": 2},
554
+ {"name": "odd", "argument": true},
555
+ {"name": "custom", "argument": false}
556
+ ]
557
+ }]
558
+ }"#;
559
+ let plan: SchemaPlan = serde_json::from_str(json).expect("valid plan");
560
+ let operations: Vec<_> = plan.fields[0]
561
+ .predicates
562
+ .iter()
563
+ .map(|predicate| predicate.op)
564
+ .collect();
565
+
566
+ assert_eq!(
567
+ operations,
568
+ vec![
569
+ PredicateOp::Gt,
570
+ PredicateOp::MinSize,
571
+ PredicateOp::Odd,
572
+ PredicateOp::Unsupported,
573
+ ]
574
+ );
575
+ }
576
+
577
+ #[test]
578
+ fn used_kinds_include_nested_fields_and_members_once() {
579
+ let json = r#"{
580
+ "engine_version": 1,
581
+ "mode": "params",
582
+ "fields": [{
583
+ "name": "schedule", "required": true, "nullable": false, "filled": false,
584
+ "type": "hash", "member": null,
585
+ "children": [{
586
+ "name": "starts_on", "required": true, "nullable": false, "filled": false,
587
+ "type": "date", "member": null, "children": [], "predicates": []
588
+ }], "predicates": []
589
+ }, {
590
+ "name": "timestamps", "required": true, "nullable": false, "filled": false,
591
+ "type": "array",
592
+ "member": {
593
+ "name": null, "required": true, "nullable": false, "filled": false,
594
+ "type": "date_time", "member": null, "children": [], "predicates": []
595
+ }, "children": [], "predicates": []
596
+ }]
597
+ }"#;
598
+
599
+ let plan: SchemaPlan = serde_json::from_str(json).expect("valid plan");
600
+
601
+ assert_eq!(
602
+ collect_used_kinds(&plan.fields),
603
+ HashSet::from([
604
+ "hash".to_owned(),
605
+ "date".to_owned(),
606
+ "array".to_owned(),
607
+ "date_time".to_owned(),
608
+ ])
609
+ );
610
+ }
611
+ }