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,449 @@
1
+ use magnus::{prelude::*, Error, Ruby, Value};
2
+
3
+ use crate::{
4
+ error::{NativeError, PathPart},
5
+ extract_primitive::{
6
+ extract_array_len, extract_f64, extract_hash_len, extract_i64, extract_string,
7
+ },
8
+ plan::{PredicateArg, PredicateOp, PredicatePlan},
9
+ };
10
+
11
+ pub(crate) fn apply_predicates(
12
+ ruby: &Ruby,
13
+ field: &crate::plan::FieldPlan,
14
+ value: Value,
15
+ path: &[PathPart],
16
+ errors: &mut Vec<NativeError>,
17
+ ) -> Result<(), Error> {
18
+ for predicate in &field.predicates {
19
+ let valid = match predicate.op {
20
+ PredicateOp::Gt | PredicateOp::Gteq | PredicateOp::Lt | PredicateOp::Lteq => {
21
+ comparison_predicate_valid(predicate.op, value, &predicate.argument).map_or_else(
22
+ || {
23
+ ruby_comparison_predicate_valid(
24
+ ruby,
25
+ predicate.op,
26
+ value,
27
+ &predicate.argument,
28
+ )
29
+ },
30
+ Ok,
31
+ )?
32
+ }
33
+ PredicateOp::MinSize | PredicateOp::MaxSize | PredicateOp::Size => {
34
+ let actual = primitive_size(value)
35
+ .map(Ok)
36
+ .unwrap_or_else(|| value.funcall::<_, _, usize>("size", ()))?;
37
+ size_predicate_valid(predicate.op, Some(actual), &predicate.argument)
38
+ }
39
+ PredicateOp::Odd => extract_i64(value)
40
+ .map(|integer| integer % 2 != 0)
41
+ .map(Ok)
42
+ .unwrap_or_else(|| value.funcall::<_, _, bool>("odd?", ()))?,
43
+ PredicateOp::Even => extract_i64(value)
44
+ .map(|integer| integer % 2 == 0)
45
+ .map(Ok)
46
+ .unwrap_or_else(|| value.funcall::<_, _, bool>("even?", ()))?,
47
+ PredicateOp::Unsupported => true,
48
+ };
49
+ if !valid {
50
+ errors.push(NativeError::new(
51
+ path,
52
+ predicate.name.clone(),
53
+ predicate_message(predicate),
54
+ ));
55
+ }
56
+ }
57
+ Ok(())
58
+ }
59
+
60
+ fn comparison_predicate_valid(
61
+ op: PredicateOp,
62
+ value: Value,
63
+ argument: &PredicateArg,
64
+ ) -> Option<bool> {
65
+ match argument {
66
+ PredicateArg::Int(expected) => {
67
+ extract_i64(value).map(|actual| compare(op, actual, *expected))
68
+ }
69
+ PredicateArg::Float(expected) => {
70
+ extract_f64(value).map(|actual| compare(op, actual, *expected))
71
+ }
72
+ PredicateArg::Str(expected) => {
73
+ extract_string(value).map(|actual| compare(op, actual, expected.clone()))
74
+ }
75
+ PredicateArg::Bool(_) | PredicateArg::List(_) => None,
76
+ }
77
+ }
78
+
79
+ fn compare<T: PartialOrd>(op: PredicateOp, actual: T, expected: T) -> bool {
80
+ match op {
81
+ PredicateOp::Gt => actual > expected,
82
+ PredicateOp::Gteq => actual >= expected,
83
+ PredicateOp::Lt => actual < expected,
84
+ PredicateOp::Lteq => actual <= expected,
85
+ _ => false,
86
+ }
87
+ }
88
+
89
+ fn ruby_comparison_predicate_valid(
90
+ ruby: &Ruby,
91
+ op: PredicateOp,
92
+ value: Value,
93
+ argument: &PredicateArg,
94
+ ) -> Result<bool, Error> {
95
+ let Some(argument) = predicate_scalar(ruby, argument) else {
96
+ return Ok(false);
97
+ };
98
+ let operator = match op {
99
+ PredicateOp::Gt => ">",
100
+ PredicateOp::Gteq => ">=",
101
+ PredicateOp::Lt => "<",
102
+ PredicateOp::Lteq => "<=",
103
+ _ => unreachable!("comparison predicate operation must be recognized"),
104
+ };
105
+ value.funcall(operator, (argument,))
106
+ }
107
+
108
+ fn primitive_size(value: Value) -> Option<usize> {
109
+ extract_string(value)
110
+ .map(|string| string.chars().count())
111
+ .or_else(|| extract_array_len(value))
112
+ .or_else(|| extract_hash_len(value))
113
+ }
114
+
115
+ fn size_predicate_valid(op: PredicateOp, actual: Option<usize>, argument: &PredicateArg) -> bool {
116
+ let Some(actual) = actual else {
117
+ return false;
118
+ };
119
+ let PredicateArg::Int(expected) = argument else {
120
+ return false;
121
+ };
122
+ let Ok(expected) = usize::try_from(*expected) else {
123
+ return false;
124
+ };
125
+
126
+ match op {
127
+ PredicateOp::MinSize => actual >= expected,
128
+ PredicateOp::MaxSize => actual <= expected,
129
+ PredicateOp::Size => actual == expected,
130
+ _ => false,
131
+ }
132
+ }
133
+
134
+ fn predicate_scalar(ruby: &Ruby, argument: &PredicateArg) -> Option<Value> {
135
+ match argument {
136
+ PredicateArg::Int(value) => Some(ruby.integer_from_i64(*value).as_value()),
137
+ PredicateArg::Float(value) => Some(ruby.float_from_f64(*value).as_value()),
138
+ PredicateArg::Str(value) => Some(ruby.str_new(value).as_value()),
139
+ PredicateArg::Bool(_) | PredicateArg::List(_) => None,
140
+ }
141
+ }
142
+
143
+ fn predicate_message(predicate: &PredicatePlan) -> String {
144
+ let argument = predicate_argument_text(&predicate.argument);
145
+ match predicate.op {
146
+ PredicateOp::Gt => format!("must be greater than {argument}"),
147
+ PredicateOp::Gteq => format!("must be greater than or equal to {argument}"),
148
+ PredicateOp::Lt => format!("must be less than {argument}"),
149
+ PredicateOp::Lteq => format!("must be less than or equal to {argument}"),
150
+ PredicateOp::MinSize => format!("size cannot be less than {argument}"),
151
+ PredicateOp::MaxSize => format!("size cannot be greater than {argument}"),
152
+ PredicateOp::Size => format!("length must be {argument}"),
153
+ PredicateOp::Odd => "must be odd".to_owned(),
154
+ PredicateOp::Even => "must be even".to_owned(),
155
+ PredicateOp::Unsupported => "is invalid".to_owned(),
156
+ }
157
+ }
158
+
159
+ fn predicate_argument_text(argument: &PredicateArg) -> String {
160
+ match argument {
161
+ PredicateArg::Str(value) => value.clone(),
162
+ _ => predicate_argument_json(argument).to_string(),
163
+ }
164
+ }
165
+
166
+ fn predicate_argument_json(argument: &PredicateArg) -> serde_json::Value {
167
+ match argument {
168
+ PredicateArg::Bool(value) => serde_json::Value::Bool(*value),
169
+ PredicateArg::Int(value) => serde_json::Value::from(*value),
170
+ PredicateArg::Float(value) => serde_json::json!(value),
171
+ PredicateArg::Str(value) => serde_json::Value::String(value.clone()),
172
+ PredicateArg::List(values) => {
173
+ serde_json::Value::Array(values.iter().map(predicate_argument_json).collect())
174
+ }
175
+ }
176
+ }
177
+
178
+ #[cfg(test)]
179
+ pub(crate) mod tests {
180
+ use super::*;
181
+ use crate::plan::FieldPlan;
182
+ use magnus::{Exception, ExceptionClass};
183
+
184
+ // Magnus permits one embedded Ruby VM per test process. Keep native Ruby
185
+ // callback coverage in this single test so Cargo's parallel test runner
186
+ // cannot attempt a second initialization.
187
+ #[test]
188
+ fn native_ruby_callbacks_preserve_predicate_and_coercion_errors() {
189
+ Ruby::init(|ruby| {
190
+ crate::coercion::tests::params_coercion_handles_native_boundary_edge_cases(ruby)?;
191
+ predicate_method_exceptions_are_propagated(ruby)
192
+ })
193
+ .expect("native Ruby callback errors should remain observable");
194
+ }
195
+
196
+ pub(crate) fn predicate_method_exceptions_are_propagated(ruby: &Ruby) -> Result<(), Error> {
197
+ pure_rust_predicates_bypass_primitive_ruby_methods(ruby)?;
198
+ let odd_error = ruby.eval::<Value>(
199
+ "Class.new { def odd? = raise RuntimeError, 'odd predicate failed' }.new",
200
+ )?;
201
+ let comparison_error = ruby.eval::<Value>(
202
+ "Class.new { def >(value) = raise TypeError, \"cannot compare with #{value}\" }.new",
203
+ )?;
204
+ let odd_field = FieldPlan {
205
+ name: Some("value".to_owned()),
206
+ required: true,
207
+ nullable: false,
208
+ filled: false,
209
+ kind: "any".to_owned(),
210
+ member: None,
211
+ children: Vec::new(),
212
+ predicates: vec![PredicatePlan {
213
+ name: "odd".to_owned(),
214
+ op: PredicateOp::Odd,
215
+ argument: PredicateArg::Bool(true),
216
+ }],
217
+ };
218
+ let comparison_field = FieldPlan {
219
+ name: Some("value".to_owned()),
220
+ required: true,
221
+ nullable: false,
222
+ filled: false,
223
+ kind: "any".to_owned(),
224
+ member: None,
225
+ children: Vec::new(),
226
+ predicates: vec![PredicatePlan {
227
+ name: "gt".to_owned(),
228
+ op: PredicateOp::Gt,
229
+ argument: PredicateArg::Int(18),
230
+ }],
231
+ };
232
+
233
+ let odd_result = apply_predicates(ruby, &odd_field, odd_error, &[], &mut Vec::new());
234
+ let comparison_result = apply_predicates(
235
+ ruby,
236
+ &comparison_field,
237
+ comparison_error,
238
+ &[],
239
+ &mut Vec::new(),
240
+ );
241
+
242
+ assert_predicate_error(
243
+ odd_result,
244
+ ruby.exception_runtime_error(),
245
+ "odd predicate failed",
246
+ )?;
247
+ assert_predicate_error(
248
+ comparison_result,
249
+ ruby.exception_type_error(),
250
+ "cannot compare with 18",
251
+ )?;
252
+ Ok(())
253
+ }
254
+
255
+ fn pure_rust_predicates_bypass_primitive_ruby_methods(ruby: &Ruby) -> Result<(), Error> {
256
+ ruby.eval::<Value>(
257
+ r#"
258
+ Integer.prepend(Module.new do
259
+ def >(...) = raise "Integer#> should not be called"
260
+ def <(...) = raise "Integer#< should not be called"
261
+ def <=(...) = raise "Integer#<= should not be called"
262
+ def odd? = raise "Integer#odd? should not be called"
263
+ def even? = raise "Integer#even? should not be called"
264
+ end)
265
+ Float.prepend(Module.new do
266
+ def >=(...) = raise "Float#>= should not be called"
267
+ end)
268
+ String.prepend(Module.new do
269
+ def <(...) = raise "String#< should not be called"
270
+ def size = raise "String#size should not be called"
271
+ end)
272
+ Array.prepend(Module.new do
273
+ def size = raise "Array#size should not be called"
274
+ end)
275
+ Hash.prepend(Module.new do
276
+ def size = raise "Hash#size should not be called"
277
+ end)
278
+ "#,
279
+ )?;
280
+
281
+ let mut errors = Vec::new();
282
+ for (value, predicate) in [
283
+ (
284
+ ruby.integer_from_i64(19).as_value(),
285
+ predicate(PredicateOp::Gt, PredicateArg::Int(18)),
286
+ ),
287
+ (
288
+ ruby.integer_from_i64(18).as_value(),
289
+ predicate(PredicateOp::Lteq, PredicateArg::Int(18)),
290
+ ),
291
+ (
292
+ ruby.integer_from_i64(17).as_value(),
293
+ predicate(PredicateOp::Lt, PredicateArg::Int(18)),
294
+ ),
295
+ (
296
+ ruby.float_from_f64(1.5).as_value(),
297
+ predicate(PredicateOp::Gteq, PredicateArg::Float(1.5)),
298
+ ),
299
+ (
300
+ ruby.str_new("apple").as_value(),
301
+ predicate(PredicateOp::Lt, PredicateArg::Str("banana".to_owned())),
302
+ ),
303
+ (
304
+ ruby.integer_from_i64(3).as_value(),
305
+ predicate(PredicateOp::Odd, PredicateArg::Bool(true)),
306
+ ),
307
+ (
308
+ ruby.integer_from_i64(4).as_value(),
309
+ predicate(PredicateOp::Even, PredicateArg::Bool(true)),
310
+ ),
311
+ (
312
+ ruby.str_new("🦀").as_value(),
313
+ predicate(PredicateOp::Size, PredicateArg::Int(1)),
314
+ ),
315
+ (
316
+ ruby.ary_from_iter([1, 2, 3]).as_value(),
317
+ predicate(PredicateOp::MinSize, PredicateArg::Int(3)),
318
+ ),
319
+ (
320
+ ruby.hash_from_iter([("one", 1)]).as_value(),
321
+ predicate(PredicateOp::MaxSize, PredicateArg::Int(1)),
322
+ ),
323
+ ] {
324
+ apply_predicates(ruby, &field_with(predicate), value, &[], &mut errors)?;
325
+ }
326
+ assert!(errors.is_empty());
327
+ Ok(())
328
+ }
329
+
330
+ fn field_with(predicate: PredicatePlan) -> FieldPlan {
331
+ FieldPlan {
332
+ name: Some("value".to_owned()),
333
+ required: true,
334
+ nullable: false,
335
+ filled: false,
336
+ kind: "any".to_owned(),
337
+ member: None,
338
+ children: Vec::new(),
339
+ predicates: vec![predicate],
340
+ }
341
+ }
342
+
343
+ fn predicate(op: PredicateOp, argument: PredicateArg) -> PredicatePlan {
344
+ PredicatePlan {
345
+ name: "test".to_owned(),
346
+ op,
347
+ argument,
348
+ }
349
+ }
350
+
351
+ fn assert_predicate_error(
352
+ result: Result<(), Error>,
353
+ expected_class: ExceptionClass,
354
+ expected_message: &str,
355
+ ) -> Result<(), Error> {
356
+ let error = result.expect_err("predicate call should fail");
357
+ let exception = Exception::from_value(error.value().expect("Ruby exception value"))
358
+ .expect("predicate failure should retain its Ruby exception");
359
+ assert!(exception.is_kind_of(expected_class));
360
+ let message: String = exception.funcall("message", ())?;
361
+ assert_eq!(message, expected_message);
362
+ Ok(())
363
+ }
364
+
365
+ #[test]
366
+ fn predicate_messages_preserve_arguments() {
367
+ let greater_than_or_equal = PredicatePlan {
368
+ name: "gteq".to_owned(),
369
+ op: PredicateOp::Gteq,
370
+ argument: PredicateArg::Int(18),
371
+ };
372
+ let size = PredicatePlan {
373
+ name: "size".to_owned(),
374
+ op: PredicateOp::Size,
375
+ argument: PredicateArg::Int(3),
376
+ };
377
+ assert_eq!(
378
+ predicate_message(&greater_than_or_equal),
379
+ "must be greater than or equal to 18"
380
+ );
381
+ assert_eq!(predicate_message(&size), "length must be 3");
382
+ }
383
+
384
+ #[test]
385
+ fn predicate_messages_render_list_arguments_as_json() {
386
+ let predicate = PredicatePlan {
387
+ name: "size".to_owned(),
388
+ op: PredicateOp::Size,
389
+ argument: PredicateArg::List(vec![
390
+ PredicateArg::Bool(true),
391
+ PredicateArg::Str("two".to_owned()),
392
+ ]),
393
+ };
394
+
395
+ assert_eq!(
396
+ predicate_message(&predicate),
397
+ "length must be [true,\"two\"]"
398
+ );
399
+ }
400
+
401
+ #[test]
402
+ fn size_predicates_accept_passing_values_at_boundaries() {
403
+ let expected = PredicateArg::Int(3);
404
+
405
+ assert!(size_predicate_valid(
406
+ PredicateOp::MinSize,
407
+ Some(3),
408
+ &expected
409
+ ));
410
+ assert!(size_predicate_valid(
411
+ PredicateOp::MaxSize,
412
+ Some(3),
413
+ &expected
414
+ ));
415
+ assert!(size_predicate_valid(PredicateOp::Size, Some(3), &expected));
416
+ }
417
+
418
+ #[test]
419
+ fn size_predicates_reject_failing_values_and_missing_values() {
420
+ let expected = PredicateArg::Int(3);
421
+
422
+ assert!(!size_predicate_valid(
423
+ PredicateOp::MinSize,
424
+ Some(2),
425
+ &expected
426
+ ));
427
+ assert!(!size_predicate_valid(
428
+ PredicateOp::MaxSize,
429
+ Some(4),
430
+ &expected
431
+ ));
432
+ assert!(!size_predicate_valid(PredicateOp::Size, Some(2), &expected));
433
+ assert!(!size_predicate_valid(PredicateOp::Size, None, &expected));
434
+ }
435
+
436
+ #[test]
437
+ fn size_predicates_reject_wrong_type_and_negative_arguments() {
438
+ assert!(!size_predicate_valid(
439
+ PredicateOp::Size,
440
+ Some(3),
441
+ &PredicateArg::Str("3".to_owned())
442
+ ));
443
+ assert!(!size_predicate_valid(
444
+ PredicateOp::Size,
445
+ Some(3),
446
+ &PredicateArg::Int(-1)
447
+ ));
448
+ }
449
+ }
@@ -0,0 +1,78 @@
1
+ use magnus::{gc::Marker, prelude::*, value::Opaque, Error, RClass, Ruby};
2
+
3
+ use crate::plan::SchemaPlan;
4
+
5
+ #[derive(Default)]
6
+ pub(crate) struct RuntimeClasses {
7
+ date: Option<Opaque<RClass>>,
8
+ date_time: Option<Opaque<RClass>>,
9
+ time: Option<Opaque<RClass>>,
10
+ big_decimal: Option<Opaque<RClass>>,
11
+ }
12
+
13
+ impl RuntimeClasses {
14
+ pub(crate) fn new(ruby: &Ruby, plan: &SchemaPlan) -> Result<Self, Error> {
15
+ let object = ruby.class_object();
16
+ Ok(Self {
17
+ date: plan
18
+ .used_kinds
19
+ .contains("date")
20
+ .then(|| object.const_get::<_, RClass>("Date"))
21
+ .transpose()?
22
+ .map(Into::into),
23
+ date_time: plan
24
+ .used_kinds
25
+ .contains("date_time")
26
+ .then(|| object.const_get::<_, RClass>("DateTime"))
27
+ .transpose()?
28
+ .map(Into::into),
29
+ time: plan
30
+ .used_kinds
31
+ .contains("time")
32
+ .then(|| object.const_get::<_, RClass>("Time"))
33
+ .transpose()?
34
+ .map(Into::into),
35
+ big_decimal: plan
36
+ .used_kinds
37
+ .contains("decimal")
38
+ .then(|| object.const_get::<_, RClass>("BigDecimal"))
39
+ .transpose()?
40
+ .map(Into::into),
41
+ })
42
+ }
43
+
44
+ pub(crate) fn date(&self, ruby: &Ruby) -> Option<RClass> {
45
+ self.date.map(|class| ruby.get_inner(class))
46
+ }
47
+
48
+ pub(crate) fn date_time(&self, ruby: &Ruby) -> Option<RClass> {
49
+ self.date_time.map(|class| ruby.get_inner(class))
50
+ }
51
+
52
+ pub(crate) fn time(&self, ruby: &Ruby) -> Option<RClass> {
53
+ self.time.map(|class| ruby.get_inner(class))
54
+ }
55
+
56
+ pub(crate) fn big_decimal(&self, ruby: &Ruby) -> Option<RClass> {
57
+ self.big_decimal.map(|class| ruby.get_inner(class))
58
+ }
59
+
60
+ pub(crate) fn mark(&self, marker: &Marker) {
61
+ for class in [self.date, self.date_time, self.time, self.big_decimal]
62
+ .into_iter()
63
+ .flatten()
64
+ {
65
+ marker.mark(class);
66
+ }
67
+ }
68
+
69
+ pub(crate) fn all(ruby: &Ruby) -> Result<Self, Error> {
70
+ let object = ruby.class_object();
71
+ Ok(Self {
72
+ date: Some(object.const_get::<_, RClass>("Date")?.into()),
73
+ date_time: Some(object.const_get::<_, RClass>("DateTime")?.into()),
74
+ time: Some(object.const_get::<_, RClass>("Time")?.into()),
75
+ big_decimal: Some(object.const_get::<_, RClass>("BigDecimal")?.into()),
76
+ })
77
+ }
78
+ }
data/lib/dry/schema.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The experimental replacement ships only the minimal schema factories needed
4
+ # by its contract DSL. Loading this entrypoint also loads exact compatibility
5
+ # mode; it is not the full upstream dry-schema gem.
6
+ require 'dry/validation'
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Validation
5
+ module Rust
6
+ # @api private
7
+ module BlockKeywordParameters
8
+ EMPTY = [].freeze
9
+
10
+ module_function
11
+
12
+ def extract(block)
13
+ block.parameters.filter_map do |kind, name|
14
+ name if %i[key keyreq].include?(kind)
15
+ end.freeze
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Validation
5
+ module Rust
6
+ # Configures the message backend used by compiled schemas.
7
+ class MessageConfig
8
+ BACKENDS = { yaml: YamlBackend, i18n: I18nBackend }.freeze
9
+
10
+ # @return [:yaml, :i18n, Class] the selected built-in identifier or custom backend class.
11
+ attr_reader :backend
12
+ attr_accessor :default_locale, :top_namespace, :load_paths
13
+
14
+ def initialize
15
+ @backend = :yaml
16
+ @default_locale = :en
17
+ @top_namespace = :dry_validation
18
+ @load_paths = []
19
+ end
20
+
21
+ # Selects a built-in backend or a custom {MessageBackend} subclass.
22
+ #
23
+ # @param backend [:yaml, :i18n, Class] backend identifier or adapter class.
24
+ # @raise [ArgumentError] if the backend is unsupported.
25
+ def backend=(backend)
26
+ @backend = BACKENDS.key?(backend) ? backend : validate_backend_class(backend)
27
+ end
28
+
29
+ def dup
30
+ copy = super
31
+ copy.load_paths = load_paths.dup
32
+ copy
33
+ end
34
+
35
+ # @api private
36
+ def backend_class
37
+ BACKENDS.fetch(backend, backend)
38
+ end
39
+
40
+ private
41
+
42
+ def validate_backend_class(backend)
43
+ return backend if backend.is_a?(Class) && backend < MessageBackend
44
+
45
+ raise ArgumentError, backend_error(backend)
46
+ end
47
+
48
+ def backend_error(backend)
49
+ "messages.backend must be :yaml, :i18n, or a MessageBackend subclass; got #{backend.inspect}"
50
+ end
51
+ end
52
+
53
+ class Config
54
+ attr_reader :validate_keys
55
+ attr_accessor :messages
56
+
57
+ def initialize
58
+ @validate_keys = false
59
+ @messages = MessageConfig.new
60
+ end
61
+
62
+ def validate_keys=(value)
63
+ @validate_keys = !!value
64
+ end
65
+
66
+ def dup
67
+ copy = super
68
+ copy.messages = messages.dup
69
+ copy
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end