dry-validation 1.0.0.alpha2 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 288a3f0d9af21271cfeb794d48752ba6bfa93c8e4fa088e8faa9f6eb9a05e083
4
- data.tar.gz: 1949525da40fb528c5eda20c610a5c017c260600613ba4fc392f23cd6a84a8d7
3
+ metadata.gz: f6a47d27d49e9894cf283af7e4809cea07f45e5f8f3f52a180dd20d8c849b315
4
+ data.tar.gz: 83f1e9b5ff94c4db24769624dd1552f0876be541926ffd57d43c2490757ab662
5
5
  SHA512:
6
- metadata.gz: f4884bb2c55f0b987b0aad70bceaedc0d1bc5adc5056bd47e9606e126a9ce9bcb450f5ec7c2035b35572eb4677c4ee0c1525856fe83866b07bf559a75b97a260
7
- data.tar.gz: f8f79772a1950752dfa03204269df85d7480c52114c4369d2e895e1edd02b6f02ee3bc03049a2c4ed0994042b104b82dd0e22fcb1c31882b7b0c741d414c8ae1
6
+ metadata.gz: 67ab872380ecdc45fbe1efeb8cec95a03dff692aae2e74ca498483ceb7104b6a2d5cd78bdc23884d69d89665f110f79c3fa67285b97a40cc2c3625f9744bc34f
7
+ data.tar.gz: f306b9dc73085526c485a7b18f2e33c83d08dd72922f1640bbc9b3b6ea0164ad1fbda752da0bb9b5d1f55dbd47c6ca6aa9ebef297035fd7adb22bb18be879c4e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ # v1.0.0.beta1 2019-03-26
2
+
3
+ ### Added
4
+
5
+ * New API for setting failures `base.failure` for base errors and `key.failure` for key errors (solnic)
6
+ * Support for `base` errors associated with a key even when child keys have errors too (solnic)
7
+ * Support for `base` errors not associated with any key (solnic)
8
+ * Result objects use `ErrorSet` object now for managing messages (solnic)
9
+ * Nested keys are properly handled when generating messages hash (issue #489) (flash-gordon + solnic)
10
+ * Result objects support `locale` and `full` options now (solnic)
11
+ * Ability to configure `top_namespace` for messages, which will be used for both schema and rule localization (solnic)
12
+ * Rule blocks receive a context object that you can use to share data between rules (solnic)
13
+
14
+ ### Changed
15
+
16
+ * [BREAKING] `Result#errors` returns an instance of `ErrorSet` now, it's an enumerable, coerible to a hash (solnic)
17
+ * [BREAKING] `failure` was removed in favor of `key.failure` or `key(:foo).failure` (solnic)
18
+ * [BREAKING] `Result#to_hash` was removed (flash-gordon)
19
+
20
+ [Compare v1.0.0.alpha2...v1.0.0.beta1](https://github.com/dry-rb/dry-validation/compare/v1.0.0.alpha2...v1.0.0.beta1)
21
+
1
22
  # v1.0.0.alpha2 2019-03-05
2
23
 
3
24
  First round of bug fixes. Thanks for testing <3!
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/schema/config'
4
+
5
+ module Dry
6
+ module Validation
7
+ class Config < Schema::Config
8
+ end
9
+ end
10
+ end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'dry/schema'
4
- require 'dry/validation/messages'
4
+ require 'dry/schema/messages'
5
+
5
6
  require 'dry/validation/constants'
6
7
 
7
8
  module Dry
@@ -11,6 +12,21 @@ module Dry
11
12
  #
12
13
  # @api public
13
14
  module ClassInterface
15
+ # @api private
16
+ def inherited(klass)
17
+ super
18
+ klass.instance_variable_set('@config', config.dup)
19
+ end
20
+
21
+ # Configuration
22
+ #
23
+ # @return [Config]
24
+ #
25
+ # @api public
26
+ def config
27
+ @config ||= Validation::Config.new
28
+ end
29
+
14
30
  # Define a params schema for your contract
15
31
  #
16
32
  # This type of schema is suitable for HTTP parameters
@@ -78,14 +94,14 @@ module Dry
78
94
 
79
95
  # @api private
80
96
  def rules
81
- @__rules__ ||= EMPTY_ARRAY
82
- .dup
83
- .concat(superclass.respond_to?(:rules) ? superclass.rules : EMPTY_ARRAY)
97
+ @rules ||= EMPTY_ARRAY
98
+ .dup
99
+ .concat(superclass.respond_to?(:rules) ? superclass.rules : EMPTY_ARRAY)
84
100
  end
85
101
 
86
102
  # @api private
87
103
  def messages
88
- @__messages__ ||= Messages.setup(config)
104
+ @messages ||= Schema::Messages.setup(config.messages)
89
105
  end
90
106
 
91
107
  private
@@ -97,7 +113,9 @@ module Dry
97
113
 
98
114
  # @api private
99
115
  def define(method_name, &block)
100
- raise ::Dry::Validation::DuplicateSchemaError, 'Schema has already been defined' if defined?(@__schema__)
116
+ if defined?(@__schema__)
117
+ raise ::Dry::Validation::DuplicateSchemaError, 'Schema has already been defined'
118
+ end
101
119
 
102
120
  case method_name
103
121
  when :schema
@@ -1,13 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'concurrent/map'
4
+
3
5
  require 'dry/equalizer'
4
- require 'dry/configurable'
5
6
  require 'dry/initializer'
6
7
 
8
+ require 'dry/validation/config'
7
9
  require 'dry/validation/constants'
8
10
  require 'dry/validation/rule'
9
11
  require 'dry/validation/evaluator'
12
+ require 'dry/validation/messages/resolver'
10
13
  require 'dry/validation/result'
14
+ require 'dry/validation/error'
11
15
  require 'dry/validation/contract/class_interface'
12
16
 
13
17
  module Dry
@@ -47,40 +51,20 @@ module Dry
47
51
  class Contract
48
52
  include Dry::Equalizer(:schema, :rules, :messages)
49
53
 
50
- extend Dry::Configurable
51
54
  extend Dry::Initializer
52
55
  extend ClassInterface
53
56
 
54
- # @!group Configuration
55
-
56
- # @overload config.messages=(identifier)
57
- # Set message backend
58
- #
59
- # @param identifier [Symbol] the backend identifier, either `:yaml` or `:i18n`
60
- #
61
- # @api public
62
- # @!scope class
63
- setting :messages, :yaml
57
+ config.messages.top_namespace = 'dry_validation'
64
58
 
65
- # @overload config.messages_file=(path)
66
- # Set additional path to messages file
67
- #
68
- # @param path [String, Pathname] the path
69
- #
59
+ # @!attribute [r] config
60
+ # @return [Config]
70
61
  # @api public
71
- # @!scope class
72
- setting :messages_file
62
+ option :config, default: -> { self.class.config }
73
63
 
74
- # @overload config.namespace=(name)
75
- # Set namespace that will be used to override default messages
76
- #
77
- # @param name [Symbol] the namespace
78
- #
64
+ # @!attribute [r] locale
65
+ # @return [Symbol]
79
66
  # @api public
80
- # @!scope class
81
- setting :namespace
82
-
83
- # @!endgroup
67
+ option :locale, default: -> { :en }
84
68
 
85
69
  # @!attribute [r] schema
86
70
  # @return [Dry::Schema::Params, Dry::Schema::JSON, Dry::Schema::Processor]
@@ -92,10 +76,10 @@ module Dry
92
76
  # @api private
93
77
  option :rules, default: -> { self.class.rules }
94
78
 
95
- # @!attribute [r] messages
96
- # @return [Messages::I18n, Messages::YAML]
79
+ # @!attribute [r] message_resolver
80
+ # @return [Messages::Resolver]
97
81
  # @api private
98
- option :messages, default: -> { self.class.messages }
82
+ option :message_resolver, default: -> { Messages::Resolver.new(self.class.messages, locale) }
99
83
 
100
84
  # Apply contract to an input
101
85
  #
@@ -103,36 +87,18 @@ module Dry
103
87
  #
104
88
  # @api public
105
89
  def call(input)
106
- Result.new(schema.(input)) do |result|
90
+ Result.new(schema.(input), locale: locale) do |result|
91
+ context = Concurrent::Map.new
92
+
107
93
  rules.each do |rule|
108
94
  next if rule.keys.any? { |key| result.error?(key) }
109
95
 
110
- rule_result = rule.(self, result)
111
- result.add_error(*rule_result.message) if rule_result.failure?
96
+ rule.(self, result, context).failures.each do |failure|
97
+ result.add_error(message_resolver[failure])
98
+ end
112
99
  end
113
100
  end
114
101
  end
115
-
116
- # Get message text for the given rule name and options
117
- #
118
- # @return [String]
119
- #
120
- # @api private
121
- def message(key, tokens: EMPTY_HASH, **opts)
122
- msg_opts = opts.merge(tokens)
123
- rule_path = Array(opts.fetch(:rule)).flatten
124
-
125
- template = messages[key, msg_opts.merge(rule: rule_path.join(DOT))]
126
- template ||= messages[key, msg_opts.merge(rule: rule_path.last)]
127
-
128
- unless template
129
- raise MissingMessageError, <<~STR
130
- Message template for #{key.inspect} under #{rule_path.join(DOT).inspect} was not found
131
- STR
132
- end
133
-
134
- template.(template.data(tokens))
135
- end
136
102
  end
137
103
  end
138
104
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/equalizer'
4
+ require 'dry/schema/message'
5
+
6
+ module Dry
7
+ module Validation
8
+ # Error message
9
+ #
10
+ # @api public
11
+ class Error < Schema::Message
12
+ include Dry::Equalizer(:text, :path)
13
+
14
+ # @!attribute [r] text
15
+ # @return [String] text The error message text
16
+ attr_reader :text
17
+
18
+ # @!attribute [r] path
19
+ # @return [Array<Symbol, Integer>] path The path to the value with the error
20
+ attr_reader :path
21
+
22
+ # @api public
23
+ class Localized < Error
24
+ # @api public
25
+ def evaluate(**opts)
26
+ Error.new(text.(opts), path: path)
27
+ end
28
+ end
29
+
30
+ # Build an error
31
+ #
32
+ # @return [Error, Error::Localized]
33
+ #
34
+ # @api public
35
+ def self.[](text, path)
36
+ text.respond_to?(:call) ? Localized.new(text, path: path) : Error.new(text, path: path)
37
+ end
38
+
39
+ # Initialize a new error object
40
+ #
41
+ # @api private
42
+ def initialize(text, path:)
43
+ @text = text
44
+ @path = Array(path)
45
+ end
46
+
47
+ # Check if this is a base error not associated with any key
48
+ #
49
+ # @return [Boolean]
50
+ #
51
+ # @api public
52
+ def base?
53
+ @base ||= path.compact.empty?
54
+ end
55
+
56
+ # Dump error to a string
57
+ #
58
+ # @return [String]
59
+ #
60
+ # @api public
61
+ def to_s
62
+ text
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/schema/message_set'
4
+
5
+ require 'dry/validation/constants'
6
+ require 'dry/validation/error'
7
+
8
+ module Dry
9
+ module Validation
10
+ # ErrorSet is a specialized message set for handling validation errors
11
+ #
12
+ # @api public
13
+ class ErrorSet < Schema::MessageSet
14
+ # @!attribute [r] source
15
+ # Return the source set of messages used to produce final evaluated messages
16
+ # @return [Array<Error, Error::Localized, Schema::Message>]
17
+ # @api private
18
+ attr_reader :source_messages
19
+
20
+ # @!attribute [r] locale
21
+ # @return [Symbol] locale
22
+ # @api public
23
+ attr_reader :locale
24
+
25
+ # @api private
26
+ def initialize(messages, options = EMPTY_HASH)
27
+ @locale = options.fetch(:locale, :en)
28
+ @source_messages = options.fetch(:source, messages.dup)
29
+ super
30
+ end
31
+
32
+ # Return a new error set using updated options
33
+ #
34
+ # @return [ErrorSet]
35
+ #
36
+ # @api private
37
+ def with(other, new_options = EMPTY_HASH)
38
+ return self if new_options.empty?
39
+
40
+ self.class.new(
41
+ other + select { |err| err.is_a?(Error) },
42
+ options.merge(source: source_messages, **new_options)
43
+ ).freeze
44
+ end
45
+
46
+ # Add a new error
47
+ #
48
+ # This is used when result is being prepared
49
+ #
50
+ # @return [ErrorSet]
51
+ #
52
+ # @api private
53
+ def add(error)
54
+ source_messages << error
55
+ messages << error
56
+ initialize_placeholders!
57
+ self
58
+ end
59
+
60
+ # Filter error set using provided predicates
61
+ #
62
+ # This method is open to any predicate because errors can be anything that
63
+ # implements Message API, thus they can implement whatever predicates you
64
+ # may need.
65
+ #
66
+ # @example get a list of base errors
67
+ # error_set = contract.(input).error_set
68
+ # error_set.filter(:base?)
69
+ #
70
+ # @param [Array<Symbol>] *predicates
71
+ #
72
+ # @return [ErrorSet]
73
+ #
74
+ # @api public
75
+ def filter(*predicates)
76
+ errors = select { |e|
77
+ predicates.all? { |predicate| e.respond_to?(predicate) && e.public_send(predicate) }
78
+ }
79
+ self.class.new(errors)
80
+ end
81
+
82
+ # @api private
83
+ def freeze
84
+ source_messages.select { |err| err.respond_to?(:evaluate) }.each do |err|
85
+ idx = source_messages.index(err)
86
+ msg = err.evaluate(locale: locale, full: options[:full])
87
+ messages[idx] = msg
88
+ end
89
+ to_h
90
+ self
91
+ end
92
+
93
+ private
94
+
95
+ # @api private
96
+ def unique_paths
97
+ source_messages.uniq(&:path).map(&:path)
98
+ end
99
+
100
+ # @api private
101
+ def messages_map
102
+ @messages_map ||= reduce(placeholders) { |hash, msg|
103
+ node = msg.path.reduce(hash) { |a, e| a.is_a?(Hash) ? a[e] : a.last[e] }
104
+ (node.size > 1 ? node[0] : node) << msg.to_s
105
+ hash
106
+ }
107
+ end
108
+
109
+ # @api private
110
+ #
111
+ # rubocop:disable Metrics/AbcSize
112
+ # rubocop:disable Metrics/PerceivedComplexity
113
+ def initialize_placeholders!
114
+ @placeholders = unique_paths.each_with_object(EMPTY_HASH.dup) { |path, hash|
115
+ curr_idx = 0
116
+ last_idx = path.size - 1
117
+ node = hash
118
+
119
+ while curr_idx <= last_idx
120
+ key = path[curr_idx]
121
+
122
+ next_node =
123
+ if node.is_a?(Array) && key.is_a?(Symbol)
124
+ node_hash = (node << [] << {}).last
125
+ node_hash[key] || (node_hash[key] = curr_idx < last_idx ? {} : [])
126
+ else
127
+ node[key] || (node[key] = curr_idx < last_idx ? {} : [])
128
+ end
129
+
130
+ node = next_node
131
+ curr_idx += 1
132
+ end
133
+ }
134
+ end
135
+ # rubocop:enable Metrics/AbcSize
136
+ # rubocop:enable Metrics/PerceivedComplexity
137
+ end
138
+ end
139
+ end
@@ -15,103 +15,128 @@ module Dry
15
15
  class Evaluator
16
16
  extend Dry::Initializer
17
17
 
18
- # @!attribute [r] _context
18
+ ROOT_PATH = [nil].freeze
19
+
20
+ # Failure accumulator object
21
+ #
22
+ # @api public
23
+ class Failures
24
+ # @api private
25
+ attr_reader :path
26
+
27
+ # @api private
28
+ attr_reader :opts
29
+
30
+ # @api private
31
+ def initialize(path = ROOT_PATH)
32
+ @path = Dry::Schema::Path[path]
33
+ @opts = []
34
+ end
35
+
36
+ # Set failure
37
+ #
38
+ # @overload failure(message)
39
+ # Set message text explicitly
40
+ # @param message [String] The message text
41
+ # @example
42
+ # failure('this failed')
43
+ #
44
+ # @overload failure(id)
45
+ # Use message identifier (needs localized messages setup)
46
+ # @param id [Symbol] The message id
47
+ # @example
48
+ # failure(:taken)
49
+ #
50
+ # @api public
51
+ def failure(message, tokens = EMPTY_HASH)
52
+ @opts << { message: message, tokens: tokens, path: path }
53
+ self
54
+ end
55
+ end
56
+
57
+ # @!attribute [r] _contract
19
58
  # @return [Contract]
20
59
  # @api private
21
- param :_context
60
+ param :_contract
22
61
 
23
62
  # @!attribute [r] keys
24
63
  # @return [Array<String, Symbol, Hash>]
25
64
  # @api private
26
65
  option :keys
27
66
 
28
- # @!attribute [r] default_id
29
- # @return [String, Symbol, Hash]
67
+ # @!attribute [r] _context
68
+ # @return [Concurrent::Map]
69
+ # @api public
70
+ option :_context
71
+
72
+ # @!attribute [r] path
73
+ # @return [Dry::Schema::Path]
30
74
  # @api private
31
- option :default_id, default: proc { keys.first }
75
+ option :path, default: proc { Dry::Schema::Path[(key = keys.first) ? key : ROOT_PATH] }
32
76
 
33
77
  # @!attribute [r] values
34
78
  # @return [Object]
35
79
  # @api private
36
80
  option :values
37
81
 
38
- # @!attribute [r] message
39
- # @return [String]
40
- # @api private
41
- attr_reader :message
42
-
43
82
  # Initialize a new evaluator
44
83
  #
45
84
  # @api private
46
85
  def initialize(*args, &block)
47
86
  super(*args)
48
- @failure = false
49
- instance_eval(&block)
87
+ instance_exec(_context, &block)
50
88
  end
51
89
 
52
- # Set failure message
90
+ # Get failures object for the default or provided path
53
91
  #
54
- # @overload failure(message)
55
- # Set message text explicitly
56
- # @param message [String] The message text
57
- # @example
58
- # failure('this failed')
92
+ # @param [Symbol,String,Hash,Array<Symbol>] path
59
93
  #
60
- # @overload failure(id)
61
- # Use message identifier (needs localized messages setup)
62
- # @param id [Symbol] The message id
63
- # @example
64
- # failure(:taken)
94
+ # @return [Failures]
95
+ #
96
+ # @see Failures#failure
97
+ #
98
+ # @api public
99
+ def key(path = self.path)
100
+ (@key ||= EMPTY_HASH.dup)[path] ||= Failures.new(path)
101
+ end
102
+
103
+ # Get failures object for base errors
65
104
  #
66
- # @overload failure(key, message)
67
- # Set message under specified key (overrides rule's default key)
68
- # @param id [Symbol] The message key
69
- # @example
70
- # failure(:my_error, 'this failed')
105
+ # @return [Failures]
71
106
  #
72
- # @return [Evaluator]
107
+ # @see Failures#failure
73
108
  #
74
109
  # @api public
75
- def failure(*args, **tokens)
76
- id, text =
77
- if args.size.equal?(1)
78
- case (msg = args[0])
79
- when Symbol
80
- [default_id, _context.message(msg, rule: default_id, tokens: tokens)]
81
- when String
82
- [default_id, msg]
83
- end
84
- else
85
- args
86
- end
87
- @failure = true
88
- @message = [id, text]
89
- self
110
+ def base
111
+ @base ||= Failures.new
90
112
  end
91
113
 
92
- # Check if evaluation resulted in a failure message
114
+ # Return aggregated failures
93
115
  #
94
- # @return [Boolean]
116
+ # @return [Array<Hash>]
95
117
  #
96
118
  # @api private
97
- def failure?
98
- @failure.equal?(true)
119
+ def failures
120
+ failures = []
121
+ failures += @base.opts if defined?(@base)
122
+ failures.concat(@key.values.flat_map(&:opts)) if defined?(@key)
123
+ failures
99
124
  end
100
125
 
101
126
  # @api private
102
127
  def respond_to_missing?(meth, include_private = false)
103
- super || _context.respond_to?(meth, true)
128
+ super || _contract.respond_to?(meth, true)
104
129
  end
105
130
 
106
131
  private
107
132
 
108
- # Forward to the underlying context
133
+ # Forward to the underlying contract
109
134
  #
110
135
  # @api private
111
136
  def method_missing(meth, *args, &block)
112
137
  # yes, we do want to delegate to private methods too
113
- if _context.respond_to?(meth, true)
114
- _context.__send__(meth, *args, &block)
138
+ if _contract.respond_to?(meth, true)
139
+ _contract.__send__(meth, *args, &block)
115
140
  else
116
141
  super
117
142
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Validation
5
+ module Messages
6
+ # Resolve translated messages from failure arguments
7
+ #
8
+ # @api public
9
+ class Resolver
10
+ # @!attribute [r] messages
11
+ # @return [Messages::I18n, Messages::YAML] messages backend
12
+ # @api private
13
+ attr_reader :messages
14
+
15
+ # @!attribute [r] locale
16
+ # @return [Symbol] current locale
17
+ # @api private
18
+ attr_reader :locale
19
+
20
+ # @api private
21
+ def initialize(messages, locale = :en)
22
+ @messages = messages
23
+ @locale = locale
24
+ end
25
+
26
+ # Resolve Error object from provided args and path
27
+ #
28
+ # This is used internally by contracts when rules are applied
29
+ #
30
+ # @return [Error, Error::Localized]
31
+ #
32
+ # @api public
33
+ def call(message:, tokens:, path:)
34
+ case message
35
+ when Symbol
36
+ Error[->(**opts) { message(message, path: path, tokens: tokens, **opts) }, path]
37
+ when String
38
+ Error[message, path]
39
+ end
40
+ end
41
+ alias_method :[], :call
42
+
43
+ # Resolve a message
44
+ #
45
+ # @return [String]
46
+ #
47
+ # @api public
48
+ def message(rule, tokens: EMPTY_HASH, path:, locale: self.locale, full: false)
49
+ keys = path.to_a.compact
50
+ msg_opts = tokens.merge(path: keys, locale: locale)
51
+
52
+ if keys.empty?
53
+ template = messages["rules.#{rule}", msg_opts]
54
+ else
55
+ template = messages[rule, msg_opts.merge(path: keys.join(DOT))]
56
+ template ||= messages[rule, msg_opts.merge(path: keys.last)]
57
+ end
58
+
59
+ unless template
60
+ raise MissingMessageError, <<~STR
61
+ Message template for #{rule.inspect} under #{keys.join(DOT).inspect} was not found
62
+ STR
63
+ end
64
+
65
+ text = template.(template.data(tokens))
66
+
67
+ full ? "#{messages.rule(keys.last, msg_opts)} #{text}" : text
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'dry/equalizer'
4
+
4
5
  require 'dry/validation/constants'
6
+ require 'dry/validation/error_set'
5
7
 
6
8
  module Dry
7
9
  module Validation
@@ -13,8 +15,10 @@ module Dry
13
15
 
14
16
  # Build a new result
15
17
  #
18
+ # @param [Dry::Schema::Result]
19
+ #
16
20
  # @api private
17
- def self.new(params, errors = EMPTY_HASH.dup)
21
+ def self.new(values, options = EMPTY_HASH)
18
22
  result = super
19
23
  yield(result) if block_given?
20
24
  result.freeze
@@ -25,25 +29,29 @@ module Dry
25
29
  # @api private
26
30
  attr_reader :values
27
31
 
28
- # @!attribute [r] errors
29
- # @return [Hash<Symbol=>Array<String>>]
30
- # @api public
31
- attr_reader :errors
32
+ # @!attribute [r] options
33
+ # @return [Hash]
34
+ # @api private
35
+ attr_reader :options
32
36
 
33
37
  # Initialize a new result
34
38
  #
35
39
  # @api private
36
- def initialize(values, errors)
40
+ def initialize(values, options)
37
41
  @values = values
38
- @errors = errors.update(values.errors)
42
+ @options = options
43
+ @errors = initialize_errors
39
44
  end
40
45
 
41
- # Return all messages including hints from schema
46
+ # Get error set
47
+ #
48
+ # @return [ErrorSet]
42
49
  #
43
50
  # @api public
44
- def messages
45
- values.messages
51
+ def errors(new_options = EMPTY_HASH)
52
+ new_options.empty? ? @errors : @errors.with(schema_errors(new_options), new_options)
46
53
  end
54
+ alias_method :messages, :errors
47
55
 
48
56
  # Check if result is successful
49
57
  #
@@ -51,7 +59,7 @@ module Dry
51
59
  #
52
60
  # @api public
53
61
  def success?
54
- errors.empty?
62
+ @errors.empty?
55
63
  end
56
64
 
57
65
  # Check if result is not successful
@@ -73,8 +81,8 @@ module Dry
73
81
  # Add a new error for the provided key
74
82
  #
75
83
  # @api private
76
- def add_error(key, message)
77
- add_to(errors, key, message)
84
+ def add_error(error)
85
+ errors.add(error)
78
86
  self
79
87
  end
80
88
 
@@ -86,27 +94,7 @@ module Dry
86
94
  #
87
95
  # @api public
88
96
  def [](key)
89
- if values.key?(key)
90
- values[key]
91
- elsif storage.key?(key)
92
- storage[key]
93
- else
94
- nil
95
- end
96
- end
97
-
98
- # Store value under specified key
99
- #
100
- # @param [Symbol] key
101
- # @param [Object] value
102
- #
103
- # @return [Object]
104
- #
105
- # @api public
106
- def []=(key, value)
107
- raise ArgumentError, "Key +#{key}+ was already set" if key?(key)
108
-
109
- storage[key] = value
97
+ values[key]
110
98
  end
111
99
 
112
100
  # Check if a key was set
@@ -117,7 +105,7 @@ module Dry
117
105
  #
118
106
  # @api public
119
107
  def key?(key)
120
- values.key?(key) || storage.key?(key)
108
+ values.key?(key)
121
109
  end
122
110
 
123
111
  # Coerce to a hash
@@ -126,38 +114,32 @@ module Dry
126
114
  def to_h
127
115
  values.to_h
128
116
  end
129
- alias_method :to_hash, :to_h
130
-
131
- # Add new errors
132
- #
133
- # @api private
134
- def update(new_errors)
135
- errors.update(new_errors)
136
- end
137
117
 
138
118
  # Return a string representation
139
119
  #
140
120
  # @api public
141
121
  def inspect
142
- "#<#{self.class}#{to_h.inspect} errors=#{errors.inspect}>"
122
+ "#<#{self.class}#{to_h.inspect} errors=#{errors.to_h.inspect}>"
123
+ end
124
+
125
+ # Freeze result and its error set
126
+ #
127
+ # @api private
128
+ def freeze
129
+ errors.freeze
130
+ super
143
131
  end
144
132
 
145
133
  private
146
134
 
147
135
  # @api private
148
- def storage
149
- @storage ||= EMPTY_HASH.dup
136
+ def initialize_errors(options = self.options)
137
+ ErrorSet.new(schema_errors(options), options)
150
138
  end
151
139
 
152
140
  # @api private
153
- def add_to(hash, path, value)
154
- if path.is_a?(Hash)
155
- key = path.keys[0]
156
- messages = (hash[key] ||= EMPTY_HASH.dup)
157
- add_to(messages, path[key], value)
158
- else
159
- (hash[path] ||= EMPTY_ARRAY.dup) << value
160
- end
141
+ def schema_errors(options)
142
+ values.message_set(options).to_a
161
143
  end
162
144
  end
163
145
  end
@@ -25,12 +25,13 @@ module Dry
25
25
 
26
26
  # Evaluate the rule within the provided context
27
27
  #
28
- # @param [Contract] context
29
- # @param [Object] values
28
+ # @param [Contract] contract
29
+ # @param [Result] result
30
+ # @param [Concurrent::Map] context
30
31
  #
31
32
  # @api private
32
- def call(context, values)
33
- Evaluator.new(context, values: values, keys: keys, &block)
33
+ def call(contract, result, context)
34
+ Evaluator.new(contract, values: result, keys: keys, _context: context, &block)
34
35
  end
35
36
  end
36
37
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dry
4
4
  module Validation
5
- VERSION = '1.0.0.alpha2'
5
+ VERSION = '1.0.0.beta1'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-validation
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.alpha2
4
+ version: 1.0.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-05 00:00:00.000000000 Z
11
+ date: 2019-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -24,26 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
- - !ruby/object:Gem::Dependency
28
- name: dry-configurable
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '0.1'
34
- - - ">="
35
- - !ruby/object:Gem::Version
36
- version: 0.1.3
37
- type: :runtime
38
- prerelease: false
39
- version_requirements: !ruby/object:Gem::Requirement
40
- requirements:
41
- - - "~>"
42
- - !ruby/object:Gem::Version
43
- version: '0.1'
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: 0.1.3
47
27
  - !ruby/object:Gem::Dependency
48
28
  name: dry-core
49
29
  requirement: !ruby/object:Gem::Requirement
@@ -96,22 +76,22 @@ dependencies:
96
76
  name: dry-schema
97
77
  requirement: !ruby/object:Gem::Requirement
98
78
  requirements:
99
- - - ">="
100
- - !ruby/object:Gem::Version
101
- version: '0.3'
102
79
  - - "~>"
103
80
  - !ruby/object:Gem::Version
104
81
  version: '0.3'
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0.4'
105
85
  type: :runtime
106
86
  prerelease: false
107
87
  version_requirements: !ruby/object:Gem::Requirement
108
88
  requirements:
109
- - - ">="
110
- - !ruby/object:Gem::Version
111
- version: '0.3'
112
89
  - - "~>"
113
90
  - !ruby/object:Gem::Version
114
91
  version: '0.3'
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0.4'
115
95
  - !ruby/object:Gem::Dependency
116
96
  name: bundler
117
97
  requirement: !ruby/object:Gem::Requirement
@@ -166,12 +146,15 @@ files:
166
146
  - README.md
167
147
  - lib/dry-validation.rb
168
148
  - lib/dry/validation.rb
149
+ - lib/dry/validation/config.rb
169
150
  - lib/dry/validation/constants.rb
170
151
  - lib/dry/validation/contract.rb
171
152
  - lib/dry/validation/contract/class_interface.rb
153
+ - lib/dry/validation/error.rb
154
+ - lib/dry/validation/error_set.rb
172
155
  - lib/dry/validation/evaluator.rb
173
156
  - lib/dry/validation/extensions/monads.rb
174
- - lib/dry/validation/messages.rb
157
+ - lib/dry/validation/messages/resolver.rb
175
158
  - lib/dry/validation/result.rb
176
159
  - lib/dry/validation/rule.rb
177
160
  - lib/dry/validation/version.rb
@@ -194,7 +177,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
194
177
  - !ruby/object:Gem::Version
195
178
  version: 1.3.1
196
179
  requirements: []
197
- rubygems_version: 3.0.1
180
+ rubygems_version: 3.0.3
198
181
  signing_key:
199
182
  specification_version: 4
200
183
  summary: Validation library
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'dry/schema/messages'
4
-
5
- module Dry
6
- module Validation
7
- module Messages
8
- class YAML < Schema::Messages::YAML
9
- config.root = config.root.gsub('dry_schema', 'dry_validation')
10
- config.rule_lookup_paths = config.rule_lookup_paths.map { |path|
11
- path.gsub('dry_schema', 'dry_validation')
12
- }
13
- end
14
-
15
- if defined?(::I18n)
16
- class I18n < Schema::Messages::I18n
17
- config.root = config.root.gsub('dry_schema', 'dry_validation')
18
- config.rule_lookup_paths = config.rule_lookup_paths.map { |path|
19
- path.gsub('dry_schema', 'dry_validation')
20
- }
21
- end
22
- end
23
-
24
- # @api private
25
- def self.setup(config)
26
- messages = build(config)
27
-
28
- if config.messages_file && config.namespace
29
- messages.merge(config.messages_file).namespaced(config.namespace)
30
- elsif config.messages_file
31
- messages.merge(config.messages_file)
32
- elsif config.namespace
33
- messages.namespaced(config.namespace)
34
- else
35
- messages
36
- end
37
- end
38
-
39
- # @api private
40
- def self.build(config)
41
- klass =
42
- case config.messages
43
- when :yaml then default
44
- when :i18n then Messages::I18n
45
- else
46
- raise "+#{config.messages}+ is not a valid messages identifier"
47
- end
48
-
49
- klass.build
50
- end
51
-
52
- # @api private
53
- def self.default
54
- Messages::YAML
55
- end
56
- end
57
- end
58
- end