dry-validation 0.9.5 → 0.10.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 (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