dry-validation 0.9.5 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -8
  3. data/CHANGELOG.md +34 -1
  4. data/Gemfile +2 -0
  5. data/Rakefile +12 -3
  6. data/config/errors.yml +1 -0
  7. data/dry-validation.gemspec +3 -2
  8. data/lib/dry/validation/executor.rb +3 -27
  9. data/lib/dry/validation/extensions/monads.rb +17 -0
  10. data/lib/dry/validation/extensions/struct.rb +32 -0
  11. data/lib/dry/validation/extensions.rb +7 -0
  12. data/lib/dry/validation/input_processor_compiler.rb +19 -3
  13. data/lib/dry/validation/message.rb +43 -30
  14. data/lib/dry/validation/message_compiler/visitor_opts.rb +39 -0
  15. data/lib/dry/validation/message_compiler.rb +98 -52
  16. data/lib/dry/validation/message_set.rb +59 -30
  17. data/lib/dry/validation/predicate_registry.rb +17 -6
  18. data/lib/dry/validation/result.rb +39 -17
  19. data/lib/dry/validation/schema/check.rb +5 -4
  20. data/lib/dry/validation/schema/class_interface.rb +6 -13
  21. data/lib/dry/validation/schema/dsl.rb +9 -3
  22. data/lib/dry/validation/schema/form.rb +12 -3
  23. data/lib/dry/validation/schema/json.rb +12 -3
  24. data/lib/dry/validation/schema/key.rb +6 -6
  25. data/lib/dry/validation/schema/rule.rb +14 -8
  26. data/lib/dry/validation/schema/value.rb +23 -21
  27. data/lib/dry/validation/schema.rb +9 -12
  28. data/lib/dry/validation/schema_compiler.rb +16 -2
  29. data/lib/dry/validation/version.rb +1 -1
  30. data/lib/dry/validation.rb +11 -23
  31. data/spec/extensions/monads/result_spec.rb +38 -0
  32. data/spec/extensions/struct/schema_spec.rb +32 -0
  33. data/spec/integration/custom_predicates_spec.rb +7 -6
  34. data/spec/integration/form/predicates/size/fixed_spec.rb +0 -2
  35. data/spec/integration/form/predicates/size/range_spec.rb +0 -2
  36. data/spec/integration/hints_spec.rb +2 -6
  37. data/spec/integration/json/defining_base_schema_spec.rb +41 -0
  38. data/spec/integration/{error_compiler_spec.rb → message_compiler_spec.rb} +79 -131
  39. data/spec/integration/result_spec.rb +26 -4
  40. data/spec/integration/schema/check_with_nested_el_spec.rb +1 -1
  41. data/spec/integration/schema/check_with_nth_el_spec.rb +1 -1
  42. data/spec/integration/schema/defining_base_schema_spec.rb +3 -0
  43. data/spec/integration/schema/dynamic_predicate_args_spec.rb +34 -9
  44. data/spec/integration/schema/form/defining_base_schema_spec.rb +41 -0
  45. data/spec/integration/schema/json_spec.rb +1 -0
  46. data/spec/integration/schema/macros/input_spec.rb +26 -0
  47. data/spec/integration/schema/macros/rule_spec.rb +2 -1
  48. data/spec/integration/schema/macros/value_spec.rb +1 -1
  49. data/spec/integration/schema/macros/when_spec.rb +1 -24
  50. data/spec/integration/schema/or_spec.rb +87 -0
  51. data/spec/integration/schema/predicates/custom_spec.rb +4 -4
  52. data/spec/integration/schema/predicates/even_spec.rb +10 -10
  53. data/spec/integration/schema/predicates/odd_spec.rb +10 -10
  54. data/spec/integration/schema/predicates/size/fixed_spec.rb +0 -3
  55. data/spec/integration/schema/predicates/size/range_spec.rb +0 -2
  56. data/spec/integration/schema/predicates/type_spec.rb +22 -0
  57. data/spec/integration/schema/using_types_spec.rb +14 -41
  58. data/spec/integration/schema/validate_spec.rb +83 -0
  59. data/spec/integration/schema/xor_spec.rb +5 -5
  60. data/spec/integration/schema_builders_spec.rb +4 -2
  61. data/spec/integration/schema_spec.rb +8 -0
  62. data/spec/shared/message_compiler.rb +11 -0
  63. data/spec/shared/predicate_helper.rb +5 -3
  64. data/spec/spec_helper.rb +15 -0
  65. data/spec/unit/input_processor_compiler/form_spec.rb +3 -3
  66. data/spec/unit/message_compiler/visit_failure_spec.rb +38 -0
  67. data/spec/unit/message_compiler/visit_spec.rb +16 -0
  68. data/spec/unit/message_compiler_spec.rb +7 -0
  69. data/spec/unit/predicate_registry_spec.rb +2 -2
  70. data/spec/unit/schema/key_spec.rb +19 -12
  71. data/spec/unit/schema/rule_spec.rb +14 -6
  72. data/spec/unit/schema/value_spec.rb +49 -52
  73. metadata +50 -20
  74. data/lib/dry/validation/constants.rb +0 -6
  75. data/lib/dry/validation/error.rb +0 -26
  76. data/lib/dry/validation/error_compiler.rb +0 -81
  77. data/lib/dry/validation/hint_compiler.rb +0 -104
  78. data/spec/unit/error_compiler_spec.rb +0 -7
  79. data/spec/unit/hint_compiler_spec.rb +0 -51
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8275fd162e586ba1fcbb31efe723708f553e31f6
4
- data.tar.gz: d95d66f951d31033be04e04258b3ac0dca700c8a
3
+ metadata.gz: 94d71785e9206636cca2df3765b30ebbf7009b38
4
+ data.tar.gz: 4d725f61ebc1f28a9dc54a0f6363caa8b66108a1
5
5
  SHA512:
6
- metadata.gz: 147d0f89a321c42bfa3799f37c3bd5700faefd22189a1c8bd49af812de16f4de80ab0bb9a98b253c688c66582f433bccacaccbcda5f0820fb3d6fb13c9c26ae5
7
- data.tar.gz: 7e32822ddbe928051bceab4e7bf4e0ad9378d63e5e95972c4c6f98056dd5a7a66a81e3e4b4a8c85c8391de782d95e1fdfd6932ffeeb96776743bf0f9cf1ee63e
6
+ metadata.gz: 1fe8b86ff9acc5ee111d271a9e20b6a1d07af65719090f65cebe3ec45337bef7534abec1ec4cc9879b281546986cd197c0c6e75d96f7a1994ca9fba890ab7382
7
+ data.tar.gz: 252091265ccc93ca446e77fc2895ace365505a602f9ff4274df1fb9d00e3d3e582b976c2c3bb58a74ca9bcc1107db1f09b0d996b0687426dc43c6578c84ebc5c
data/.travis.yml CHANGED
@@ -3,26 +3,19 @@ sudo: false
3
3
  cache: bundler
4
4
  bundler_args: --without console benchmarks
5
5
  script:
6
- - bundle exec rake spec
6
+ - bundle exec rake
7
7
  rvm:
8
- - 2.0
9
8
  - 2.1.10
10
9
  - 2.2.5
11
10
  - 2.3.1
12
11
  - rbx-2
13
12
  - jruby-9.1.1.0
14
- - ruby-head
15
13
  env:
16
14
  global:
17
15
  - JRUBY_OPTS='--dev -J-Xmx1024M'
18
16
  matrix:
19
17
  allow_failures:
20
18
  - rvm: rbx-2
21
- - rvm: ruby-head
22
- - rvm: jruby-head
23
- include:
24
- - rvm: jruby-head
25
- before_install: gem install bundler --no-ri --no-rdoc
26
19
 
27
20
  notifications:
28
21
  email: false
data/CHANGELOG.md CHANGED
@@ -1,4 +1,37 @@
1
- # v0.9.5 2016-08-15
1
+ # v0.10.0 to-be-released
2
+
3
+ ### Added
4
+
5
+ * Support for `validate` DSL which accepts an arbitratry validation block that gets executed in the context of a schema object and is treated as a custom predicate (solnic)
6
+ * Support for `or` error messages ie "must be a string or must be an integer" (solnic)
7
+ * Support for retrieving error messages exclusively via `schema.(input).errors` (solnic)
8
+ * Support for retrieving hint messages exclusively via `schema.(input).hints` (solnic)
9
+ * Support for opt-in extensions loaded via `Dry::Validation.load_extensions(:my_ext)` (flash-gordon)
10
+ * Add `:monads` extension which transforms a result instance to `Either` monad, `schema.(input).to_either` (flash-gordon)
11
+ * Add `dry-struct` integration via an extension activated by `Dry::Validation.load_extension(:struct)` (flash-gordon)
12
+
13
+ ### Fixed
14
+
15
+ * Input rules (defined via `input` macro) are now lazy-initialized which makes it work with predicates defined on the schema object (solnic)
16
+ * Hints are properly generated based on argument type in cases like `size?`, where the message should be different for strings (uses "length") or other types (uses "size") (solnic)
17
+ * Defining nested keys without `schema` blocks results in `ArgumentError` (solnic)
18
+
19
+ ### Changed
20
+
21
+ * [BREAKING] `when` macro no longer supports multiple disconnected rules in its block, whatever the block returns will be used for the implication (solnic)
22
+ * [BREAKING] `rule(some_name: %i(some keys))` will *always* use `:some_name` as the key for failure messages (solnic)
23
+
24
+ ### Internal
25
+
26
+ * ~2 x performance boost (solnic)
27
+ * Rule AST was updated to latest dry-logic (solnic)
28
+ * `MessageCompiler` was drastically simplified based on the new result AST from dry-logic (solnic)
29
+ * `HintCompiler` is gone as hints are now part of the result AST (solnic)
30
+ * `maybe` macro creates an implication instead of a disjunction (`not(none?).then(*your-predicates)`) (solnic)
31
+
32
+ [Compare v0.9.5...v0.10.0](https://github.com/dryrb/dry-validation/compare/v0.9.5...v0.10.0)
33
+
34
+ # v0.9.5 2016-08-16
2
35
 
3
36
  ### Fixed
4
37
 
data/Gemfile CHANGED
@@ -5,6 +5,8 @@ gemspec
5
5
  group :test do
6
6
  gem 'i18n', require: false
7
7
  gem 'codeclimate-test-reporter', platform: :rbx
8
+ gem 'dry-monads', require: false
9
+ gem 'dry-struct', github: 'dry-rb/dry-struct', branch: 'master', require: false
8
10
  end
9
11
 
10
12
  group :tools do
data/Rakefile CHANGED
@@ -6,7 +6,16 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
6
6
  require 'rspec/core'
7
7
  require 'rspec/core/rake_task'
8
8
 
9
- task default: :spec
10
-
11
9
  desc 'Run all specs in spec directory'
12
- RSpec::Core::RakeTask.new(:spec)
10
+ task :run_specs do
11
+ require 'rspec/core'
12
+
13
+ RSpec::Core::Runner.run(['spec/integration', 'spec/unit'])
14
+ RSpec.clear_examples
15
+ Dir[SPEC_ROOT.join('shared/**/*.rb')].each(&method(:load))
16
+
17
+ Dry::Validation.load_extensions(:monads, :struct)
18
+ RSpec::Core::Runner.run(['spec'])
19
+ end
20
+
21
+ task default: :run_specs
data/config/errors.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  en:
2
2
  errors:
3
+ or: "or"
3
4
  array?: "must be an array"
4
5
 
5
6
  empty?: "must be empty"
@@ -19,8 +19,9 @@ Gem::Specification.new do |spec|
19
19
  spec.add_runtime_dependency 'dry-configurable', '~> 0.1', '>= 0.1.3'
20
20
  spec.add_runtime_dependency 'dry-container', '~> 0.2', '>= 0.2.8'
21
21
  spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
22
- spec.add_runtime_dependency 'dry-logic', '~> 0.3', '>= 0.3.0'
23
- spec.add_runtime_dependency 'dry-types', '~> 0.8', '>= 0.8.1'
22
+ spec.add_runtime_dependency 'dry-logic', '~> 0.4', '>= 0.4.0'
23
+ spec.add_runtime_dependency 'dry-types', '~> 0.9', '>= 0.9.0'
24
+ spec.add_runtime_dependency 'dry-core', '~> 0.1'
24
25
 
25
26
  spec.add_development_dependency 'bundler'
26
27
  spec.add_development_dependency 'rake'
@@ -52,40 +52,16 @@ module Dry
52
52
  end
53
53
 
54
54
  class BuildErrors
55
- attr_reader :path
56
-
57
- def self.[](path)
58
- path.nil? || path.empty? ? Flat.new : Nested.new(path)
59
- end
60
-
61
- class Flat < BuildErrors
62
- def error_path(name)
63
- name
64
- end
65
- end
66
-
67
- class Nested < BuildErrors
68
- def initialize(path)
69
- @path = path
70
- end
71
-
72
- def error_path(name)
73
- [*path, name]
74
- end
75
- end
76
-
77
55
  def call(result)
78
- result
79
- .select { |_, r| r.failure? }
80
- .map { |name, r| Error.new(error_path(name), r) }
56
+ result.values.select(&:failure?)
81
57
  end
82
58
  end
83
59
 
84
60
  class Executor
85
61
  attr_reader :steps, :final
86
62
 
87
- def self.new(path, &block)
88
- super(BuildErrors[path]).tap { |executor| yield(executor.steps) }.freeze
63
+ def self.new(&block)
64
+ super(BuildErrors.new).tap { |executor| yield(executor.steps) }.freeze
89
65
  end
90
66
 
91
67
  def initialize(final)
@@ -0,0 +1,17 @@
1
+ require 'dry/monads/either'
2
+
3
+ module Dry
4
+ module Validation
5
+ class Result
6
+ include Dry::Monads::Either::Mixin
7
+
8
+ def to_either(options = EMPTY_HASH)
9
+ if success?
10
+ Right(output)
11
+ else
12
+ Left(messages(options))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ require 'dry-struct'
2
+
3
+ module Dry
4
+ module Validation
5
+ class Schema
6
+ module StructClassBuilder
7
+ def create_class(target, other = nil)
8
+ if other.is_a?(Class) && other < Dry::Struct
9
+ super do
10
+ other.schema.each { |attr, type| required(attr).filled(type) }
11
+ end
12
+ else
13
+ super
14
+ end
15
+ end
16
+ end
17
+
18
+ module StructNode
19
+ def node(input, *)
20
+ if input.is_a?(::Class) && input < ::Dry::Struct
21
+ [type, [name, [:schema, Schema.create_class(self, input)]]]
22
+ else
23
+ super
24
+ end
25
+ end
26
+ end
27
+
28
+ singleton_class.prepend(StructClassBuilder)
29
+ Value.prepend(StructNode)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ Dry::Validation.register_extension(:monads) do
2
+ require 'dry/validation/extensions/monads'
3
+ end
4
+
5
+ Dry::Validation.register_extension(:struct) do
6
+ require 'dry/validation/extensions/struct'
7
+ end
@@ -24,6 +24,11 @@ module Dry
24
24
  send(:"visit_#{node[0]}", node[1], *args)
25
25
  end
26
26
 
27
+ def visit_rule(node, *args)
28
+ _, rule = node
29
+ visit(rule, *args)
30
+ end
31
+
27
32
  def visit_type(type, *args)
28
33
  if type.is_a?(Types::Constructor)
29
34
  [:constructor, [type.primitive, type.fn]]
@@ -63,9 +68,20 @@ module Dry
63
68
  end
64
69
  end
65
70
 
66
- def visit_implication(node)
67
- key, types = node
68
- [:key, [visit(key), visit(types, false)]]
71
+ def visit_implication(node, *args)
72
+ left, right = node
73
+
74
+ key = visit(left)
75
+
76
+ if key.is_a?(Symbol)
77
+ [:key, [key, visit(right, false)]]
78
+ else
79
+ [:sum, [key, visit(right, false)]]
80
+ end
81
+ end
82
+
83
+ def visit_not(node, *args)
84
+ visit(node, *args)
69
85
  end
70
86
 
71
87
  def visit_key(node, *args)
@@ -1,32 +1,55 @@
1
- require 'dry/validation/constants'
1
+ require 'dry/core/constants'
2
2
 
3
3
  module Dry
4
4
  module Validation
5
5
  class Message
6
+ include Core::Constants
6
7
  include Dry::Equalizer(:predicate, :path, :text, :options)
7
8
 
8
- Index = Class.new {
9
- def inspect
10
- "index"
9
+ attr_reader :predicate, :path, :text, :rule, :args, :options
10
+
11
+ class Or
12
+ attr_reader :left
13
+
14
+ attr_reader :right
15
+
16
+ attr_reader :path
17
+
18
+ attr_reader :messages
19
+
20
+ def initialize(left, right, messages)
21
+ @left = left
22
+ @right = right
23
+ @messages = messages
24
+ @path = left.path
11
25
  end
12
- alias_method :to_s, :inspect
13
- }.new
14
26
 
15
- attr_reader :predicate, :path, :text, :rule, :args, :options
27
+ def hint?
28
+ false
29
+ end
30
+
31
+ def root?
32
+ path.empty?
33
+ end
16
34
 
17
- class Each < Message
18
- def index_path
19
- @index_path ||= [*path[0..path.size-2], Index]
35
+ def to_s
36
+ [left, right].uniq.join(" #{messages[:or]} ")
20
37
  end
38
+ end
21
39
 
22
- def each?
23
- true
40
+ class Check < Message
41
+ def initialize(*args)
42
+ super
43
+ @path = [rule] unless rule.to_s.end_with?('?') || path.include?(rule)
24
44
  end
25
45
  end
26
46
 
27
47
  def self.[](predicate, path, text, options)
28
- klass = options[:each] ? Message::Each : Message
29
- klass.new(predicate, path, text, options)
48
+ if options[:check]
49
+ Message::Check.new(predicate, path, text, options)
50
+ else
51
+ Message.new(predicate, path, text, options)
52
+ end
30
53
  end
31
54
 
32
55
  def initialize(predicate, path, text, options)
@@ -35,22 +58,19 @@ module Dry
35
58
  @text = text
36
59
  @options = options
37
60
  @rule = options[:rule]
38
- @each = options[:each] || false
39
61
  @args = options[:args] || EMPTY_ARRAY
40
- end
41
62
 
42
- alias_method :index_path, :path
63
+ if predicate == :key?
64
+ @path << rule
65
+ end
66
+ end
43
67
 
44
68
  def to_s
45
69
  text
46
70
  end
47
71
 
48
72
  def signature
49
- @signature ||= [predicate, args, index_path].hash
50
- end
51
-
52
- def each?
53
- @each
73
+ @signature ||= [predicate, args, path].hash
54
74
  end
55
75
 
56
76
  def hint?
@@ -68,14 +88,7 @@ module Dry
68
88
 
69
89
  class Hint < Message
70
90
  def self.[](predicate, path, text, options)
71
- klass = options[:each] ? Hint::Each : Hint
72
- klass.new(predicate, path, text, options)
73
- end
74
-
75
- class Each < Hint
76
- def index_path
77
- @index_path ||= [*path, Index]
78
- end
91
+ Hint.new(predicate, path, text, options)
79
92
  end
80
93
 
81
94
  def hint?
@@ -0,0 +1,39 @@
1
+ require 'dry/core/constants'
2
+
3
+ module Dry
4
+ module Validation
5
+ class MessageCompiler
6
+ class VisitorOpts < Hash
7
+ def self.new
8
+ opts = super
9
+ opts[:path] = Core::Constants::EMPTY_ARRAY
10
+ opts[:rule] = nil
11
+ opts[:message_type] = :failure
12
+ opts
13
+ end
14
+
15
+ def path?
16
+ ! path.empty?
17
+ end
18
+
19
+ def path
20
+ self[:path]
21
+ end
22
+
23
+ def rule
24
+ self[:rule]
25
+ end
26
+
27
+ def with_rule(new_rule, **other)
28
+ opts = dup
29
+ opts[:rule] = new_rule unless opts.rule
30
+ opts.(other)
31
+ end
32
+
33
+ def call(other)
34
+ merge(other.update(path: [*path, *other[:path]]))
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,85 +1,144 @@
1
- require 'dry/validation/constants'
1
+ require 'dry/core/constants'
2
2
  require 'dry/validation/message'
3
3
  require 'dry/validation/message_set'
4
+ require 'dry/validation/message_compiler/visitor_opts'
4
5
 
5
6
  module Dry
6
7
  module Validation
7
8
  class MessageCompiler
9
+ include Core::Constants
10
+
8
11
  attr_reader :messages, :options, :locale, :default_lookup_options
9
12
 
13
+ EMPTY_OPTS = VisitorOpts.new
14
+ LIST_SEPARATOR = ', '.freeze
15
+
10
16
  def initialize(messages, options = {})
11
17
  @messages = messages
12
18
  @options = options
13
19
  @full = @options.fetch(:full, false)
20
+ @hints = @options.fetch(:hints, true)
14
21
  @locale = @options.fetch(:locale, messages.default_locale)
15
- @default_lookup_options = { message_type: message_type, locale: locale }
16
- end
17
-
18
- def call(ast)
19
- MessageSet[ast.map { |node| visit(node) }]
22
+ @default_lookup_options = { locale: locale }
20
23
  end
21
24
 
22
25
  def full?
23
26
  @full
24
27
  end
25
28
 
29
+ def hints?
30
+ @hints
31
+ end
32
+
26
33
  def with(new_options)
27
34
  return self if new_options.empty?
28
35
  self.class.new(messages, options.merge(new_options))
29
36
  end
30
37
 
38
+ def call(ast)
39
+ MessageSet[ast.map { |node| visit(node) }, failures: options.fetch(:failures, true)]
40
+ end
41
+
31
42
  def visit(node, *args)
32
43
  __send__(:"visit_#{node[0]}", node[1], *args)
33
44
  end
34
45
 
35
- def visit_predicate(node, base_opts = EMPTY_HASH)
36
- predicate, args = node
46
+ def visit_failure(node, opts = EMPTY_OPTS)
47
+ rule, other = node
48
+ visit(other, opts.(rule: rule))
49
+ end
50
+
51
+ def visit_hint(node, opts = EMPTY_OPTS)
52
+ if hints?
53
+ visit(node, opts.(message_type: :hint))
54
+ end
55
+ end
37
56
 
38
- *arg_vals, _ = args.map(&:last)
57
+ def visit_each(node, opts = EMPTY_OPTS)
58
+ # TODO: we can still generate a hint for elements here!
59
+ []
60
+ end
39
61
 
40
- tokens = message_tokens(args)
62
+ def visit_not(node, opts = EMPTY_OPTS)
63
+ visit(node, opts.(not: true))
64
+ end
65
+
66
+ def visit_check(node, opts = EMPTY_OPTS)
67
+ keys, other = node
68
+ visit(other, opts.(path: keys.last, check: true))
69
+ end
41
70
 
42
- if base_opts[:message] == false
43
- return [predicate, arg_vals, tokens]
71
+ def visit_rule(node, opts = EMPTY_OPTS)
72
+ name, other = node
73
+ visit(other, opts.(rule: name))
74
+ end
75
+
76
+ def visit_schema(node, opts = EMPTY_OPTS)
77
+ node.rule_ast.map { |rule| visit(rule, opts) }
78
+ end
79
+
80
+ def visit_and(node, opts = EMPTY_OPTS)
81
+ left, right = node.map { |n| visit(n, opts) }
82
+
83
+ if right
84
+ [left, right]
85
+ else
86
+ left
44
87
  end
88
+ end
89
+
90
+ def visit_or(node, opts = EMPTY_OPTS)
91
+ left, right = node.map { |n| visit(n, opts) }
45
92
 
46
- options = base_opts.update(lookup_options(base_opts, arg_vals))
93
+ if [left, right].flatten.map(&:path).uniq.size == 1
94
+ Message::Or.new(left, right, -> k { messages[k, default_lookup_options] })
95
+ elsif right.is_a?(Array)
96
+ right
97
+ else
98
+ [left, right]
99
+ end
100
+ end
101
+
102
+ def visit_predicate(node, base_opts = EMPTY_OPTS)
103
+ predicate, args = node
104
+
105
+ *arg_vals, val = args.map(&:last)
106
+ tokens = message_tokens(args)
107
+
108
+ input = val != Undefined ? val : nil
109
+
110
+ options = base_opts.update(lookup_options(arg_vals: arg_vals, input: input))
47
111
  msg_opts = options.update(tokens)
48
112
 
49
- name = msg_opts[:name]
50
- rule = msg_opts[:rule] || name
113
+ rule = msg_opts[:rule]
114
+ path = msg_opts[:path]
51
115
 
52
- template = messages[predicate, msg_opts]
116
+ template = messages[rule] || messages[predicate, msg_opts]
53
117
 
54
118
  unless template
55
119
  raise MissingMessageError, "message for #{predicate} was not found"
56
120
  end
57
121
 
58
122
  text = message_text(rule, template, tokens, options)
59
- path = message_path(msg_opts, name)
123
+
124
+ message_class = options[:message_type] == :hint ? Hint : Message
60
125
 
61
126
  message_class[
62
127
  predicate, path, text,
63
- args: arg_vals, rule: rule, each: base_opts[:each] == true
128
+ args: arg_vals,
129
+ input: input,
130
+ rule: rule,
131
+ check: base_opts[:check]
64
132
  ]
65
133
  end
66
134
 
67
- def visit_key(node, opts = EMPTY_HASH)
68
- name, predicate = node
69
- visit(predicate, opts.merge(name: name))
135
+ def visit_key(node, opts = EMPTY_OPTS)
136
+ name, other = node
137
+ visit(other, opts.(path: name))
70
138
  end
71
139
 
72
- def visit_val(node, opts = EMPTY_HASH)
73
- visit(node, opts)
74
- end
75
-
76
- def visit_set(node, opts = EMPTY_HASH)
77
- node.map { |input| visit(input, opts) }
78
- end
79
-
80
- def visit_el(node, opts = EMPTY_HASH)
81
- idx, el = node
82
- visit(el, opts.merge(path: opts[:path] + [idx]))
140
+ def visit_set(node, opts = EMPTY_OPTS)
141
+ node.map { |el| visit(el, opts) }
83
142
  end
84
143
 
85
144
  def visit_implication(node, *args)
@@ -87,14 +146,15 @@ module Dry
87
146
  visit(right, *args)
88
147
  end
89
148
 
90
- def visit_xor(node, *args)
91
- _, right = node
92
- visit(right, *args)
149
+ def visit_xor(node, opts = EMPTY_OPTS)
150
+ left, right = node
151
+ [visit(left, opts), visit(right, opts)].uniq
93
152
  end
94
153
 
95
- def lookup_options(_opts, arg_vals = [])
154
+ def lookup_options(arg_vals: [], input: nil)
96
155
  default_lookup_options.merge(
97
- arg_type: arg_vals.size == 1 && arg_vals[0].class
156
+ arg_type: arg_vals.size == 1 && arg_vals[0].class,
157
+ val_type: input.class
98
158
  )
99
159
  end
100
160
 
@@ -109,25 +169,11 @@ module Dry
109
169
  end
110
170
  end
111
171
 
112
- def message_path(opts, name)
113
- if name.is_a?(Array)
114
- name
115
- else
116
- path = opts[:path] || Array(name)
117
-
118
- if name && path.last != name
119
- path += [name]
120
- end
121
-
122
- path
123
- end
124
- end
125
-
126
172
  def message_tokens(args)
127
173
  args.each_with_object({}) { |arg, hash|
128
174
  case arg[1]
129
175
  when Array
130
- hash[arg[0]] = arg[1].join(', ')
176
+ hash[arg[0]] = arg[1].join(LIST_SEPARATOR)
131
177
  when Range
132
178
  hash["#{arg[0]}_left".to_sym] = arg[1].first
133
179
  hash["#{arg[0]}_right".to_sym] = arg[1].last