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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +74 -0
- data/LICENSE +21 -0
- data/NOTICE.md +28 -0
- data/README.md +459 -0
- data/docs/ARCHITECTURE.md +256 -0
- data/docs/COMPATIBILITY.md +198 -0
- data/docs/FEASIBILITY.md +207 -0
- data/docs/SUPPORT_MATRIX.md +66 -0
- data/docs/VERIFICATION.md +128 -0
- data/dry-validation-rust.gemspec +58 -0
- data/ext/dry_validation_rust/Cargo.lock +809 -0
- data/ext/dry_validation_rust/Cargo.toml +44 -0
- data/ext/dry_validation_rust/benches/coercion.rs +77 -0
- data/ext/dry_validation_rust/benches/full_schema.rs +189 -0
- data/ext/dry_validation_rust/benches/plan_compile.rs +37 -0
- data/ext/dry_validation_rust/benches/predicates.rs +105 -0
- data/ext/dry_validation_rust/extconf.rb +29 -0
- data/ext/dry_validation_rust/src/coercion.rs +515 -0
- data/ext/dry_validation_rust/src/engine.rs +416 -0
- data/ext/dry_validation_rust/src/error.rs +82 -0
- data/ext/dry_validation_rust/src/extract_primitive.rs +23 -0
- data/ext/dry_validation_rust/src/generated_predicates.rs +33 -0
- data/ext/dry_validation_rust/src/lib.rs +228 -0
- data/ext/dry_validation_rust/src/plan.rs +611 -0
- data/ext/dry_validation_rust/src/predicates.rs +449 -0
- data/ext/dry_validation_rust/src/ruby_bridge.rs +78 -0
- data/lib/dry/schema.rb +6 -0
- data/lib/dry/validation/rust/block_keyword_parameters.rb +20 -0
- data/lib/dry/validation/rust/config.rb +74 -0
- data/lib/dry/validation/rust/contract/result.rb +180 -0
- data/lib/dry/validation/rust/contract/values.rb +73 -0
- data/lib/dry/validation/rust/contract.rb +400 -0
- data/lib/dry/validation/rust/errors.rb +14 -0
- data/lib/dry/validation/rust/evaluator.rb +295 -0
- data/lib/dry/validation/rust/failures.rb +57 -0
- data/lib/dry/validation/rust/generated_predicates.rb +14 -0
- data/lib/dry/validation/rust/macros.rb +45 -0
- data/lib/dry/validation/rust/message.rb +41 -0
- data/lib/dry/validation/rust/message_backend.rb +115 -0
- data/lib/dry/validation/rust/message_set.rb +159 -0
- data/lib/dry/validation/rust/native.rb +25 -0
- data/lib/dry/validation/rust/path.rb +65 -0
- data/lib/dry/validation/rust/path_trie.rb +57 -0
- data/lib/dry/validation/rust/result.rb +3 -0
- data/lib/dry/validation/rust/rule.rb +62 -0
- data/lib/dry/validation/rust/schema/dsl.rb +76 -0
- data/lib/dry/validation/rust/schema/field_builder.rb +156 -0
- data/lib/dry/validation/rust/schema/field_definition.rb +99 -0
- data/lib/dry/validation/rust/schema/predicate_block.rb +56 -0
- data/lib/dry/validation/rust/schema/processor_hooks.rb +46 -0
- data/lib/dry/validation/rust/schema/result.rb +67 -0
- data/lib/dry/validation/rust/schema/ruby_type_processor.rb +44 -0
- data/lib/dry/validation/rust/schema.rb +323 -0
- data/lib/dry/validation/rust/values.rb +3 -0
- data/lib/dry/validation/rust/version.rb +10 -0
- data/lib/dry/validation/rust.rb +55 -0
- data/lib/dry/validation.rb +66 -0
- data/lib/dry-schema.rb +3 -0
- data/lib/dry-validation.rb +3 -0
- data/lib/dry_validation_rust.rb +3 -0
- data/predicates.yml +67 -0
- data/rust-toolchain.toml +9 -0
- 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
|