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,44 @@
1
+ [package]
2
+ name = "dry_validation_rust_native"
3
+ version = "0.1.0-pre.5"
4
+ edition = "2021"
5
+ rust-version = "1.75"
6
+ license = "MIT"
7
+ publish = false
8
+
9
+ [lib]
10
+ name = "native"
11
+ crate-type = ["cdylib", "rlib"]
12
+
13
+ [dependencies]
14
+ bigdecimal = "0.4"
15
+ chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
16
+ magnus = { version = "~0.8.2", default-features = false }
17
+ rb-sys = { version = "~0.9.124", default-features = false, features = ["stable-api-compiled-fallback"] }
18
+ serde = { version = "1", features = ["derive"] }
19
+ serde_json = { version = "1", features = ["unbounded_depth"] }
20
+
21
+ [dev-dependencies]
22
+ criterion = "0.5.1"
23
+ magnus = { version = "~0.8.2", default-features = false, features = ["embed"] }
24
+
25
+ [[bench]]
26
+ name = "plan_compile"
27
+ harness = false
28
+
29
+ [[bench]]
30
+ name = "coercion"
31
+ harness = false
32
+
33
+ [[bench]]
34
+ name = "predicates"
35
+ harness = false
36
+
37
+ [[bench]]
38
+ name = "full_schema"
39
+ harness = false
40
+
41
+ [profile.release]
42
+ codegen-units = 1
43
+ lto = "thin"
44
+ strip = "debuginfo"
@@ -0,0 +1,77 @@
1
+ use criterion::{black_box, Criterion};
2
+ use magnus::{Error, Ruby};
3
+ use native::benchmark::CoercionRuntime;
4
+
5
+ fn bench_coercion(
6
+ criterion: &mut Criterion,
7
+ runtime: &CoercionRuntime,
8
+ ruby: &Ruby,
9
+ group_name: &str,
10
+ kind: &str,
11
+ inputs: &[&str],
12
+ ) {
13
+ let mut group = criterion.benchmark_group(group_name);
14
+ for input in inputs {
15
+ group.bench_function(*input, |bencher| {
16
+ bencher.iter(|| {
17
+ runtime
18
+ .coerce(ruby, black_box(kind), black_box(input))
19
+ .expect("benchmark coercion inputs must not raise Ruby errors")
20
+ });
21
+ });
22
+ }
23
+ group.finish();
24
+ }
25
+
26
+ fn run(ruby: &Ruby) -> Result<(), Error> {
27
+ let runtime = CoercionRuntime::new(ruby)?;
28
+ let mut criterion = Criterion::default().configure_from_args();
29
+
30
+ bench_coercion(
31
+ &mut criterion,
32
+ &runtime,
33
+ ruby,
34
+ "integer_coercion",
35
+ "integer",
36
+ &["42", "-99", "1_000", "0xFF"],
37
+ );
38
+ bench_coercion(
39
+ &mut criterion,
40
+ &runtime,
41
+ ruby,
42
+ "float_coercion",
43
+ "float",
44
+ &["3.14", "-2.5e10", "Infinity"],
45
+ );
46
+ bench_coercion(
47
+ &mut criterion,
48
+ &runtime,
49
+ ruby,
50
+ "bool_coercion",
51
+ "bool",
52
+ &["true", "false", "1", "0", "yes", "no"],
53
+ );
54
+ bench_coercion(
55
+ &mut criterion,
56
+ &runtime,
57
+ ruby,
58
+ "date_coercion",
59
+ "date",
60
+ &["2024-01-01", "2024-01-01T12:00:00Z"],
61
+ );
62
+ bench_coercion(
63
+ &mut criterion,
64
+ &runtime,
65
+ ruby,
66
+ "decimal_coercion",
67
+ "decimal",
68
+ &["123.456", "0.0000001"],
69
+ );
70
+
71
+ criterion.final_summary();
72
+ Ok(())
73
+ }
74
+
75
+ fn main() {
76
+ Ruby::init(run).expect("embedded Ruby benchmark setup must succeed");
77
+ }
@@ -0,0 +1,189 @@
1
+ use criterion::{black_box, Criterion};
2
+ use magnus::{gc, Error, RHash, Ruby, Value};
3
+ use native::benchmark::FullSchemaRuntime;
4
+
5
+ fn field(name: &str, kind: &str, filled: bool, predicate: Option<(&str, i64)>) -> String {
6
+ let predicates = predicate.map_or_else(
7
+ || "[]".to_owned(),
8
+ |(name, argument)| format!(r#"[{{"name":"{name}","argument":{argument}}}]"#),
9
+ );
10
+ format!(
11
+ r#"{{"name":"{name}","required":true,"nullable":false,"filled":{filled},"type":"{kind}","member":null,"children":[],"predicates":{predicates}}}"#
12
+ )
13
+ }
14
+
15
+ fn plan(fields: Vec<String>) -> String {
16
+ format!(
17
+ r#"{{"engine_version":1,"mode":"params","validate_keys":true,"fields":[{}]}}"#,
18
+ fields.join(",")
19
+ )
20
+ }
21
+
22
+ fn flat_plan(field_count: usize) -> String {
23
+ plan(
24
+ (0..field_count)
25
+ .map(|index| field(&format!("field_{index}"), "integer", false, Some(("gt", 0))))
26
+ .collect(),
27
+ )
28
+ }
29
+
30
+ fn nested_plan(depth: usize) -> String {
31
+ let mut child = field("value", "integer", false, Some(("gt", 0)));
32
+ for index in (0..depth).rev() {
33
+ child = format!(
34
+ r#"{{"name":"level_{index}","required":true,"nullable":false,"filled":false,"type":"hash","member":null,"children":[{child}],"predicates":[]}}"#
35
+ );
36
+ }
37
+ plan(vec![child])
38
+ }
39
+
40
+ fn array_plan() -> String {
41
+ let member_children = [
42
+ field("id", "integer", false, Some(("gt", 0))),
43
+ field("name", "string", true, None),
44
+ field("age", "integer", false, Some(("gteq", 18))),
45
+ field("active", "bool", false, None),
46
+ field("role", "string", true, None),
47
+ ]
48
+ .join(",");
49
+ plan(vec![format!(
50
+ r#"{{"name":"items","required":true,"nullable":false,"filled":false,"type":"array","member":{{"name":null,"required":true,"nullable":false,"filled":false,"type":"hash","member":null,"children":[{member_children}],"predicates":[]}},"children":[],"predicates":[]}}"#
51
+ )])
52
+ }
53
+
54
+ fn flat_payload(ruby: &Ruby, field_count: usize, invalid: bool) -> Result<RHash, Error> {
55
+ let payload = ruby.hash_new_capa(field_count);
56
+ for index in 0..field_count {
57
+ let value = if invalid { "invalid" } else { "1" };
58
+ payload.aset(format!("field_{index}"), value)?;
59
+ }
60
+ Ok(payload)
61
+ }
62
+
63
+ fn nested_payload(ruby: &Ruby, depth: usize) -> Result<RHash, Error> {
64
+ let mut payload = ruby.hash_from_iter([("value", "1")]);
65
+ for index in (0..depth).rev() {
66
+ payload = ruby.hash_from_iter([(format!("level_{index}"), payload)]);
67
+ }
68
+ Ok(payload)
69
+ }
70
+
71
+ fn array_payload(ruby: &Ruby, invalid: bool) -> Result<RHash, Error> {
72
+ let items = ruby.ary_new_capa(100);
73
+ for index in 0..100 {
74
+ let item = ruby.hash_new_capa(5);
75
+ let invalid_item = invalid && index == 0;
76
+ item.aset("id", if invalid_item { "invalid" } else { "1" })?;
77
+ item.aset("name", if invalid_item { "" } else { "person" })?;
78
+ item.aset("age", if invalid_item { "invalid" } else { "30" })?;
79
+ item.aset("active", "true")?;
80
+ item.aset("role", "member")?;
81
+ items.push(item)?;
82
+ }
83
+ Ok(ruby.hash_from_iter([("items", items)]))
84
+ }
85
+
86
+ fn bench_case(
87
+ criterion: &mut Criterion,
88
+ ruby: &Ruby,
89
+ name: &str,
90
+ plan_json: String,
91
+ inputs: Vec<(RHash, usize)>,
92
+ ) -> Result<(), Error> {
93
+ let runtime = FullSchemaRuntime::new(ruby, plan_json)?;
94
+ for (input, expected_errors) in &inputs {
95
+ gc::register_mark_object(*input);
96
+ assert_eq!(
97
+ runtime.error_count(ruby, *input)?,
98
+ *expected_errors,
99
+ "{name} setup"
100
+ );
101
+ }
102
+
103
+ let mut input_index = 0;
104
+ criterion.bench_function(name, |bencher| {
105
+ bencher.iter(|| {
106
+ let input = inputs[input_index % inputs.len()].0;
107
+ input_index += 1;
108
+ runtime
109
+ .call(black_box(input))
110
+ .expect("pre-built benchmark inputs must not raise Ruby errors")
111
+ });
112
+ });
113
+ Ok(())
114
+ }
115
+
116
+ fn run(ruby: &Ruby) -> Result<(), Error> {
117
+ ruby.eval::<Value>(
118
+ "module Dry; module Validation; module Rust; module Native; class SchemaResult; end; end; end; end; end",
119
+ )?;
120
+ let mut criterion = Criterion::default().configure_from_args();
121
+ bench_case(
122
+ &mut criterion,
123
+ ruby,
124
+ "full_schema/small_form",
125
+ flat_plan(5),
126
+ vec![(flat_payload(ruby, 5, false)?, 0)],
127
+ )?;
128
+ bench_case(
129
+ &mut criterion,
130
+ ruby,
131
+ "full_schema/medium_form",
132
+ flat_plan(25),
133
+ vec![
134
+ (flat_payload(ruby, 25, false)?, 0),
135
+ (flat_payload(ruby, 25, false)?, 0),
136
+ (flat_payload(ruby, 25, false)?, 0),
137
+ (flat_payload(ruby, 25, false)?, 0),
138
+ (flat_payload(ruby, 25, true)?, 25),
139
+ ],
140
+ )?;
141
+ bench_case(
142
+ &mut criterion,
143
+ ruby,
144
+ "full_schema/large_form",
145
+ flat_plan(100),
146
+ vec![
147
+ (flat_payload(ruby, 100, false)?, 0),
148
+ (flat_payload(ruby, 100, true)?, 100),
149
+ ],
150
+ )?;
151
+ bench_case(
152
+ &mut criterion,
153
+ ruby,
154
+ "full_schema/nested_object",
155
+ nested_plan(10),
156
+ vec![(nested_payload(ruby, 10)?, 0)],
157
+ )?;
158
+ bench_case(
159
+ &mut criterion,
160
+ ruby,
161
+ "full_schema/array_of_objects",
162
+ array_plan(),
163
+ vec![
164
+ (array_payload(ruby, false)?, 0),
165
+ (array_payload(ruby, false)?, 0),
166
+ (array_payload(ruby, false)?, 0),
167
+ (array_payload(ruby, false)?, 0),
168
+ (array_payload(ruby, false)?, 0),
169
+ (array_payload(ruby, false)?, 0),
170
+ (array_payload(ruby, false)?, 0),
171
+ (array_payload(ruby, false)?, 0),
172
+ (array_payload(ruby, false)?, 0),
173
+ (array_payload(ruby, true)?, 3),
174
+ ],
175
+ )?;
176
+ bench_case(
177
+ &mut criterion,
178
+ ruby,
179
+ "full_schema/all_invalid",
180
+ flat_plan(20),
181
+ vec![(flat_payload(ruby, 20, true)?, 20)],
182
+ )?;
183
+ criterion.final_summary();
184
+ Ok(())
185
+ }
186
+
187
+ fn main() {
188
+ Ruby::init(run).expect("embedded Ruby benchmark setup must succeed");
189
+ }
@@ -0,0 +1,37 @@
1
+ use criterion::{black_box, criterion_group, criterion_main, Criterion};
2
+
3
+ fn plan_json(field_count: usize) -> String {
4
+ let fields = (0..field_count)
5
+ .map(|index| {
6
+ format!(
7
+ r#"{{"name":"field_{index}","required":true,"nullable":false,"filled":false,"type":"string","member":null,"children":[],"predicates":[{{"name":"min_size","argument":1}}]}}"#
8
+ )
9
+ })
10
+ .collect::<Vec<_>>()
11
+ .join(",");
12
+
13
+ format!(r#"{{"engine_version":1,"mode":"params","validate_keys":true,"fields":[{fields}]}}"#)
14
+ }
15
+
16
+ fn bench_plan_compile(c: &mut Criterion, name: &str, field_count: usize) {
17
+ let plan = plan_json(field_count);
18
+
19
+ c.bench_function(name, |bencher| {
20
+ bencher.iter(|| black_box(native::fuzzing::parse_plan(black_box(&plan))));
21
+ });
22
+ }
23
+
24
+ fn small_schema(c: &mut Criterion) {
25
+ bench_plan_compile(c, "small_schema", 5);
26
+ }
27
+
28
+ fn medium_schema(c: &mut Criterion) {
29
+ bench_plan_compile(c, "medium_schema", 50);
30
+ }
31
+
32
+ fn large_schema(c: &mut Criterion) {
33
+ bench_plan_compile(c, "large_schema", 200);
34
+ }
35
+
36
+ criterion_group!(plan_compile, small_schema, medium_schema, large_schema);
37
+ criterion_main!(plan_compile);
@@ -0,0 +1,105 @@
1
+ use criterion::{black_box, Criterion};
2
+ use magnus::{prelude::*, Error, Ruby, Value};
3
+ use native::benchmark::{PredicateCase, PredicateRuntime};
4
+
5
+ fn bench_predicate(
6
+ criterion: &mut Criterion,
7
+ ruby: &Ruby,
8
+ runtime: &PredicateRuntime,
9
+ group_name: &str,
10
+ case_name: &str,
11
+ predicate_case: PredicateCase,
12
+ value: Value,
13
+ ) {
14
+ let mut group = criterion.benchmark_group(group_name);
15
+ group.bench_function(case_name, |bencher| {
16
+ bencher.iter(|| {
17
+ runtime
18
+ .evaluate(ruby, predicate_case, black_box(value))
19
+ .expect("benchmark predicate inputs must not raise Ruby errors")
20
+ });
21
+ });
22
+ group.finish();
23
+ }
24
+
25
+ fn run(ruby: &Ruby) -> Result<(), Error> {
26
+ let runtime = PredicateRuntime::new();
27
+ let mut criterion = Criterion::default().configure_from_args();
28
+ let integer = ruby.integer_from_i64(19).as_value();
29
+ let even_integer = ruby.integer_from_i64(20).as_value();
30
+ let float = ruby.float_from_f64(1.5).as_value();
31
+ let string = ruby.str_new("abc").as_value();
32
+ let array = ruby.ary_from_iter([1, 2, 3]).as_value();
33
+ let hash = ruby
34
+ .hash_from_iter([("one", 1), ("two", 2), ("three", 3)])
35
+ .as_value();
36
+
37
+ for (name, predicate_case, value) in [
38
+ ("gt_integer", PredicateCase::GtInteger, integer),
39
+ ("gteq_integer", PredicateCase::GteqInteger, integer),
40
+ ("lt_integer", PredicateCase::LtInteger, integer),
41
+ ("lteq_integer", PredicateCase::LteqInteger, integer),
42
+ ("gt_float", PredicateCase::GtFloat, float),
43
+ ("gteq_float", PredicateCase::GteqFloat, float),
44
+ ("lt_float", PredicateCase::LtFloat, float),
45
+ ("lteq_float", PredicateCase::LteqFloat, float),
46
+ ] {
47
+ bench_predicate(
48
+ &mut criterion,
49
+ ruby,
50
+ &runtime,
51
+ "comparison",
52
+ name,
53
+ predicate_case,
54
+ value,
55
+ );
56
+ }
57
+
58
+ for (name, predicate_case, value) in [
59
+ ("size_string", PredicateCase::Size, string),
60
+ ("min_size_string", PredicateCase::MinSize, string),
61
+ ("max_size_string", PredicateCase::MaxSize, string),
62
+ ("size_array", PredicateCase::Size, array),
63
+ ("min_size_array", PredicateCase::MinSize, array),
64
+ ("max_size_array", PredicateCase::MaxSize, array),
65
+ ("size_hash", PredicateCase::Size, hash),
66
+ ("min_size_hash", PredicateCase::MinSize, hash),
67
+ ("max_size_hash", PredicateCase::MaxSize, hash),
68
+ ] {
69
+ bench_predicate(
70
+ &mut criterion,
71
+ ruby,
72
+ &runtime,
73
+ "size",
74
+ name,
75
+ predicate_case,
76
+ value,
77
+ );
78
+ }
79
+
80
+ bench_predicate(
81
+ &mut criterion,
82
+ ruby,
83
+ &runtime,
84
+ "parity",
85
+ "odd_integer",
86
+ PredicateCase::Odd,
87
+ integer,
88
+ );
89
+ bench_predicate(
90
+ &mut criterion,
91
+ ruby,
92
+ &runtime,
93
+ "parity",
94
+ "even_integer",
95
+ PredicateCase::Even,
96
+ even_integer,
97
+ );
98
+
99
+ criterion.final_summary();
100
+ Ok(())
101
+ }
102
+
103
+ fn main() {
104
+ Ruby::init(run).expect("embedded Ruby benchmark setup must succeed");
105
+ }
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+
5
+ begin
6
+ require 'rb_sys/mkmf'
7
+ rescue LoadError
8
+ local_rb_sys = ENV.fetch('RB_SYS_GEM_LIB', nil)
9
+ if local_rb_sys && File.directory?(local_rb_sys)
10
+ $LOAD_PATH.unshift(local_rb_sys)
11
+ require 'rb_sys/mkmf'
12
+ else
13
+ abort 'rb_sys is required to build dry-validation-rust (gem install rb_sys)'
14
+ end
15
+ end
16
+
17
+ create_rust_makefile('dry_validation_rust/native') do |config|
18
+ config.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', 'release').to_sym
19
+ config.ext_dir = '.'
20
+ config.env = {
21
+ 'BINDGEN_EXTRA_CLANG_ARGS' => '-include stdbool.h'
22
+ }
23
+ config.extra_rustup_targets = %w[
24
+ aarch64-unknown-linux-gnu
25
+ x86_64-apple-darwin
26
+ aarch64-apple-darwin
27
+ ]
28
+ config.use_stable_api_compiled_fallback = true
29
+ end