dry-validation 0.8.0 → 0.9.0

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGELOG.md +39 -1
  4. data/benchmarks/benchmark_schema_invalid_huge.rb +52 -0
  5. data/benchmarks/profile_schema_huge_invalid.rb +30 -0
  6. data/config/errors.yml +3 -2
  7. data/dry-validation.gemspec +2 -2
  8. data/lib/dry/validation.rb +20 -32
  9. data/lib/dry/validation/constants.rb +6 -0
  10. data/lib/dry/validation/error.rb +5 -2
  11. data/lib/dry/validation/error_compiler.rb +46 -116
  12. data/lib/dry/validation/executor.rb +105 -0
  13. data/lib/dry/validation/hint_compiler.rb +36 -68
  14. data/lib/dry/validation/message.rb +86 -0
  15. data/lib/dry/validation/message_compiler.rb +141 -0
  16. data/lib/dry/validation/message_set.rb +70 -0
  17. data/lib/dry/validation/messages/abstract.rb +1 -1
  18. data/lib/dry/validation/messages/i18n.rb +5 -0
  19. data/lib/dry/validation/predicate_registry.rb +8 -3
  20. data/lib/dry/validation/result.rb +6 -7
  21. data/lib/dry/validation/schema.rb +21 -227
  22. data/lib/dry/validation/schema/check.rb +1 -1
  23. data/lib/dry/validation/schema/class_interface.rb +193 -0
  24. data/lib/dry/validation/schema/deprecated.rb +1 -2
  25. data/lib/dry/validation/schema/key.rb +4 -0
  26. data/lib/dry/validation/schema/value.rb +12 -7
  27. data/lib/dry/validation/schema_compiler.rb +20 -1
  28. data/lib/dry/validation/type_specs.rb +70 -0
  29. data/lib/dry/validation/version.rb +1 -1
  30. data/spec/fixtures/locales/pl.yml +1 -1
  31. data/spec/integration/custom_predicates_spec.rb +37 -0
  32. data/spec/integration/error_compiler_spec.rb +39 -39
  33. data/spec/integration/form/predicates/key_spec.rb +10 -18
  34. data/spec/integration/form/predicates/size/fixed_spec.rb +8 -12
  35. data/spec/integration/form/predicates/size/range_spec.rb +7 -7
  36. data/spec/integration/hints_spec.rb +17 -0
  37. data/spec/integration/messages/i18n_spec.rb +2 -2
  38. data/spec/integration/schema/check_rules_spec.rb +2 -2
  39. data/spec/integration/schema/defining_base_schema_spec.rb +38 -0
  40. data/spec/integration/schema/dynamic_predicate_args_spec.rb +18 -0
  41. data/spec/integration/schema/macros/each_spec.rb +2 -2
  42. data/spec/integration/schema/macros/input_spec.rb +102 -10
  43. data/spec/integration/schema/macros/maybe_spec.rb +30 -0
  44. data/spec/integration/schema/nested_schemas_spec.rb +200 -0
  45. data/spec/integration/schema/nested_values_spec.rb +3 -1
  46. data/spec/integration/schema/option_with_default_spec.rb +54 -20
  47. data/spec/integration/schema/predicates/size/fixed_spec.rb +10 -10
  48. data/spec/integration/schema/predicates/size/range_spec.rb +8 -10
  49. data/spec/unit/error_compiler_spec.rb +1 -1
  50. data/spec/unit/hint_compiler_spec.rb +2 -2
  51. metadata +18 -7
  52. data/examples/rule_ast.rb +0 -25
  53. data/lib/dry/validation/error_compiler/input.rb +0 -135
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 16fb2c50b37c5d1aff843031ff3d7af6e87ab05a
4
- data.tar.gz: 64f74fc7df4b183322908ec96120d4dbbb4b38c2
3
+ metadata.gz: f2a8ab72eef1372d4c7e77ca26f239505a4bc8e3
4
+ data.tar.gz: cdf67dd43d094a74a2e1b7426e465e8a1207de70
5
5
  SHA512:
6
- metadata.gz: 244e25e33c6a1795436fc015cab5d4c23961c974eaf7208144faf69a81c040ede351c54e7225efc1695283f061225b0543e2ee6120fce29c00069725fc0ee791
7
- data.tar.gz: fc89edb2c0f6862d1771b46a77863b644947db429861a34d944cab9799d328df01e7bb0554291fa23f12d67ba545a276fe8f3c98435837485fb002bee55b5101
6
+ metadata.gz: 171bc1e3cd30d4b5d6bcb04fbaeaf3d3150a35dd07eadd293f9f4295845caa453e071c342f50fbab1f78d9f877ffb8030a5bf940c65b3a0211a8bcbd91117bdd
7
+ data.tar.gz: 0ad0b772fffd453b7f948169568fbebc57b8731d4baf18f6be47be330bf36540f2a30d99312af786489e8be633edf08601a8b4fdae3860eacb775f69714c3a40
@@ -1,7 +1,7 @@
1
1
  language: ruby
2
2
  sudo: false
3
3
  cache: bundler
4
- bundler_args: --full-index --without console benchmarks
4
+ bundler_args: --without console benchmarks
5
5
  script:
6
6
  - bundle exec rake spec
7
7
  rvm:
@@ -1,3 +1,41 @@
1
+ # v0.9.0 2016-07-08
2
+
3
+ ### Added
4
+
5
+ * Support for defining maybe-schemas via `maybe { schema { .. } }` (solnic)
6
+ * Support for interpolation of custom failure messages for custom rules (solnic)
7
+ * Support for defining a base schema **class** with config and rules (solnic)
8
+ * Support for more than 1 predicate in `input` macro (solnic)
9
+ * Class-level `define!` API for defining rules on a class (solnic)
10
+ * `:i18n` messages support merging from other paths via `messages_file` setting (solnic)
11
+ * Support for message token transformations in custom predicates (fran-worley)
12
+ * [EXPERIMENTAL] Ability to compose predicates that accept dynamic args provided by the schema (solnic)
13
+
14
+ ### Changed
15
+
16
+ * Tokens for `size?` were renamed `left` => `size_left` and `right` => `size_right` (fran-worley)
17
+
18
+ ### Fixed
19
+
20
+ * Duped key names in nested schemas no longer result in invalid error messages structure (solnic)
21
+ * Error message structure for deeply nested each/schema rules (solnic)
22
+ * Values from `option` are passed down to nested schemas when using `Schema#with` (solnic)
23
+ * Hints now work with array elements too (solnic)
24
+ * Hints for elements are no longer provided for an array when the value is not an array (solnic)
25
+ * `input` macro no longer messes up error messages for nested structures (solnic)
26
+ * `messages` and `error_compiler` are now properly inherited from base schema class (solnic)
27
+
28
+ ### Internal
29
+
30
+ * Compiling messages is now ~5% faster (solnic + splattael)
31
+ * Refactored Error and Hint compilers (solnic)
32
+ * Refactored Schema to use an internal executor objects with steps (solnic)
33
+ * Extracted root-rule into a separate validation step (solnic)
34
+ * Added `MessageSet` that result objects now use (in 1.0.0 it'll be exposed via public API) (solnic)
35
+ * We can now distinguish error messages from validation hints via `Message` and `Hint` objects (solnic)
36
+
37
+ [Compare v0.8.0...master](https://github.com/dryrb/dry-validation/compare/v0.8.0...master)
38
+
1
39
  # v0.8.0 2016-07-01
2
40
 
3
41
  ### Added
@@ -54,7 +92,7 @@
54
92
  * Make pry console optional with IRB as a default (flash-gordon)
55
93
  * Remove wrapping rules in :set nodes (solnic)
56
94
 
57
- [Compare v0.7.4...master](https://github.com/dryrb/dry-validation/compare/v0.7.4...master)
95
+ [Compare v0.7.4...v0.8.0](https://github.com/dryrb/dry-validation/compare/v0.7.4...v0.8.0)
58
96
 
59
97
  # v0.7.4 2016-04-06
60
98
 
@@ -0,0 +1,52 @@
1
+ require 'benchmark/ips'
2
+
3
+ require 'active_model'
4
+ require 'dry-validation'
5
+
6
+ I18n.locale = :en
7
+ I18n.backend.load_translations
8
+
9
+ COUNT = ENV['COUNT'].to_i
10
+ FIELDS = COUNT.times.map { |i| :"field_#{i}" }
11
+
12
+ class User
13
+ include ActiveModel::Validations
14
+
15
+ attr_reader(*FIELDS)
16
+ validates(*FIELDS, presence: true, numericality: { greater_than: FIELDS.size / 2 })
17
+
18
+ def initialize(attrs)
19
+ attrs.each do |field, value|
20
+ instance_variable_set(:"@#{field}", value)
21
+ end
22
+ end
23
+ end
24
+
25
+ schema = Dry::Validation.Schema do
26
+ configure do
27
+ config.messages = :i18n
28
+ end
29
+
30
+ FIELDS.each do |field|
31
+ required(field).value(:int?, gt?: FIELDS.size / 2)
32
+ end
33
+ end
34
+
35
+ data = FIELDS.reduce({}) { |h, f| h.update(f => FIELDS.index(f) + 1) }
36
+
37
+ puts schema.(data).inspect
38
+ puts User.new(data).validate
39
+
40
+ Benchmark.ips do |x|
41
+ x.report('ActiveModel::Validations') do
42
+ user = User.new(data)
43
+ user.validate
44
+ user.errors
45
+ end
46
+
47
+ x.report('dry-validation / schema') do
48
+ schema.(data).messages
49
+ end
50
+
51
+ x.compare!
52
+ end
@@ -0,0 +1,30 @@
1
+ require_relative 'suite'
2
+ require 'hotch'
3
+
4
+ require 'dry-validation'
5
+
6
+ I18n.locale = :en
7
+ I18n.backend.load_translations
8
+
9
+ COUNT = ENV['COUNT'].to_i
10
+ FIELDS = COUNT.times.map { |i| :"field_#{i}" }
11
+
12
+ schema = Dry::Validation.Schema do
13
+ configure do
14
+ config.messages = :i18n
15
+ end
16
+
17
+ FIELDS.each do |field|
18
+ required(field).filled(gt?: FIELDS.size / 2)
19
+ end
20
+ end
21
+
22
+ data = FIELDS.reduce({}) { |h, f| h.update(f => FIELDS.index(f) + 1) }
23
+
24
+ puts schema.(data).inspect
25
+
26
+ Hotch() do
27
+ 100.times do
28
+ schema.(data).messages
29
+ end
30
+ end
@@ -7,6 +7,7 @@ en:
7
7
  excludes?: "must not include %{value}"
8
8
 
9
9
  excluded_from?: "must not be one of: %{list}"
10
+ exclusion?: "must not be one of: %{list}"
10
11
 
11
12
  eql?: "must be equal to %{left}"
12
13
 
@@ -72,10 +73,10 @@ en:
72
73
  size?:
73
74
  arg:
74
75
  default: "size must be %{size}"
75
- range: "size must be within %{left} - %{right}"
76
+ range: "size must be within %{size_left} - %{size_right}"
76
77
 
77
78
  value:
78
79
  string:
79
80
  arg:
80
81
  default: "length must be %{size}"
81
- range: "length must be within %{left} - %{right}"
82
+ range: "length must be within %{size_left} - %{size_right}"
@@ -10,8 +10,8 @@ Gem::Specification.new do |spec|
10
10
  spec.homepage = 'https://github.com/dryrb/dry-validation'
11
11
  spec.license = 'MIT'
12
12
 
13
- spec.files = `git ls-files -z`.split("\x0")
14
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
13
+ spec.files = `git ls-files -z`.split("\x0") - ['bin/console']
14
+ spec.executables = []
15
15
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
16
  spec.require_paths = ['lib']
17
17
 
@@ -7,6 +7,25 @@ require 'dry/validation/schema/form'
7
7
  require 'dry/validation/schema/json'
8
8
 
9
9
  module Dry
10
+ # FIXME: move this to dry-logic if it works lol
11
+ require 'dry/logic/predicate'
12
+ module Logic
13
+ class Predicate
14
+ class Curried < Predicate
15
+ def evaluate_args!(schema)
16
+ @args = args.map { |arg|
17
+ arg.is_a?(UnboundMethod) ? arg.bind(schema).() : arg
18
+ }
19
+ self
20
+ end
21
+ end
22
+
23
+ def evaluate_args!(*)
24
+ self
25
+ end
26
+ end
27
+ end
28
+
10
29
  module Validation
11
30
  MissingMessageError = Class.new(StandardError)
12
31
  InvalidSchemaError = Class.new(StandardError)
@@ -17,38 +36,7 @@ module Dry
17
36
 
18
37
  def self.Schema(base = Schema, **options, &block)
19
38
  schema_class = Class.new(base.is_a?(Schema) ? base.class : base)
20
-
21
- dsl_opts = {
22
- schema_class: schema_class,
23
- registry: schema_class.registry,
24
- parent: options[:parent]
25
- }
26
-
27
- dsl_ext = schema_class.config.dsl_extensions
28
-
29
- dsl = Schema::Value.new(dsl_opts)
30
- dsl_ext.__send__(:extend_object, dsl) if dsl_ext
31
- dsl.predicates(options[:predicates]) if options.key?(:predicates)
32
- dsl.instance_exec(&block) if block
33
-
34
- klass = dsl.schema_class
35
-
36
- base_rules = klass.config.rules + (options.fetch(:rules, []) + dsl.rules)
37
-
38
- rules =
39
- if klass.config.input
40
- input_rule = dsl.__send__(klass.config.input)
41
- [input_rule.and(dsl.with(rules: base_rules))]
42
- else
43
- base_rules
44
- end
45
-
46
- klass.configure do |config|
47
- config.rules = rules
48
- config.checks = config.checks + dsl.checks
49
- config.path = dsl.path
50
- config.type_map = klass.build_type_map(dsl.type_map) if config.type_specs
51
- end
39
+ klass = schema_class.define(options.merge(schema_class: schema_class), &block)
52
40
 
53
41
  if options[:build] == false
54
42
  klass
@@ -0,0 +1,6 @@
1
+ module Dry
2
+ module Validation
3
+ EMPTY_ARRAY = [].freeze
4
+ EMPTY_HASH = {}.freeze
5
+ end
6
+ end
@@ -15,8 +15,11 @@ module Dry
15
15
  end
16
16
 
17
17
  def to_ast
18
- node = [:error, [name, result.to_ast]]
19
- schema? ? [:schema, node] : node
18
+ if schema?
19
+ [:schema, [name, result.response.to_ast]]
20
+ else
21
+ [:error, [name, result.to_ast]]
22
+ end
20
23
  end
21
24
  end
22
25
  end
@@ -1,147 +1,77 @@
1
+ require 'dry/validation/message_compiler'
2
+
1
3
  module Dry
2
4
  module Validation
3
- class ErrorCompiler
4
- attr_reader :messages, :hints, :options
5
-
6
- DEFAULT_RESULT = {}.freeze
7
- EMPTY_HINTS = [].freeze
8
- KEY_SEPARATOR = '.'.freeze
9
-
10
- def initialize(messages, options = {})
11
- @messages = messages
12
- @options = Hash[options]
13
- @hints = @options.fetch(:hints, DEFAULT_RESULT)
14
- @full = options.fetch(:full, false)
5
+ class ErrorCompiler < MessageCompiler
6
+ def message_type
7
+ :failure
15
8
  end
16
9
 
17
- def full?
18
- @full
10
+ def message_class
11
+ Message
19
12
  end
20
13
 
21
- def call(ast, *args)
22
- merge(ast.map { |node| visit(node, *args) }) || DEFAULT_RESULT
23
- end
14
+ def visit_error(node, opts = EMPTY_HASH)
15
+ rule, error = node
16
+ node_path = Array(opts.fetch(:path, rule))
24
17
 
25
- def with(new_options)
26
- self.class.new(messages, options.merge(new_options))
27
- end
18
+ path = if rule.is_a?(Array) && rule.size > node_path.size
19
+ rule
20
+ else
21
+ node_path
22
+ end
28
23
 
29
- def visit(node, *args)
30
- __send__(:"visit_#{node[0]}", node[1], *args)
31
- end
24
+ path.compact!
32
25
 
33
- def visit_schema(node, *args)
34
- visit_error(node[1], true)
35
- end
26
+ template = messages[rule, default_lookup_options]
36
27
 
37
- def visit_set(node, *args)
38
- call(node, *args)
39
- end
40
-
41
- def visit_error(error, schema = false)
42
- name, other = error
43
- message = messages[name]
44
-
45
- if message
46
- { name => [message] }
28
+ if template
29
+ predicate, args, tokens = visit(error, opts.merge(path: path, message: false))
30
+ message_class[predicate, path, template % tokens, rule: rule, args: args]
47
31
  else
48
- result = schema ? visit(other, name) : visit(other)
49
-
50
- if result.is_a?(Array)
51
- merge(result)
52
- elsif !schema
53
- merge_hints(result)
54
- elsif schema
55
- merge_hints(result, hints[schema] || DEFAULT_RESULT)
56
- else
57
- result
58
- end
32
+ visit(error, opts.merge(rule: rule, path: path))
59
33
  end
60
34
  end
61
35
 
62
- def visit_input(node, path = nil)
63
- name, result = node
64
- visit(result, path || name)
65
- end
36
+ def visit_input(node, opts = EMPTY_HASH)
37
+ rule, result = node
38
+ opt_rule = opts[:rule]
66
39
 
67
- def visit_result(node, name = nil)
68
- value, other = node
69
- input_visitor(name, value).visit(other)
70
- end
71
-
72
- def visit_implication(node)
73
- _, right = node
74
- visit(right)
75
- end
76
-
77
- def visit_key(rule)
78
- _, predicate = rule
79
- visit(predicate)
80
- end
81
-
82
- def visit_attr(rule)
83
- _, predicate = rule
84
- visit(predicate)
40
+ if opts[:each] && opt_rule.is_a?(Array)
41
+ visit(result, opts.merge(rule: rule, path: opts[:path] + [opt_rule.last]))
42
+ else
43
+ visit(result, opts.merge(rule: rule))
44
+ end
85
45
  end
86
46
 
87
- def visit_val(node)
88
- visit(node)
47
+ def visit_result(node, opts = EMPTY_HASH)
48
+ input, other = node
49
+ visit(other, opts.merge(input: input))
89
50
  end
90
51
 
91
- def dump_messages(hash)
92
- hash.each_with_object({}) do |(key, val), res|
93
- res[key] =
94
- case val
95
- when Hash then dump_messages(val)
96
- when Array then val.map(&:to_s)
97
- end
98
- end
52
+ def visit_each(node, opts = EMPTY_HASH)
53
+ node.map { |el| visit(el, opts.merge(each: true)) }
99
54
  end
100
55
 
101
- private
102
-
103
- def merge_hints(messages, hints = self.hints)
104
- messages.each_with_object({}) do |(name, msgs), res|
105
- res[name] =
106
- if msgs.is_a?(Hash)
107
- res[name] = merge_hints(msgs, hints)
108
- else
109
- all_hints = (hints[name] || EMPTY_HINTS)
56
+ def visit_schema(node, opts = EMPTY_HASH)
57
+ path, other = node
110
58
 
111
- if all_hints.is_a?(Array)
112
- all_msgs = msgs + all_hints
113
- all_msgs.uniq!(&:signature)
114
- all_msgs
115
- else
116
- msgs
117
- end
118
- end
59
+ if opts[:path]
60
+ opts[:path] << path.last
61
+ visit(other, opts)
62
+ else
63
+ visit(other, opts.merge(path: [path]))
119
64
  end
120
65
  end
121
66
 
122
- def normalize_name(name)
123
- Array(name).join('.').to_sym
124
- end
125
-
126
- def merge(result)
127
- result.reduce { |a, e| deep_merge(a, e) } || DEFAULT_RESULT
67
+ def visit_check(node, opts = EMPTY_HASH)
68
+ path, other = node
69
+ visit(other, opts.merge(path: Array(path)))
128
70
  end
129
71
 
130
- def deep_merge(left, right)
131
- left.merge(right) do |_, a, e|
132
- if a.is_a?(Hash)
133
- deep_merge(a, e)
134
- else
135
- a + e
136
- end
137
- end
138
- end
139
-
140
- def input_visitor(name, input)
141
- Input.new(messages, options.merge(name: name, input: input))
72
+ def lookup_options(opts, arg_vals = [])
73
+ super.update(val_type: opts[:input].class)
142
74
  end
143
75
  end
144
76
  end
145
77
  end
146
-
147
- require 'dry/validation/error_compiler/input'