dry-validation 0.13.3 → 1.4.2

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 (206) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +390 -95
  3. data/LICENSE +1 -1
  4. data/README.md +15 -12
  5. data/config/errors.yml +3 -88
  6. data/dry-validation.gemspec +30 -19
  7. data/lib/dry-validation.rb +2 -0
  8. data/lib/dry/validation.rb +47 -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 +160 -0
  12. data/lib/dry/validation/contract/class_interface.rb +222 -0
  13. data/lib/dry/validation/evaluator.rb +198 -0
  14. data/lib/dry/validation/extensions/hints.rb +69 -0
  15. data/lib/dry/validation/extensions/monads.rb +23 -7
  16. data/lib/dry/validation/extensions/predicates_as_macros.rb +75 -0
  17. data/lib/dry/validation/failures.rb +65 -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 +79 -79
  22. data/lib/dry/validation/message_set.rb +109 -88
  23. data/lib/dry/validation/messages/resolver.rb +106 -0
  24. data/lib/dry/validation/result.rb +168 -40
  25. data/lib/dry/validation/rule.rb +135 -0
  26. data/lib/dry/validation/schema_ext.rb +46 -0
  27. data/lib/dry/validation/values.rb +95 -0
  28. data/lib/dry/validation/version.rb +3 -1
  29. metadata +45 -335
  30. data/.codeclimate.yml +0 -17
  31. data/.gitignore +0 -9
  32. data/.rspec +0 -3
  33. data/.travis.yml +0 -29
  34. data/CONTRIBUTING.md +0 -31
  35. data/Gemfile +0 -25
  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 -137
  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 -188
  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 -119
  65. data/lib/dry/validation/messages/i18n.rb +0 -47
  66. data/lib/dry/validation/messages/namespaced.rb +0 -39
  67. data/lib/dry/validation/messages/yaml.rb +0 -61
  68. data/lib/dry/validation/predicate_registry.rb +0 -115
  69. data/lib/dry/validation/predicates.rb +0 -19
  70. data/lib/dry/validation/schema.rb +0 -126
  71. data/lib/dry/validation/schema/check.rb +0 -37
  72. data/lib/dry/validation/schema/class_interface.rb +0 -190
  73. data/lib/dry/validation/schema/deprecated.rb +0 -30
  74. data/lib/dry/validation/schema/dsl.rb +0 -118
  75. data/lib/dry/validation/schema/form.rb +0 -9
  76. data/lib/dry/validation/schema/json.rb +0 -21
  77. data/lib/dry/validation/schema/key.rb +0 -71
  78. data/lib/dry/validation/schema/params.rb +0 -22
  79. data/lib/dry/validation/schema/rule.rb +0 -202
  80. data/lib/dry/validation/schema/value.rb +0 -211
  81. data/lib/dry/validation/schema_compiler.rb +0 -81
  82. data/lib/dry/validation/template.rb +0 -66
  83. data/lib/dry/validation/type_specs.rb +0 -70
  84. data/spec/extensions/monads/result_spec.rb +0 -40
  85. data/spec/extensions/struct/schema_spec.rb +0 -32
  86. data/spec/fixtures/locales/en.yml +0 -8
  87. data/spec/fixtures/locales/pl.yml +0 -22
  88. data/spec/integration/custom_error_messages_spec.rb +0 -54
  89. data/spec/integration/custom_predicates_spec.rb +0 -228
  90. data/spec/integration/hints_spec.rb +0 -170
  91. data/spec/integration/injecting_rules_spec.rb +0 -30
  92. data/spec/integration/json/defining_base_schema_spec.rb +0 -41
  93. data/spec/integration/localized_error_messages_spec.rb +0 -72
  94. data/spec/integration/message_compiler_spec.rb +0 -405
  95. data/spec/integration/messages/i18n_spec.rb +0 -104
  96. data/spec/integration/optional_keys_spec.rb +0 -28
  97. data/spec/integration/params/predicates/array_spec.rb +0 -287
  98. data/spec/integration/params/predicates/empty_spec.rb +0 -263
  99. data/spec/integration/params/predicates/eql_spec.rb +0 -327
  100. data/spec/integration/params/predicates/even_spec.rb +0 -455
  101. data/spec/integration/params/predicates/excluded_from_spec.rb +0 -455
  102. data/spec/integration/params/predicates/excludes_spec.rb +0 -391
  103. data/spec/integration/params/predicates/false_spec.rb +0 -455
  104. data/spec/integration/params/predicates/filled_spec.rb +0 -467
  105. data/spec/integration/params/predicates/format_spec.rb +0 -454
  106. data/spec/integration/params/predicates/gt_spec.rb +0 -519
  107. data/spec/integration/params/predicates/gteq_spec.rb +0 -519
  108. data/spec/integration/params/predicates/included_in_spec.rb +0 -455
  109. data/spec/integration/params/predicates/includes_spec.rb +0 -391
  110. data/spec/integration/params/predicates/key_spec.rb +0 -67
  111. data/spec/integration/params/predicates/lt_spec.rb +0 -519
  112. data/spec/integration/params/predicates/lteq_spec.rb +0 -519
  113. data/spec/integration/params/predicates/max_size_spec.rb +0 -391
  114. data/spec/integration/params/predicates/min_size_spec.rb +0 -391
  115. data/spec/integration/params/predicates/none_spec.rb +0 -265
  116. data/spec/integration/params/predicates/not_eql_spec.rb +0 -327
  117. data/spec/integration/params/predicates/odd_spec.rb +0 -455
  118. data/spec/integration/params/predicates/size/fixed_spec.rb +0 -393
  119. data/spec/integration/params/predicates/size/range_spec.rb +0 -396
  120. data/spec/integration/params/predicates/true_spec.rb +0 -455
  121. data/spec/integration/params/predicates/type_spec.rb +0 -391
  122. data/spec/integration/result_spec.rb +0 -81
  123. data/spec/integration/schema/array_schema_spec.rb +0 -59
  124. data/spec/integration/schema/check_rules_spec.rb +0 -119
  125. data/spec/integration/schema/check_with_nested_el_spec.rb +0 -37
  126. data/spec/integration/schema/check_with_nth_el_spec.rb +0 -25
  127. data/spec/integration/schema/default_settings_spec.rb +0 -11
  128. data/spec/integration/schema/defining_base_schema_spec.rb +0 -41
  129. data/spec/integration/schema/dynamic_predicate_args_spec.rb +0 -43
  130. data/spec/integration/schema/each_with_set_spec.rb +0 -70
  131. data/spec/integration/schema/extending_dsl_spec.rb +0 -27
  132. data/spec/integration/schema/form_spec.rb +0 -236
  133. data/spec/integration/schema/hash_schema_spec.rb +0 -47
  134. data/spec/integration/schema/inheriting_schema_spec.rb +0 -31
  135. data/spec/integration/schema/input_processor_spec.rb +0 -46
  136. data/spec/integration/schema/json/explicit_types_spec.rb +0 -157
  137. data/spec/integration/schema/json_spec.rb +0 -163
  138. data/spec/integration/schema/macros/confirmation_spec.rb +0 -35
  139. data/spec/integration/schema/macros/each_spec.rb +0 -268
  140. data/spec/integration/schema/macros/filled_spec.rb +0 -87
  141. data/spec/integration/schema/macros/input_spec.rb +0 -139
  142. data/spec/integration/schema/macros/maybe_spec.rb +0 -99
  143. data/spec/integration/schema/macros/rule_spec.rb +0 -75
  144. data/spec/integration/schema/macros/value_spec.rb +0 -119
  145. data/spec/integration/schema/macros/when_spec.rb +0 -62
  146. data/spec/integration/schema/nested_schemas_spec.rb +0 -236
  147. data/spec/integration/schema/nested_values_spec.rb +0 -46
  148. data/spec/integration/schema/not_spec.rb +0 -34
  149. data/spec/integration/schema/numbers_spec.rb +0 -19
  150. data/spec/integration/schema/option_with_default_spec.rb +0 -64
  151. data/spec/integration/schema/or_spec.rb +0 -87
  152. data/spec/integration/schema/params/defining_base_schema_spec.rb +0 -41
  153. data/spec/integration/schema/params/explicit_types_spec.rb +0 -195
  154. data/spec/integration/schema/params_spec.rb +0 -234
  155. data/spec/integration/schema/predicate_verification_spec.rb +0 -9
  156. data/spec/integration/schema/predicates/array_spec.rb +0 -295
  157. data/spec/integration/schema/predicates/custom_spec.rb +0 -103
  158. data/spec/integration/schema/predicates/empty_spec.rb +0 -263
  159. data/spec/integration/schema/predicates/eql_spec.rb +0 -327
  160. data/spec/integration/schema/predicates/even_spec.rb +0 -455
  161. data/spec/integration/schema/predicates/excluded_from/array_spec.rb +0 -459
  162. data/spec/integration/schema/predicates/excluded_from/range_spec.rb +0 -459
  163. data/spec/integration/schema/predicates/excludes_spec.rb +0 -391
  164. data/spec/integration/schema/predicates/filled_spec.rb +0 -467
  165. data/spec/integration/schema/predicates/format_spec.rb +0 -455
  166. data/spec/integration/schema/predicates/gt_spec.rb +0 -519
  167. data/spec/integration/schema/predicates/gteq_spec.rb +0 -519
  168. data/spec/integration/schema/predicates/hash_spec.rb +0 -69
  169. data/spec/integration/schema/predicates/included_in/array_spec.rb +0 -459
  170. data/spec/integration/schema/predicates/included_in/range_spec.rb +0 -459
  171. data/spec/integration/schema/predicates/includes_spec.rb +0 -391
  172. data/spec/integration/schema/predicates/key_spec.rb +0 -88
  173. data/spec/integration/schema/predicates/lt_spec.rb +0 -520
  174. data/spec/integration/schema/predicates/lteq_spec.rb +0 -519
  175. data/spec/integration/schema/predicates/max_size_spec.rb +0 -391
  176. data/spec/integration/schema/predicates/min_size_spec.rb +0 -391
  177. data/spec/integration/schema/predicates/none_spec.rb +0 -265
  178. data/spec/integration/schema/predicates/not_eql_spec.rb +0 -391
  179. data/spec/integration/schema/predicates/odd_spec.rb +0 -455
  180. data/spec/integration/schema/predicates/size/fixed_spec.rb +0 -398
  181. data/spec/integration/schema/predicates/size/range_spec.rb +0 -395
  182. data/spec/integration/schema/predicates/type_spec.rb +0 -413
  183. data/spec/integration/schema/reusing_schema_spec.rb +0 -33
  184. data/spec/integration/schema/using_types_spec.rb +0 -135
  185. data/spec/integration/schema/validate_spec.rb +0 -120
  186. data/spec/integration/schema/xor_spec.rb +0 -35
  187. data/spec/integration/schema_builders_spec.rb +0 -17
  188. data/spec/integration/schema_spec.rb +0 -173
  189. data/spec/shared/message_compiler.rb +0 -11
  190. data/spec/shared/predicate_helper.rb +0 -15
  191. data/spec/shared/rule_compiler.rb +0 -8
  192. data/spec/spec_helper.rb +0 -62
  193. data/spec/support/define_struct.rb +0 -25
  194. data/spec/support/matchers.rb +0 -38
  195. data/spec/support/mutant.rb +0 -9
  196. data/spec/support/predicates_integration.rb +0 -7
  197. data/spec/unit/input_processor_compiler/json_spec.rb +0 -283
  198. data/spec/unit/input_processor_compiler/params_spec.rb +0 -328
  199. data/spec/unit/message_compiler/visit_failure_spec.rb +0 -38
  200. data/spec/unit/message_compiler/visit_spec.rb +0 -16
  201. data/spec/unit/message_compiler_spec.rb +0 -7
  202. data/spec/unit/predicate_registry_spec.rb +0 -34
  203. data/spec/unit/schema/key_spec.rb +0 -38
  204. data/spec/unit/schema/rule_spec.rb +0 -42
  205. data/spec/unit/schema/value_spec.rb +0 -131
  206. data/spec/unit/schema_spec.rb +0 -35
@@ -1,122 +1,143 @@
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
30
+ @locale = options[:locale]
31
+ @source_messages = options.fetch(:source) { messages.dup }
32
+ super
32
33
  end
33
34
 
34
- def failures?
35
- options[:failures].equal?(true)
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
36
47
  end
37
48
 
38
- def empty?
39
- messages.empty?
40
- end
41
-
42
- def root?
43
- !empty? && failures.all?(&:root?)
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
+ initialize_placeholders!
61
+ self
44
62
  end
45
63
 
46
- def each(&block)
47
- return to_enum unless block
48
- messages.each(&block)
64
+ # Filter message set using provided predicates
65
+ #
66
+ # This method is open to any predicate because messages can be anything that
67
+ # implements Message API, thus they can implement whatever predicates you
68
+ # may need.
69
+ #
70
+ # @example get a list of base messages
71
+ # message_set = contract.(input).errors
72
+ # message_set.filter(:base?)
73
+ #
74
+ # @param [Array<Symbol>] predicates
75
+ #
76
+ # @return [MessageSet]
77
+ #
78
+ # @api public
79
+ def filter(*predicates)
80
+ messages = select { |msg|
81
+ predicates.all? { |predicate| msg.respond_to?(predicate) && msg.public_send(predicate) }
82
+ }
83
+ self.class.new(messages)
49
84
  end
50
85
 
51
- def to_h
52
- if root?
53
- { nil => failures.map(&:to_s) }
54
- else
55
- failures? ? messages_map : hints_map
86
+ # @api private
87
+ def freeze
88
+ source_messages.select { |err| err.respond_to?(:evaluate) }.each do |err|
89
+ idx = source_messages.index(err)
90
+ msg = err.evaluate(locale: locale, full: options[:full])
91
+ messages[idx] = msg
56
92
  end
57
- end
58
- alias_method :to_hash, :to_h
59
-
60
- def to_a
61
- to_h.values.flatten
93
+ to_h
94
+ self
62
95
  end
63
96
 
64
97
  private
65
98
 
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
99
+ # @api private
100
+ def unique_paths
101
+ source_messages.uniq(&:path).map(&:path)
81
102
  end
82
103
 
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
-
104
+ # @api private
105
+ def messages_map
106
+ @messages_map ||= reduce(placeholders) { |hash, msg|
107
+ node = msg.path.reduce(hash) { |a, e| a.is_a?(Hash) ? a[e] : a.last[e] }
108
+ (node[0].is_a?(::Array) ? node[0] : node) << msg.dump
93
109
  hash
94
- end
95
- end
96
-
97
- def hint_groups
98
- @hint_groups ||= hints.group_by(&:path)
99
- end
100
-
101
- def initialize_hints!
102
- hints.reject! { |hint| HINT_EXCLUSION.include?(hint.predicate) }
110
+ }
103
111
  end
104
112
 
113
+ # @api private
114
+ #
115
+ # rubocop:disable Metrics/AbcSize
116
+ # rubocop:disable Metrics/PerceivedComplexity
105
117
  def initialize_placeholders!
106
- @placeholders = paths.reduce({}) do |hash, path|
118
+ @placeholders = unique_paths.sort_by(&:size).each_with_object(EMPTY_HASH.dup) { |path, hash|
107
119
  curr_idx = 0
108
120
  last_idx = path.size - 1
109
121
  node = hash
110
122
 
111
- while curr_idx <= last_idx do
123
+ while curr_idx <= last_idx
112
124
  key = path[curr_idx]
113
- node = (node[key] || node[key] = curr_idx < last_idx ? {} : [])
125
+
126
+ next_node =
127
+ if node.is_a?(Array) && key.is_a?(Symbol)
128
+ node_hash = (node << [] << {}).last
129
+ node_hash[key] || (node_hash[key] = curr_idx < last_idx ? {} : [])
130
+ else
131
+ node[key] || (node[key] = curr_idx < last_idx ? {} : [])
132
+ end
133
+
134
+ node = next_node
114
135
  curr_idx += 1
115
136
  end
116
-
117
- hash
118
- end
137
+ }
119
138
  end
139
+ # rubocop:enable Metrics/AbcSize
140
+ # rubocop:enable Metrics/PerceivedComplexity
120
141
  end
121
142
  end
122
143
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/validation/message'
4
+
5
+ module Dry
6
+ module Validation
7
+ module Messages
8
+ # Resolve translated messages from failure arguments
9
+ #
10
+ # @api public
11
+ class Resolver
12
+ # @!attribute [r] messages
13
+ # @return [Messages::I18n, Messages::YAML] messages backend
14
+ # @api private
15
+ attr_reader :messages
16
+
17
+ # @api private
18
+ def initialize(messages)
19
+ @messages = messages
20
+ end
21
+
22
+ # Resolve Message object from provided args and path
23
+ #
24
+ # This is used internally by contracts when rules are applied
25
+ # If message argument is a Hash, then it MUST have a :text key,
26
+ # which value will be used as the message value
27
+ #
28
+ # @return [Message, Message::Localized]
29
+ #
30
+ # @api public
31
+ def call(message:, tokens:, path:, meta: EMPTY_HASH)
32
+ case message
33
+ when Symbol
34
+ Message[->(**opts) { message(message, path: path, tokens: tokens, **opts) }, path, meta]
35
+ when String
36
+ Message[message, path, meta]
37
+ when Hash
38
+ meta = message.dup
39
+ text = meta.delete(:text) { |key|
40
+ raise ArgumentError, <<~STR
41
+ +message+ Hash must contain :#{key} key (#{message.inspect} given)
42
+ STR
43
+ }
44
+
45
+ call(message: text, tokens: tokens, path: path, meta: meta)
46
+ else
47
+ raise ArgumentError, <<~STR
48
+ +message+ must be either a Symbol, String or Hash (#{message.inspect} given)
49
+ STR
50
+ end
51
+ end
52
+ alias_method :[], :call
53
+
54
+ # Resolve a message
55
+ #
56
+ # @return [String]
57
+ #
58
+ # @api public
59
+ #
60
+ # rubocop:disable Metrics/AbcSize
61
+ def message(rule, tokens: EMPTY_HASH, locale: nil, full: false, path:)
62
+ keys = path.to_a.compact
63
+ msg_opts = tokens.merge(path: keys, locale: locale || messages.default_locale)
64
+
65
+ if keys.empty?
66
+ template, meta = messages["rules.#{rule}", msg_opts]
67
+ else
68
+ template, meta = messages[rule, msg_opts.merge(path: keys.join(DOT))]
69
+ template, meta = messages[rule, msg_opts.merge(path: keys.last)] unless template
70
+ end
71
+
72
+ unless template
73
+ raise MissingMessageError, <<~STR
74
+ Message template for #{rule.inspect} under #{keys.join(DOT).inspect} was not found
75
+ STR
76
+ end
77
+
78
+ parsed_tokens = parse_tokens(tokens)
79
+ text = template.(template.data(parsed_tokens))
80
+
81
+ [full ? "#{messages.rule(keys.last, msg_opts)} #{text}" : text, meta]
82
+ end
83
+ # rubocop:enable Metrics/AbcSize
84
+
85
+ private
86
+
87
+ def parse_tokens(tokens)
88
+ Hash[
89
+ tokens.map do |key, token|
90
+ [key, parse_token(token)]
91
+ end
92
+ ]
93
+ end
94
+
95
+ def parse_token(token)
96
+ case token
97
+ when Array
98
+ token.join(', ')
99
+ else
100
+ token
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -1,82 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent/map'
1
4
  require 'dry/equalizer'
2
5
 
6
+ require 'dry/validation/constants'
7
+ require 'dry/validation/message_set'
8
+ require 'dry/validation/values'
9
+
3
10
  module Dry
4
11
  module Validation
12
+ # Result objects are returned by contracts
13
+ #
14
+ # @api public
5
15
  class Result
6
- include Dry::Equalizer(:output, :errors)
7
- include Enumerable
8
-
9
- attr_reader :output
10
- attr_reader :results
11
- attr_reader :message_compiler
12
- attr_reader :path
13
-
14
- alias_method :to_hash, :output
15
- alias_method :to_h, :output # for MRI 2.0, remove it when drop support
16
+ include Dry::Equalizer(:schema_result, :context, :errors, inspect: false)
17
+
18
+ # Build a new result
19
+ #
20
+ # @param [Dry::Schema::Result] schema_result
21
+ #
22
+ # @api private
23
+ def self.new(schema_result, context = ::Concurrent::Map.new, options = EMPTY_HASH)
24
+ result = super
25
+ yield(result) if block_given?
26
+ result.freeze
27
+ end
16
28
 
17
- def initialize(output, results, message_compiler, path)
18
- @output = output
19
- @results = results
20
- @message_compiler = message_compiler
21
- @path = path
29
+ # Context that's shared between rules
30
+ #
31
+ # @return [Concurrent::Map]
32
+ #
33
+ # @api public
34
+ attr_reader :context
35
+
36
+ # Result from contract's schema
37
+ #
38
+ # @return [Dry::Schema::Result]
39
+ #
40
+ # @api private
41
+ attr_reader :schema_result
42
+
43
+ # Result options
44
+ #
45
+ # @return [Hash]
46
+ #
47
+ # @api private
48
+ attr_reader :options
49
+
50
+ # Initialize a new result
51
+ #
52
+ # @api private
53
+ def initialize(schema_result, context, options)
54
+ @schema_result = schema_result
55
+ @context = context
56
+ @options = options
57
+ @errors = initialize_errors
22
58
  end
23
59
 
24
- def each(&block)
25
- output.each(&block)
60
+ # Return values wrapper with the input processed by schema
61
+ #
62
+ # @return [Values]
63
+ #
64
+ # @api public
65
+ def values
66
+ @values ||= Values.new(schema_result.to_h)
26
67
  end
27
68
 
28
- def [](name)
29
- output.fetch(name)
69
+ # Get error set
70
+ #
71
+ # @!macro errors-options
72
+ # @param [Hash] new_options
73
+ # @option new_options [Symbol] :locale Set locale for messages
74
+ # @option new_options [Boolean] :hints Enable/disable hints
75
+ # @option new_options [Boolean] :full Get messages that include key names
76
+ #
77
+ # @return [MessageSet]
78
+ #
79
+ # @api public
80
+ def errors(new_options = EMPTY_HASH)
81
+ new_options.empty? ? @errors : @errors.with(schema_errors(new_options), new_options)
30
82
  end
31
83
 
84
+ # Check if result is successful
85
+ #
86
+ # @return [Bool]
87
+ #
88
+ # @api public
32
89
  def success?
33
- results.empty?
90
+ @errors.empty?
34
91
  end
35
92
 
93
+ # Check if result is not successful
94
+ #
95
+ # @return [Bool]
96
+ #
97
+ # @api public
36
98
  def failure?
37
99
  !success?
38
100
  end
39
101
 
40
- def messages(options = EMPTY_HASH)
41
- message_set(options).dump
102
+ # Check if values include an error for the provided key
103
+ #
104
+ # @api private
105
+ def error?(key)
106
+ schema_result.error?(key)
107
+ end
108
+
109
+ # Check if there's any error for the provided key
110
+ #
111
+ # This does not consider errors from the nested values
112
+ #
113
+ # @api private
114
+ def base_error?(key)
115
+ schema_result.errors.any? { |error|
116
+ key_path = Schema::Path[key]
117
+ err_path = Schema::Path[error.path]
118
+
119
+ return false unless key_path.same_root?(err_path)
120
+
121
+ key_path == err_path
122
+ }
123
+ end
124
+
125
+ # Add a new error for the provided key
126
+ #
127
+ # @api private
128
+ def add_error(error)
129
+ @errors.add(error)
130
+ self
42
131
  end
43
132
 
44
- def errors(options = EMPTY_HASH)
45
- message_set(options.merge(hints: false)).dump
133
+ # Read a value under provided key
134
+ #
135
+ # @param [Symbol] key
136
+ #
137
+ # @return [Object]
138
+ #
139
+ # @api public
140
+ def [](key)
141
+ values[key]
46
142
  end
47
143
 
48
- def hints(options = EMPTY_HASH)
49
- message_set(options.merge(failures: false)).dump
144
+ # Check if a key was set
145
+ #
146
+ # @param [Symbol] key
147
+ #
148
+ # @return [Bool]
149
+ #
150
+ # @api public
151
+ def key?(key)
152
+ values.key?(key)
50
153
  end
51
154
 
52
- def message_set(options = EMPTY_HASH)
53
- message_compiler.with(options).(result_ast)
155
+ # Coerce to a hash
156
+ #
157
+ # @api public
158
+ def to_h
159
+ values.to_h
54
160
  end
55
161
 
56
- def to_ast
57
- if name
58
- [type, [name, [:set, result_ast]]]
162
+ # Return a string representation
163
+ #
164
+ # @api public
165
+ def inspect
166
+ if context.empty?
167
+ "#<#{self.class}#{to_h} errors=#{errors.to_h}>"
59
168
  else
60
- ast
169
+ "#<#{self.class}#{to_h} errors=#{errors.to_h} context=#{context.each.to_h}>"
61
170
  end
62
171
  end
63
172
 
64
- def ast(*)
65
- [:set, result_ast]
173
+ # Freeze result and its error set
174
+ #
175
+ # @api private
176
+ def freeze
177
+ values.freeze
178
+ errors.freeze
179
+ super
66
180
  end
67
181
 
68
- def name
69
- Array(path).last
182
+ if RUBY_VERSION >= '2.7'
183
+ # Pattern matching
184
+ #
185
+ # @api private
186
+ def deconstruct_keys(keys)
187
+ values.deconstruct_keys(keys)
188
+ end
189
+
190
+ # Pattern matching
191
+ #
192
+ # @api private
193
+ def deconstruct
194
+ [values, context.each.to_h]
195
+ end
70
196
  end
71
197
 
72
198
  private
73
199
 
74
- def type
75
- success? ? :success : :failure
200
+ # @api private
201
+ def initialize_errors(options = self.options)
202
+ MessageSet.new(schema_errors(options), options)
76
203
  end
77
204
 
78
- def result_ast
79
- @result_ast ||= results.map(&:to_ast)
205
+ # @api private
206
+ def schema_errors(options)
207
+ schema_result.message_set(options).to_a
80
208
  end
81
209
  end
82
210
  end