dry-validation 0.12.0 → 1.6.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 (204) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +696 -239
  3. data/LICENSE +1 -1
  4. data/README.md +15 -12
  5. data/config/errors.yml +3 -88
  6. data/dry-validation.gemspec +31 -17
  7. data/lib/dry-validation.rb +3 -1
  8. data/lib/dry/validation.rb +45 -28
  9. data/lib/dry/validation/config.rb +24 -0
  10. data/lib/dry/validation/constants.rb +43 -0
  11. data/lib/dry/validation/contract.rb +171 -0
  12. data/lib/dry/validation/contract/class_interface.rb +230 -0
  13. data/lib/dry/validation/evaluator.rb +224 -0
  14. data/lib/dry/validation/extensions/hints.rb +67 -0
  15. data/lib/dry/validation/extensions/monads.rb +24 -8
  16. data/lib/dry/validation/extensions/predicates_as_macros.rb +75 -0
  17. data/lib/dry/validation/failures.rb +70 -0
  18. data/lib/dry/validation/function.rb +44 -0
  19. data/lib/dry/validation/macro.rb +38 -0
  20. data/lib/dry/validation/macros.rb +104 -0
  21. data/lib/dry/validation/message.rb +80 -80
  22. data/lib/dry/validation/message_set.rb +80 -105
  23. data/lib/dry/validation/messages/resolver.rb +131 -0
  24. data/lib/dry/validation/result.rb +183 -41
  25. data/lib/dry/validation/rule.rb +135 -0
  26. data/lib/dry/validation/schema_ext.rb +19 -0
  27. data/lib/dry/validation/values.rb +104 -0
  28. data/lib/dry/validation/version.rb +3 -1
  29. metadata +47 -336
  30. data/.codeclimate.yml +0 -17
  31. data/.gitignore +0 -9
  32. data/.rspec +0 -3
  33. data/.travis.yml +0 -26
  34. data/CONTRIBUTING.md +0 -31
  35. data/Gemfile +0 -33
  36. data/Rakefile +0 -22
  37. data/benchmarks/benchmark_form_invalid.rb +0 -64
  38. data/benchmarks/benchmark_form_valid.rb +0 -64
  39. data/benchmarks/benchmark_schema_invalid_huge.rb +0 -52
  40. data/benchmarks/profile_schema_call_invalid.rb +0 -20
  41. data/benchmarks/profile_schema_call_valid.rb +0 -20
  42. data/benchmarks/profile_schema_definition.rb +0 -14
  43. data/benchmarks/profile_schema_huge_invalid.rb +0 -30
  44. data/benchmarks/profile_schema_messages_invalid.rb +0 -20
  45. data/benchmarks/suite.rb +0 -5
  46. data/examples/basic.rb +0 -15
  47. data/examples/each.rb +0 -14
  48. data/examples/json.rb +0 -12
  49. data/examples/multiple.rb +0 -27
  50. data/examples/nested.rb +0 -22
  51. data/examples/params.rb +0 -11
  52. data/lib/dry/validation/compat/form.rb +0 -67
  53. data/lib/dry/validation/deprecations.rb +0 -24
  54. data/lib/dry/validation/executor.rb +0 -91
  55. data/lib/dry/validation/extensions.rb +0 -7
  56. data/lib/dry/validation/extensions/struct.rb +0 -32
  57. data/lib/dry/validation/input_processor_compiler.rb +0 -135
  58. data/lib/dry/validation/input_processor_compiler/json.rb +0 -45
  59. data/lib/dry/validation/input_processor_compiler/params.rb +0 -49
  60. data/lib/dry/validation/input_processor_compiler/sanitizer.rb +0 -47
  61. data/lib/dry/validation/message_compiler.rb +0 -192
  62. data/lib/dry/validation/message_compiler/visitor_opts.rb +0 -37
  63. data/lib/dry/validation/messages.rb +0 -14
  64. data/lib/dry/validation/messages/abstract.rb +0 -112
  65. data/lib/dry/validation/messages/i18n.rb +0 -35
  66. data/lib/dry/validation/messages/namespaced.rb +0 -32
  67. data/lib/dry/validation/messages/yaml.rb +0 -54
  68. data/lib/dry/validation/predicate_registry.rb +0 -115
  69. data/lib/dry/validation/schema.rb +0 -126
  70. data/lib/dry/validation/schema/check.rb +0 -37
  71. data/lib/dry/validation/schema/class_interface.rb +0 -189
  72. data/lib/dry/validation/schema/deprecated.rb +0 -30
  73. data/lib/dry/validation/schema/dsl.rb +0 -118
  74. data/lib/dry/validation/schema/form.rb +0 -9
  75. data/lib/dry/validation/schema/json.rb +0 -21
  76. data/lib/dry/validation/schema/key.rb +0 -71
  77. data/lib/dry/validation/schema/params.rb +0 -22
  78. data/lib/dry/validation/schema/rule.rb +0 -202
  79. data/lib/dry/validation/schema/value.rb +0 -211
  80. data/lib/dry/validation/schema_compiler.rb +0 -81
  81. data/lib/dry/validation/type_specs.rb +0 -70
  82. data/spec/extensions/monads/result_spec.rb +0 -40
  83. data/spec/extensions/struct/schema_spec.rb +0 -32
  84. data/spec/fixtures/locales/en.yml +0 -6
  85. data/spec/fixtures/locales/pl.yml +0 -18
  86. data/spec/integration/custom_error_messages_spec.rb +0 -48
  87. data/spec/integration/custom_predicates_spec.rb +0 -228
  88. data/spec/integration/hints_spec.rb +0 -170
  89. data/spec/integration/injecting_rules_spec.rb +0 -30
  90. data/spec/integration/json/defining_base_schema_spec.rb +0 -41
  91. data/spec/integration/localized_error_messages_spec.rb +0 -72
  92. data/spec/integration/message_compiler_spec.rb +0 -405
  93. data/spec/integration/messages/i18n_spec.rb +0 -94
  94. data/spec/integration/optional_keys_spec.rb +0 -28
  95. data/spec/integration/params/predicates/array_spec.rb +0 -287
  96. data/spec/integration/params/predicates/empty_spec.rb +0 -263
  97. data/spec/integration/params/predicates/eql_spec.rb +0 -327
  98. data/spec/integration/params/predicates/even_spec.rb +0 -455
  99. data/spec/integration/params/predicates/excluded_from_spec.rb +0 -455
  100. data/spec/integration/params/predicates/excludes_spec.rb +0 -391
  101. data/spec/integration/params/predicates/false_spec.rb +0 -455
  102. data/spec/integration/params/predicates/filled_spec.rb +0 -467
  103. data/spec/integration/params/predicates/format_spec.rb +0 -454
  104. data/spec/integration/params/predicates/gt_spec.rb +0 -519
  105. data/spec/integration/params/predicates/gteq_spec.rb +0 -519
  106. data/spec/integration/params/predicates/included_in_spec.rb +0 -455
  107. data/spec/integration/params/predicates/includes_spec.rb +0 -391
  108. data/spec/integration/params/predicates/key_spec.rb +0 -67
  109. data/spec/integration/params/predicates/lt_spec.rb +0 -519
  110. data/spec/integration/params/predicates/lteq_spec.rb +0 -519
  111. data/spec/integration/params/predicates/max_size_spec.rb +0 -391
  112. data/spec/integration/params/predicates/min_size_spec.rb +0 -391
  113. data/spec/integration/params/predicates/none_spec.rb +0 -265
  114. data/spec/integration/params/predicates/not_eql_spec.rb +0 -327
  115. data/spec/integration/params/predicates/odd_spec.rb +0 -455
  116. data/spec/integration/params/predicates/size/fixed_spec.rb +0 -393
  117. data/spec/integration/params/predicates/size/range_spec.rb +0 -396
  118. data/spec/integration/params/predicates/true_spec.rb +0 -455
  119. data/spec/integration/params/predicates/type_spec.rb +0 -391
  120. data/spec/integration/result_spec.rb +0 -81
  121. data/spec/integration/schema/array_schema_spec.rb +0 -59
  122. data/spec/integration/schema/check_rules_spec.rb +0 -119
  123. data/spec/integration/schema/check_with_nested_el_spec.rb +0 -37
  124. data/spec/integration/schema/check_with_nth_el_spec.rb +0 -25
  125. data/spec/integration/schema/default_settings_spec.rb +0 -11
  126. data/spec/integration/schema/defining_base_schema_spec.rb +0 -41
  127. data/spec/integration/schema/dynamic_predicate_args_spec.rb +0 -43
  128. data/spec/integration/schema/each_with_set_spec.rb +0 -70
  129. data/spec/integration/schema/extending_dsl_spec.rb +0 -27
  130. data/spec/integration/schema/form_spec.rb +0 -236
  131. data/spec/integration/schema/hash_schema_spec.rb +0 -47
  132. data/spec/integration/schema/inheriting_schema_spec.rb +0 -31
  133. data/spec/integration/schema/input_processor_spec.rb +0 -46
  134. data/spec/integration/schema/json/explicit_types_spec.rb +0 -157
  135. data/spec/integration/schema/json_spec.rb +0 -163
  136. data/spec/integration/schema/macros/confirmation_spec.rb +0 -35
  137. data/spec/integration/schema/macros/each_spec.rb +0 -268
  138. data/spec/integration/schema/macros/filled_spec.rb +0 -87
  139. data/spec/integration/schema/macros/input_spec.rb +0 -139
  140. data/spec/integration/schema/macros/maybe_spec.rb +0 -99
  141. data/spec/integration/schema/macros/rule_spec.rb +0 -75
  142. data/spec/integration/schema/macros/value_spec.rb +0 -119
  143. data/spec/integration/schema/macros/when_spec.rb +0 -62
  144. data/spec/integration/schema/nested_schemas_spec.rb +0 -236
  145. data/spec/integration/schema/nested_values_spec.rb +0 -46
  146. data/spec/integration/schema/not_spec.rb +0 -34
  147. data/spec/integration/schema/numbers_spec.rb +0 -19
  148. data/spec/integration/schema/option_with_default_spec.rb +0 -64
  149. data/spec/integration/schema/or_spec.rb +0 -87
  150. data/spec/integration/schema/params/defining_base_schema_spec.rb +0 -41
  151. data/spec/integration/schema/params/explicit_types_spec.rb +0 -195
  152. data/spec/integration/schema/params_spec.rb +0 -234
  153. data/spec/integration/schema/predicate_verification_spec.rb +0 -9
  154. data/spec/integration/schema/predicates/array_spec.rb +0 -295
  155. data/spec/integration/schema/predicates/custom_spec.rb +0 -103
  156. data/spec/integration/schema/predicates/empty_spec.rb +0 -263
  157. data/spec/integration/schema/predicates/eql_spec.rb +0 -327
  158. data/spec/integration/schema/predicates/even_spec.rb +0 -455
  159. data/spec/integration/schema/predicates/excluded_from/array_spec.rb +0 -459
  160. data/spec/integration/schema/predicates/excluded_from/range_spec.rb +0 -459
  161. data/spec/integration/schema/predicates/excludes_spec.rb +0 -391
  162. data/spec/integration/schema/predicates/filled_spec.rb +0 -467
  163. data/spec/integration/schema/predicates/format_spec.rb +0 -455
  164. data/spec/integration/schema/predicates/gt_spec.rb +0 -519
  165. data/spec/integration/schema/predicates/gteq_spec.rb +0 -519
  166. data/spec/integration/schema/predicates/hash_spec.rb +0 -69
  167. data/spec/integration/schema/predicates/included_in/array_spec.rb +0 -459
  168. data/spec/integration/schema/predicates/included_in/range_spec.rb +0 -459
  169. data/spec/integration/schema/predicates/includes_spec.rb +0 -391
  170. data/spec/integration/schema/predicates/key_spec.rb +0 -88
  171. data/spec/integration/schema/predicates/lt_spec.rb +0 -520
  172. data/spec/integration/schema/predicates/lteq_spec.rb +0 -519
  173. data/spec/integration/schema/predicates/max_size_spec.rb +0 -391
  174. data/spec/integration/schema/predicates/min_size_spec.rb +0 -391
  175. data/spec/integration/schema/predicates/none_spec.rb +0 -265
  176. data/spec/integration/schema/predicates/not_eql_spec.rb +0 -391
  177. data/spec/integration/schema/predicates/odd_spec.rb +0 -455
  178. data/spec/integration/schema/predicates/size/fixed_spec.rb +0 -398
  179. data/spec/integration/schema/predicates/size/range_spec.rb +0 -395
  180. data/spec/integration/schema/predicates/type_spec.rb +0 -413
  181. data/spec/integration/schema/reusing_schema_spec.rb +0 -33
  182. data/spec/integration/schema/using_types_spec.rb +0 -135
  183. data/spec/integration/schema/validate_spec.rb +0 -120
  184. data/spec/integration/schema/xor_spec.rb +0 -35
  185. data/spec/integration/schema_builders_spec.rb +0 -17
  186. data/spec/integration/schema_spec.rb +0 -173
  187. data/spec/shared/message_compiler.rb +0 -11
  188. data/spec/shared/predicate_helper.rb +0 -15
  189. data/spec/shared/rule_compiler.rb +0 -8
  190. data/spec/spec_helper.rb +0 -58
  191. data/spec/support/define_struct.rb +0 -25
  192. data/spec/support/matchers.rb +0 -38
  193. data/spec/support/mutant.rb +0 -9
  194. data/spec/support/predicates_integration.rb +0 -7
  195. data/spec/unit/input_processor_compiler/json_spec.rb +0 -283
  196. data/spec/unit/input_processor_compiler/params_spec.rb +0 -328
  197. data/spec/unit/message_compiler/visit_failure_spec.rb +0 -38
  198. data/spec/unit/message_compiler/visit_spec.rb +0 -16
  199. data/spec/unit/message_compiler_spec.rb +0 -7
  200. data/spec/unit/predicate_registry_spec.rb +0 -34
  201. data/spec/unit/schema/key_spec.rb +0 -38
  202. data/spec/unit/schema/rule_spec.rb +0 -42
  203. data/spec/unit/schema/value_spec.rb +0 -131
  204. data/spec/unit/schema_spec.rb +0 -35
@@ -1,98 +1,98 @@
1
- require 'dry/equalizer'
1
+ # frozen_string_literal: true
2
2
 
3
- module Dry
4
- module Validation
5
- class Message
6
- include Dry::Equalizer(:predicate, :path, :text, :options)
7
-
8
- attr_reader :predicate, :path, :text, :rule, :args, :options
9
-
10
- class Or
11
- attr_reader :left
12
-
13
- attr_reader :right
3
+ require "dry/equalizer"
14
4
 
15
- attr_reader :path
5
+ require "dry/schema/constants"
6
+ require "dry/schema/message"
16
7
 
17
- attr_reader :messages
18
-
19
- def initialize(left, right, messages)
20
- @left = left
21
- @right = right
22
- @messages = messages
23
- @path = left.path
24
- end
25
-
26
- def hint?
27
- false
28
- end
29
-
30
- def root?
31
- path.empty?
32
- end
33
-
34
- def to_s
35
- [left, right].uniq.join(" #{messages[:or]} ")
8
+ module Dry
9
+ module Validation
10
+ # Message message
11
+ #
12
+ # @api public
13
+ class Message < Schema::Message
14
+ include Dry::Equalizer(:text, :path, :meta)
15
+
16
+ # The error message text
17
+ #
18
+ # @return [String] text
19
+ #
20
+ # @api public
21
+ attr_reader :text
22
+
23
+ # The path to the value with the error
24
+ #
25
+ # @return [Array<Symbol, Integer>]
26
+ #
27
+ # @api public
28
+ attr_reader :path
29
+
30
+ # Optional hash with meta-data
31
+ #
32
+ # @return [Hash]
33
+ #
34
+ # @api public
35
+ attr_reader :meta
36
+
37
+ # A localized message type
38
+ #
39
+ # Localized messsages can be translated to other languages at run-time
40
+ #
41
+ # @api public
42
+ class Localized < Message
43
+ # Evaluate message text using provided locale
44
+ #
45
+ # @example
46
+ # result.errors[:email].evaluate(locale: :en, full: true)
47
+ # # "email is invalid"
48
+ #
49
+ # @param [Hash] opts
50
+ # @option opts [Symbol] :locale Which locale to use
51
+ # @option opts [Boolean] :full Whether message text should include the key name
52
+ #
53
+ # @api public
54
+ def evaluate(**opts)
55
+ evaluated_text, rest = text.(**opts)
56
+ Message.new(evaluated_text, path: path, meta: rest.merge(meta))
36
57
  end
37
58
  end
38
59
 
39
- class Check < Message
40
- def initialize(*args)
41
- super
42
- @path = [rule] unless rule.to_s.end_with?('?') || path.include?(rule)
43
- end
60
+ # Build an error
61
+ #
62
+ # @return [Message, Message::Localized]
63
+ #
64
+ # @api private
65
+ def self.[](text, path, meta)
66
+ klass = text.respond_to?(:call) ? Localized : Message
67
+ klass.new(text, path: path, meta: meta)
44
68
  end
45
69
 
46
- def self.[](predicate, path, text, options)
47
- if options[:check]
48
- Message::Check.new(predicate, path, text, options)
49
- else
50
- Message.new(predicate, path, text, options)
51
- end
52
- end
53
-
54
- def initialize(predicate, path, text, options)
55
- @predicate = predicate
56
- @path = path
70
+ # Initialize a new error object
71
+ #
72
+ # @api private
73
+ def initialize(text, path:, meta: EMPTY_HASH)
57
74
  @text = text
58
- @options = options
59
- @rule = options[:rule]
60
- @args = options[:args] || EMPTY_ARRAY
75
+ @path = Array(path)
76
+ @meta = meta
77
+ end
61
78
 
62
- if predicate == :key?
63
- @path << rule
64
- end
79
+ # Check if this is a base error not associated with any key
80
+ #
81
+ # @return [Boolean]
82
+ #
83
+ # @api public
84
+ def base?
85
+ @base ||= path.compact.empty?
65
86
  end
66
87
 
88
+ # Dump error to a string
89
+ #
90
+ # @return [String]
91
+ #
92
+ # @api public
67
93
  def to_s
68
94
  text
69
95
  end
70
-
71
- def signature
72
- @signature ||= [predicate, args, path].hash
73
- end
74
-
75
- def hint?
76
- false
77
- end
78
-
79
- def root?
80
- path.empty?
81
- end
82
-
83
- def eql?(other)
84
- other.is_a?(String) ? text == other : super
85
- end
86
- end
87
-
88
- class Hint < Message
89
- def self.[](predicate, path, text, options)
90
- Hint.new(predicate, path, text, options)
91
- end
92
-
93
- def hint?
94
- true
95
- end
96
96
  end
97
97
  end
98
98
  end
@@ -1,121 +1,96 @@
1
- module Dry
2
- module Validation
3
- class MessageSet
4
- include Enumerable
1
+ # frozen_string_literal: true
5
2
 
6
- HINT_EXCLUSION = %i(
7
- key? filled? none? bool?
8
- str? int? float? decimal?
9
- date? date_time? time? hash?
10
- array? format?
11
- ).freeze
3
+ require "dry/schema/message_set"
12
4
 
13
- attr_reader :messages, :failures, :hints, :paths, :placeholders, :options
14
-
15
- def self.[](messages, options = EMPTY_HASH)
16
- new(messages.flatten, options)
17
- end
5
+ require "dry/validation/constants"
6
+ require "dry/validation/message"
18
7
 
8
+ module Dry
9
+ module Validation
10
+ # MessageSet is a specialized message set for handling validation messages
11
+ #
12
+ # @api public
13
+ class MessageSet < Schema::MessageSet
14
+ # Return the source set of messages used to produce final evaluated messages
15
+ #
16
+ # @return [Array<Message, Message::Localized, Schema::Message>]
17
+ #
18
+ # @api private
19
+ attr_reader :source_messages
20
+
21
+ # Configured locale
22
+ #
23
+ # @return [Symbol]
24
+ #
25
+ # @api public
26
+ attr_reader :locale
27
+
28
+ # @api private
19
29
  def initialize(messages, options = EMPTY_HASH)
20
- @messages = messages
21
- @hints = messages.select(&:hint?)
22
- @failures = messages - hints
23
- @paths = failures.map(&:path).uniq
24
- @options = options
25
-
26
- initialize_hints!
27
- initialize_placeholders!
28
- end
29
-
30
- def dump
31
- root? ? to_a : to_h
32
- end
33
-
34
- def failures?
35
- options[:failures].equal?(true)
36
- end
37
-
38
- def empty?
39
- messages.empty?
40
- end
41
-
42
- def root?
43
- !empty? && failures.all?(&:root?)
44
- end
45
-
46
- def each(&block)
47
- return to_enum unless block
48
- messages.each(&block)
49
- end
50
-
51
- def to_h
52
- if root?
53
- { nil => failures.map(&:to_s) }
54
- else
55
- failures? ? messages_map : hints_map
56
- end
57
- end
58
- alias_method :to_hash, :to_h
59
-
60
- def to_a
61
- to_h.values.flatten
62
- end
63
-
64
- private
65
-
66
- def messages_map
67
- failures.group_by(&:path).reduce(placeholders) do |hash, (path, msgs)|
68
- node = path.reduce(hash) { |a, e| a[e] }
69
-
70
- msgs.each do |msg|
71
- node << msg
72
-
73
- msg_hints = hint_groups[msg.path]
74
- node.concat(msg_hints) if msg_hints
75
- end
76
-
77
- node.map!(&:to_s)
78
-
79
- hash
80
- end
30
+ @locale = options[:locale]
31
+ @source_messages = options.fetch(:source) { messages.dup }
32
+ super
81
33
  end
82
34
 
83
- def hints_map
84
- hints.group_by(&:path).reduce(placeholders) do |hash, (path, msgs)|
85
- node = path.reduce(hash) { |a, e| a[e] }
86
-
87
- msgs.each do |msg|
88
- node << msg
89
- end
90
-
91
- node.map!(&:to_s)
92
-
93
- hash
94
- end
35
+ # Return a new message set using updated options
36
+ #
37
+ # @return [MessageSet]
38
+ #
39
+ # @api private
40
+ def with(other, new_options = EMPTY_HASH)
41
+ return self if new_options.empty? && other.eql?(messages)
42
+
43
+ self.class.new(
44
+ other | select { |err| err.is_a?(Message) },
45
+ options.merge(source: source_messages, **new_options)
46
+ ).freeze
95
47
  end
96
48
 
97
- def hint_groups
98
- @hint_groups ||= hints.group_by(&:path)
49
+ # Add a new message
50
+ #
51
+ # This is used when result is being prepared
52
+ #
53
+ # @return [MessageSet]
54
+ #
55
+ # @api private
56
+ def add(message)
57
+ @empty = nil
58
+ source_messages << message
59
+ messages << message
60
+ self
99
61
  end
100
62
 
101
- def initialize_hints!
102
- hints.reject! { |hint| HINT_EXCLUSION.include?(hint.predicate) }
63
+ # Filter message set using provided predicates
64
+ #
65
+ # This method is open to any predicate because messages can be anything that
66
+ # implements Message API, thus they can implement whatever predicates you
67
+ # may need.
68
+ #
69
+ # @example get a list of base messages
70
+ # message_set = contract.(input).errors
71
+ # message_set.filter(:base?)
72
+ #
73
+ # @param [Array<Symbol>] predicates
74
+ #
75
+ # @return [MessageSet]
76
+ #
77
+ # @api public
78
+ def filter(*predicates)
79
+ messages = select { |msg|
80
+ predicates.all? { |predicate| msg.respond_to?(predicate) && msg.public_send(predicate) }
81
+ }
82
+ self.class.new(messages)
103
83
  end
104
84
 
105
- def initialize_placeholders!
106
- @placeholders = paths.reduce({}) do |hash, path|
107
- curr_idx = 0
108
- last_idx = path.size - 1
109
- node = hash
110
-
111
- while curr_idx <= last_idx do
112
- key = path[curr_idx]
113
- node = (node[key] || node[key] = curr_idx < last_idx ? {} : [])
114
- curr_idx += 1
115
- end
116
-
117
- hash
85
+ # @api private
86
+ def freeze
87
+ source_messages.select { |err| err.respond_to?(:evaluate) }.each do |err|
88
+ idx = messages.index(err) || source_messages.index(err)
89
+ msg = err.evaluate(locale: locale, full: options[:full])
90
+ messages[idx] = msg
118
91
  end
92
+ to_h
93
+ self
119
94
  end
120
95
  end
121
96
  end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/validation/message"
4
+ require "dry/schema/message_compiler"
5
+
6
+ module Dry
7
+ module Validation
8
+ module Messages
9
+ FULL_MESSAGE_WHITESPACE = Dry::Schema::MessageCompiler::FULL_MESSAGE_WHITESPACE
10
+
11
+ # Resolve translated messages from failure arguments
12
+ #
13
+ # @api public
14
+ class Resolver
15
+ # @!attribute [r] messages
16
+ # @return [Messages::I18n, Messages::YAML] messages backend
17
+ # @api private
18
+ attr_reader :messages
19
+
20
+ # @api private
21
+ def initialize(messages)
22
+ @messages = messages
23
+ end
24
+
25
+ # Resolve Message object from provided args and path
26
+ #
27
+ # This is used internally by contracts when rules are applied
28
+ # If message argument is a Hash, then it MUST have a :text key,
29
+ # which value will be used as the message value
30
+ #
31
+ # @return [Message, Message::Localized]
32
+ #
33
+ # @api public
34
+ def call(message:, tokens:, path:, meta: EMPTY_HASH)
35
+ case message
36
+ when Symbol
37
+ Message[->(**opts) { message(message, path: path, tokens: tokens, **opts) }, path, meta]
38
+ when String
39
+ Message[->(**opts) { [message_text(message, path: path, **opts), meta] }, path, meta]
40
+ when Hash
41
+ meta = message.dup
42
+ text = meta.delete(:text) { |key|
43
+ raise ArgumentError, <<~STR
44
+ +message+ Hash must contain :#{key} key (#{message.inspect} given)
45
+ STR
46
+ }
47
+
48
+ call(message: text, tokens: tokens, path: path, meta: meta)
49
+ else
50
+ raise ArgumentError, <<~STR
51
+ +message+ must be either a Symbol, String or Hash (#{message.inspect} given)
52
+ STR
53
+ end
54
+ end
55
+ alias_method :[], :call
56
+
57
+ # Resolve a message
58
+ #
59
+ # @return [String]
60
+ #
61
+ # @api public
62
+ #
63
+ # rubocop:disable Metrics/AbcSize
64
+ def message(rule, tokens: EMPTY_HASH, locale: nil, full: false, path:)
65
+ keys = path.to_a.compact
66
+ msg_opts = tokens.merge(path: keys, locale: locale || messages.default_locale)
67
+
68
+ if keys.empty?
69
+ template, meta = messages["rules.#{rule}", msg_opts]
70
+ else
71
+ template, meta = messages[rule, msg_opts.merge(path: keys.join(DOT))]
72
+ template, meta = messages[rule, msg_opts.merge(path: keys.last)] unless template
73
+ end
74
+
75
+ if !template && keys.size > 1
76
+ non_index_keys = keys.reject { |k| k.is_a?(Integer) }
77
+ template, meta = messages[rule, msg_opts.merge(path: non_index_keys.join(DOT))]
78
+ end
79
+
80
+ unless template
81
+ raise MissingMessageError, <<~STR
82
+ Message template for #{rule.inspect} under #{keys.join(DOT).inspect} was not found
83
+ STR
84
+ end
85
+
86
+ parsed_tokens = parse_tokens(tokens)
87
+ text = template.(template.data(parsed_tokens))
88
+
89
+ [message_text(text, path: path, locale: locale, full: full), meta]
90
+ end
91
+ # rubocop:enable Metrics/AbcSize
92
+
93
+ private
94
+
95
+ def message_text(text, path:, locale: nil, full: false)
96
+ return text unless full
97
+
98
+ key = key_text(path: path, locale: locale)
99
+
100
+ [key, text].compact.join(FULL_MESSAGE_WHITESPACE[locale])
101
+ end
102
+
103
+ def key_text(path:, locale: nil)
104
+ locale ||= messages.default_locale
105
+
106
+ keys = path.to_a.compact
107
+ msg_opts = {path: keys, locale: locale}
108
+
109
+ messages.rule(keys.last, msg_opts) || keys.last
110
+ end
111
+
112
+ def parse_tokens(tokens)
113
+ Hash[
114
+ tokens.map do |key, token|
115
+ [key, parse_token(token)]
116
+ end
117
+ ]
118
+ end
119
+
120
+ def parse_token(token)
121
+ case token
122
+ when Array
123
+ token.join(", ")
124
+ else
125
+ token
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end