dry-validation 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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'