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,416 @@
1
+ use std::collections::HashSet;
2
+
3
+ use magnus::{
4
+ gc::Marker, prelude::*, r_hash::ForEach, typed_data::Obj, DataTypeFunctions, Error, RArray,
5
+ RHash, Ruby, TypedData, Value,
6
+ };
7
+
8
+ use crate::{
9
+ coercion::{coerce, empty_value, null_if_empty_nullable_param, type_matches},
10
+ error::{type_message, NativeError, PathPart},
11
+ plan::{parse_plan, FieldPlan, Mode, SchemaPlan},
12
+ predicates::apply_predicates,
13
+ ruby_bridge::RuntimeClasses,
14
+ SchemaResult,
15
+ };
16
+
17
+ const MAX_TRAVERSAL_DEPTH: u16 = 128;
18
+ const DEPTH_ERROR_CODE: &str = "depth";
19
+ const DEPTH_ERROR_TEXT: &str = "schema nesting depth exceeds limit (128)";
20
+
21
+ #[derive(TypedData)]
22
+ #[magnus(
23
+ class = "Dry::Validation::Rust::Native::Engine",
24
+ free_immediately,
25
+ mark,
26
+ size
27
+ )]
28
+ pub(crate) struct Engine {
29
+ plan: SchemaPlan,
30
+ classes: RuntimeClasses,
31
+ plan_bytes: usize,
32
+ field_count: usize,
33
+ }
34
+
35
+ impl DataTypeFunctions for Engine {
36
+ fn mark(&self, marker: &Marker) {
37
+ self.classes.mark(marker);
38
+ }
39
+ }
40
+
41
+ struct Traversal<'a> {
42
+ ruby: &'a Ruby,
43
+ classes: &'a RuntimeClasses,
44
+ mode: Mode,
45
+ validate_keys: bool,
46
+ errors: &'a mut Vec<NativeError>,
47
+ }
48
+
49
+ enum TypeValidation {
50
+ Valid(Value),
51
+ Invalid(Value),
52
+ }
53
+
54
+ impl Engine {
55
+ pub(crate) fn new(ruby: &Ruby, json: String) -> Result<Self, Error> {
56
+ let plan = parse_plan(ruby, &json)?;
57
+ let classes = RuntimeClasses::new(ruby, &plan)?;
58
+ let field_count = count_fields(&plan.fields);
59
+ Ok(Self {
60
+ plan,
61
+ classes,
62
+ plan_bytes: json.len(),
63
+ field_count,
64
+ })
65
+ }
66
+
67
+ pub(crate) fn call(&self, input: RHash) -> Result<Obj<SchemaResult>, Error> {
68
+ let ruby = Ruby::get_with(input);
69
+ let mut errors = Vec::new();
70
+ let output = {
71
+ let mut traversal = Traversal {
72
+ ruby: &ruby,
73
+ classes: &self.classes,
74
+ mode: self.plan.mode,
75
+ validate_keys: self.plan.validate_keys,
76
+ errors: &mut errors,
77
+ };
78
+ process_hash(&mut traversal, &self.plan.fields, input, &mut Vec::new(), 0)?
79
+ };
80
+ let ruby_errors = ruby.ary_new();
81
+ for error in errors {
82
+ let ruby_error = ruby.hash_new();
83
+ let path = ruby.ary_new_capa(error.path.len());
84
+ for part in error.path {
85
+ match part {
86
+ PathPart::Key(key) => path.push(ruby.to_symbol(key))?,
87
+ PathPart::Index(index) => path.push(index)?,
88
+ }
89
+ }
90
+ ruby_error.aset(ruby.to_symbol("path"), path)?;
91
+ ruby_error.aset(ruby.to_symbol("code"), ruby.to_symbol(error.code.as_ref()))?;
92
+ ruby_error.aset(ruby.to_symbol("text"), ruby.str_new(error.text.as_ref()))?;
93
+ ruby_errors.push(ruby_error)?;
94
+ }
95
+ Ok(ruby.obj_wrap(SchemaResult {
96
+ output: output.into(),
97
+ errors: ruby_errors.into(),
98
+ }))
99
+ }
100
+
101
+ pub(crate) fn field_count(&self) -> usize {
102
+ self.field_count
103
+ }
104
+
105
+ pub(crate) fn plan_bytes(&self) -> usize {
106
+ self.plan_bytes
107
+ }
108
+ }
109
+
110
+ fn count_fields(fields: &[FieldPlan]) -> usize {
111
+ fields
112
+ .iter()
113
+ .map(|field| {
114
+ 1 + count_fields(&field.children)
115
+ + field
116
+ .member
117
+ .as_ref()
118
+ .map_or(0, |member| count_fields(&member.children))
119
+ })
120
+ .sum()
121
+ }
122
+
123
+ fn process_hash(
124
+ traversal: &mut Traversal<'_>,
125
+ fields: &[FieldPlan],
126
+ input: RHash,
127
+ path: &mut Vec<PathPart>,
128
+ depth: u16,
129
+ ) -> Result<RHash, Error> {
130
+ let output = traversal.ruby.hash_new_capa(fields.len());
131
+ if !within_depth_limit(depth, path, traversal.errors) {
132
+ return Ok(output);
133
+ }
134
+ for field in fields {
135
+ process_field(traversal, &output, field, input, path, depth)?;
136
+ }
137
+ report_unexpected_keys(traversal, fields, input, path)?;
138
+ Ok(output)
139
+ }
140
+
141
+ fn report_unexpected_keys(
142
+ traversal: &mut Traversal<'_>,
143
+ fields: &[FieldPlan],
144
+ input: RHash,
145
+ path: &[PathPart],
146
+ ) -> Result<(), Error> {
147
+ if !traversal.validate_keys || traversal.mode == Mode::Schema {
148
+ return Ok(());
149
+ }
150
+
151
+ let declared: HashSet<&str> = fields
152
+ .iter()
153
+ .map(|field| field.name.as_deref().unwrap_or_default())
154
+ .collect();
155
+
156
+ input.foreach(|key: Value, _: Value| {
157
+ let key_name: String = key.funcall("to_s", ())?;
158
+ if !declared.contains(key_name.as_str()) {
159
+ let mut error_path = path.to_vec();
160
+ error_path.push(PathPart::Key(key_name));
161
+ traversal.errors.push(NativeError::new(
162
+ &error_path,
163
+ "unexpected_key",
164
+ "is not allowed",
165
+ ));
166
+ }
167
+ Ok(ForEach::Continue)
168
+ })
169
+ }
170
+
171
+ fn process_field(
172
+ traversal: &mut Traversal<'_>,
173
+ output: &RHash,
174
+ field: &FieldPlan,
175
+ input: RHash,
176
+ path: &mut Vec<PathPart>,
177
+ depth: u16,
178
+ ) -> Result<(), Error> {
179
+ let name = field.name.as_deref().unwrap_or_default();
180
+ path.push(PathPart::Key(name.to_owned()));
181
+ let result = match resolve_field_input(input, traversal.ruby, traversal.mode, name) {
182
+ Some(raw) => process_value(traversal, field, raw, path, depth)
183
+ .and_then(|processed| output.aset(traversal.ruby.to_symbol(name), processed)),
184
+ None => {
185
+ report_missing_field(traversal, field, path);
186
+ Ok(())
187
+ }
188
+ };
189
+ path.pop();
190
+ result
191
+ }
192
+
193
+ fn resolve_field_input(input: RHash, ruby: &Ruby, mode: Mode, name: &str) -> Option<Value> {
194
+ input.get(ruby.to_symbol(name)).or_else(|| {
195
+ if mode == Mode::Schema {
196
+ None
197
+ } else {
198
+ input.get(name)
199
+ }
200
+ })
201
+ }
202
+
203
+ fn report_missing_field(traversal: &mut Traversal<'_>, field: &FieldPlan, path: &[PathPart]) {
204
+ if field.required {
205
+ traversal
206
+ .errors
207
+ .push(NativeError::new(path, "key", "is missing"));
208
+ }
209
+ }
210
+
211
+ fn process_value(
212
+ traversal: &mut Traversal<'_>,
213
+ field: &FieldPlan,
214
+ raw: Value,
215
+ path: &mut Vec<PathPart>,
216
+ depth: u16,
217
+ ) -> Result<Value, Error> {
218
+ if !within_depth_limit(depth, path, traversal.errors) {
219
+ return Ok(raw);
220
+ }
221
+ if validate_nil_value(traversal, field, raw, path) {
222
+ return Ok(raw);
223
+ }
224
+ if let Some(nil) =
225
+ null_if_empty_nullable_param(traversal.ruby, traversal.mode, field.nullable, raw)
226
+ {
227
+ return Ok(nil);
228
+ }
229
+
230
+ let coerced = match coerce_and_validate_type(traversal, field, raw, path)? {
231
+ TypeValidation::Valid(value) => value,
232
+ TypeValidation::Invalid(value) => return Ok(value),
233
+ };
234
+ let filled_error = report_filled_error(traversal, field, coerced, path);
235
+ let value = process_children(traversal, field, coerced, path, depth)?;
236
+ if !filled_error {
237
+ apply_field_predicates(traversal, field, value, path)?;
238
+ }
239
+ Ok(value)
240
+ }
241
+
242
+ fn validate_nil_value(
243
+ traversal: &mut Traversal<'_>,
244
+ field: &FieldPlan,
245
+ raw: Value,
246
+ path: &[PathPart],
247
+ ) -> bool {
248
+ if !raw.is_nil() {
249
+ return false;
250
+ }
251
+
252
+ if field.filled
253
+ && (traversal.mode == Mode::Params || field.kind == "nil" || field.kind == "any")
254
+ {
255
+ traversal
256
+ .errors
257
+ .push(NativeError::new(path, "filled", "must be filled"));
258
+ } else if !field.nullable && field.kind != "nil" && field.kind != "any" {
259
+ traversal
260
+ .errors
261
+ .push(NativeError::new(path, "type", type_message(&field.kind)));
262
+ }
263
+ true
264
+ }
265
+
266
+ fn coerce_and_validate_type(
267
+ traversal: &mut Traversal<'_>,
268
+ field: &FieldPlan,
269
+ raw: Value,
270
+ path: &[PathPart],
271
+ ) -> Result<TypeValidation, Error> {
272
+ let Some(coerced) = coerce(
273
+ traversal.ruby,
274
+ traversal.classes,
275
+ traversal.mode,
276
+ &field.kind,
277
+ raw,
278
+ )?
279
+ else {
280
+ traversal
281
+ .errors
282
+ .push(NativeError::new(path, "type", type_message(&field.kind)));
283
+ return Ok(TypeValidation::Invalid(raw));
284
+ };
285
+
286
+ if type_matches(traversal.ruby, traversal.classes, &field.kind, coerced) {
287
+ Ok(TypeValidation::Valid(coerced))
288
+ } else {
289
+ traversal
290
+ .errors
291
+ .push(NativeError::new(path, "type", type_message(&field.kind)));
292
+ Ok(TypeValidation::Invalid(coerced))
293
+ }
294
+ }
295
+
296
+ fn report_filled_error(
297
+ traversal: &mut Traversal<'_>,
298
+ field: &FieldPlan,
299
+ value: Value,
300
+ path: &[PathPart],
301
+ ) -> bool {
302
+ let filled_error = field.filled && empty_value(value);
303
+ if filled_error {
304
+ traversal
305
+ .errors
306
+ .push(NativeError::new(path, "filled", "must be filled"));
307
+ }
308
+ filled_error
309
+ }
310
+
311
+ fn process_children(
312
+ traversal: &mut Traversal<'_>,
313
+ field: &FieldPlan,
314
+ value: Value,
315
+ path: &mut Vec<PathPart>,
316
+ depth: u16,
317
+ ) -> Result<Value, Error> {
318
+ if field.kind == "hash" && !field.children.is_empty() {
319
+ if let Some(hash) = RHash::from_value(value) {
320
+ return Ok(process_hash(traversal, &field.children, hash, path, depth + 1)?.as_value());
321
+ }
322
+ } else if field.kind == "array" {
323
+ return process_array_members(traversal, field, value, path, depth);
324
+ }
325
+ Ok(value)
326
+ }
327
+
328
+ fn process_array_members(
329
+ traversal: &mut Traversal<'_>,
330
+ field: &FieldPlan,
331
+ value: Value,
332
+ path: &mut Vec<PathPart>,
333
+ depth: u16,
334
+ ) -> Result<Value, Error> {
335
+ let (Some(member), Some(array)) = (field.member.as_ref(), RArray::from_value(value)) else {
336
+ return Ok(value);
337
+ };
338
+
339
+ let output = traversal.ruby.ary_new_capa(array.len());
340
+ for (index, item) in array.into_iter().enumerate() {
341
+ path.push(PathPart::Index(index));
342
+ let processed = process_value(traversal, member, item, path, depth + 1);
343
+ path.pop();
344
+ output.push(processed?)?;
345
+ }
346
+ Ok(output.as_value())
347
+ }
348
+
349
+ fn apply_field_predicates(
350
+ traversal: &mut Traversal<'_>,
351
+ field: &FieldPlan,
352
+ value: Value,
353
+ path: &[PathPart],
354
+ ) -> Result<(), Error> {
355
+ apply_predicates(traversal.ruby, field, value, path, traversal.errors)
356
+ }
357
+
358
+ fn within_depth_limit(depth: u16, path: &[PathPart], errors: &mut Vec<NativeError>) -> bool {
359
+ if depth <= MAX_TRAVERSAL_DEPTH {
360
+ return true;
361
+ }
362
+
363
+ errors.push(NativeError::new(path, DEPTH_ERROR_CODE, DEPTH_ERROR_TEXT));
364
+ false
365
+ }
366
+
367
+ #[cfg(test)]
368
+ mod tests {
369
+ use super::*;
370
+
371
+ #[test]
372
+ fn field_count_includes_nested_and_member_fields() {
373
+ let json = r#"{
374
+ "engine_version": 1, "mode": "params", "fields": [{
375
+ "name": "items", "required": true, "nullable": false, "filled": false, "type": "array",
376
+ "member": {"name": null, "required": true, "nullable": false, "filled": false, "type": "hash", "member": null,
377
+ "children": [{"name": "id", "required": true, "nullable": false, "filled": false, "type": "integer", "member": null, "children": [], "predicates": []}], "predicates": []},
378
+ "children": [], "predicates": []
379
+ }]
380
+ }"#;
381
+ let plan: SchemaPlan = serde_json::from_str(json).expect("valid plan");
382
+ let field_count = count_fields(&plan.fields);
383
+ let engine = Engine {
384
+ plan,
385
+ classes: RuntimeClasses::default(),
386
+ plan_bytes: json.len(),
387
+ field_count,
388
+ };
389
+ assert_eq!(engine.field_count(), 2);
390
+ assert_eq!(engine.plan_bytes(), json.len());
391
+ }
392
+
393
+ #[test]
394
+ fn nested_structure_over_128_levels_returns_a_depth_error() {
395
+ fn traverse_nested_structure(
396
+ depth: u16,
397
+ path: &mut Vec<PathPart>,
398
+ errors: &mut Vec<NativeError>,
399
+ ) {
400
+ if !within_depth_limit(depth, path, errors) || depth == 200 {
401
+ return;
402
+ }
403
+
404
+ path.push(PathPart::Key(format!("level_{depth}")));
405
+ traverse_nested_structure(depth + 1, path, errors);
406
+ path.pop();
407
+ }
408
+
409
+ let mut errors = Vec::new();
410
+ traverse_nested_structure(0, &mut Vec::new(), &mut errors);
411
+
412
+ assert_eq!(errors.len(), 1);
413
+ assert_eq!(errors[0].code, DEPTH_ERROR_CODE);
414
+ assert_eq!(errors[0].text, "schema nesting depth exceeds limit (128)");
415
+ }
416
+ }
@@ -0,0 +1,82 @@
1
+ use std::borrow::Cow;
2
+
3
+ #[derive(Debug, Clone)]
4
+ pub(crate) enum PathPart {
5
+ Key(String),
6
+ Index(usize),
7
+ }
8
+
9
+ #[derive(Debug)]
10
+ pub(crate) struct NativeError {
11
+ pub(crate) path: Vec<PathPart>,
12
+ pub(crate) code: Cow<'static, str>,
13
+ pub(crate) text: Cow<'static, str>,
14
+ }
15
+
16
+ impl NativeError {
17
+ pub(crate) fn new(
18
+ path: &[PathPart],
19
+ code: impl Into<Cow<'static, str>>,
20
+ text: impl Into<Cow<'static, str>>,
21
+ ) -> Self {
22
+ Self {
23
+ path: path.to_vec(),
24
+ code: code.into(),
25
+ text: text.into(),
26
+ }
27
+ }
28
+ }
29
+
30
+ #[inline]
31
+ pub(crate) fn type_message(kind: &str) -> &'static str {
32
+ match kind {
33
+ "nil" => "must be nil",
34
+ "bool" => "must be boolean",
35
+ "true" => "must be true",
36
+ "false" => "must be false",
37
+ "integer" => "must be an integer",
38
+ "float" => "must be a float",
39
+ "decimal" => "must be a decimal",
40
+ "string" => "must be a string",
41
+ "symbol" => "must be a symbol",
42
+ "array" => "must be an array",
43
+ "hash" => "must be a hash",
44
+ "date" => "must be a date",
45
+ "date_time" => "must be a date time",
46
+ "time" => "must be a time",
47
+ _ => "has invalid type",
48
+ }
49
+ }
50
+
51
+ #[cfg(test)]
52
+ mod tests {
53
+ use std::borrow::Cow;
54
+
55
+ use super::{type_message, NativeError, PathPart};
56
+
57
+ #[test]
58
+ fn native_error_owns_a_clone_of_key_and_index_path_parts() {
59
+ let mut path = vec![PathPart::Key("profile".to_owned()), PathPart::Index(2)];
60
+ let error = NativeError::new(&path, "type", "must be a hash");
61
+ path[0] = PathPart::Key("changed".to_owned());
62
+
63
+ assert!(
64
+ matches!(&error.path[..], [PathPart::Key(key), PathPart::Index(2)] if key == "profile")
65
+ );
66
+ }
67
+
68
+ #[test]
69
+ fn type_messages_are_stable() {
70
+ assert_eq!(type_message("integer"), "must be an integer");
71
+ assert_eq!(type_message("date_time"), "must be a date time");
72
+ assert_eq!(type_message("something_new"), "has invalid type");
73
+ }
74
+
75
+ #[test]
76
+ fn type_errors_borrow_static_code_and_text() {
77
+ let error = NativeError::new(&[], "type", type_message("integer"));
78
+
79
+ assert!(matches!(error.code, Cow::Borrowed("type")));
80
+ assert!(matches!(error.text, Cow::Borrowed("must be an integer")));
81
+ }
82
+ }
@@ -0,0 +1,23 @@
1
+ use magnus::{Float, Integer, RArray, RHash, RString, Value};
2
+
3
+ pub(crate) fn extract_i64(value: Value) -> Option<i64> {
4
+ Integer::from_value(value)?.to_i64().ok()
5
+ }
6
+
7
+ pub(crate) fn extract_f64(value: Value) -> Option<f64> {
8
+ Some(Float::from_value(value)?.to_f64())
9
+ }
10
+
11
+ /// Returns only UTF-8 strings so native ordering and character counts preserve
12
+ /// the Ruby behavior represented by a Rust `String`.
13
+ pub(crate) fn extract_string(value: Value) -> Option<String> {
14
+ RString::from_value(value)?.to_string().ok()
15
+ }
16
+
17
+ pub(crate) fn extract_array_len(value: Value) -> Option<usize> {
18
+ Some(RArray::from_value(value)?.len())
19
+ }
20
+
21
+ pub(crate) fn extract_hash_len(value: Value) -> Option<usize> {
22
+ Some(RHash::from_value(value)?.len())
23
+ }
@@ -0,0 +1,33 @@
1
+ // This file is generated by `bundle exec rake generate:predicates`.
2
+ // Do not edit it directly; update predicates.yml instead.
3
+
4
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
5
+ pub(crate) enum PredicateOp {
6
+ Gt,
7
+ Gteq,
8
+ Lt,
9
+ Lteq,
10
+ MinSize,
11
+ MaxSize,
12
+ Size,
13
+ Odd,
14
+ Even,
15
+ Unsupported,
16
+ }
17
+
18
+ impl PredicateOp {
19
+ fn from_name(name: &str) -> Self {
20
+ match name {
21
+ "gt" => Self::Gt,
22
+ "gteq" => Self::Gteq,
23
+ "lt" => Self::Lt,
24
+ "lteq" => Self::Lteq,
25
+ "min_size" => Self::MinSize,
26
+ "max_size" => Self::MaxSize,
27
+ "size" => Self::Size,
28
+ "odd" => Self::Odd,
29
+ "even" => Self::Even,
30
+ _ => Self::Unsupported,
31
+ }
32
+ }
33
+ }