dry-validation 0.1.0 → 1.8.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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +969 -1
- data/LICENSE +1 -1
- data/README.md +19 -286
- data/config/errors.yml +4 -35
- data/dry-validation.gemspec +38 -22
- data/lib/dry/validation/config.rb +24 -0
- data/lib/dry/validation/constants.rb +43 -0
- data/lib/dry/validation/contract/class_interface.rb +230 -0
- data/lib/dry/validation/contract.rb +173 -0
- data/lib/dry/validation/evaluator.rb +233 -0
- data/lib/dry/validation/extensions/hints.rb +67 -0
- data/lib/dry/validation/extensions/monads.rb +34 -0
- data/lib/dry/validation/extensions/predicates_as_macros.rb +75 -0
- data/lib/dry/validation/failures.rb +70 -0
- data/lib/dry/validation/function.rb +43 -0
- data/lib/dry/validation/macro.rb +38 -0
- data/lib/dry/validation/macros.rb +104 -0
- data/lib/dry/validation/message.rb +100 -0
- data/lib/dry/validation/message_set.rb +97 -0
- data/lib/dry/validation/messages/resolver.rb +129 -0
- data/lib/dry/validation/result.rb +206 -38
- data/lib/dry/validation/rule.rb +116 -106
- data/lib/dry/validation/schema_ext.rb +19 -0
- data/lib/dry/validation/values.rb +108 -0
- data/lib/dry/validation/version.rb +3 -1
- data/lib/dry/validation.rb +55 -7
- data/lib/dry-validation.rb +3 -1
- metadata +80 -106
- data/.gitignore +0 -8
- data/.rspec +0 -3
- data/.rubocop.yml +0 -16
- data/.rubocop_todo.yml +0 -7
- data/.travis.yml +0 -29
- data/Gemfile +0 -11
- data/Rakefile +0 -12
- data/examples/basic.rb +0 -21
- data/examples/nested.rb +0 -30
- data/examples/rule_ast.rb +0 -33
- data/lib/dry/validation/error.rb +0 -43
- data/lib/dry/validation/error_compiler.rb +0 -116
- data/lib/dry/validation/messages.rb +0 -71
- data/lib/dry/validation/predicate.rb +0 -39
- data/lib/dry/validation/predicate_set.rb +0 -22
- data/lib/dry/validation/predicates.rb +0 -88
- data/lib/dry/validation/rule_compiler.rb +0 -57
- data/lib/dry/validation/schema/definition.rb +0 -15
- data/lib/dry/validation/schema/key.rb +0 -39
- data/lib/dry/validation/schema/rule.rb +0 -28
- data/lib/dry/validation/schema/value.rb +0 -31
- data/lib/dry/validation/schema.rb +0 -74
- data/rakelib/rubocop.rake +0 -18
- data/spec/fixtures/errors.yml +0 -4
- data/spec/integration/custom_error_messages_spec.rb +0 -35
- data/spec/integration/custom_predicates_spec.rb +0 -57
- data/spec/integration/validation_spec.rb +0 -118
- data/spec/shared/predicates.rb +0 -31
- data/spec/spec_helper.rb +0 -18
- data/spec/unit/error_compiler_spec.rb +0 -165
- data/spec/unit/predicate_spec.rb +0 -37
- data/spec/unit/predicates/empty_spec.rb +0 -38
- data/spec/unit/predicates/eql_spec.rb +0 -21
- data/spec/unit/predicates/exclusion_spec.rb +0 -35
- data/spec/unit/predicates/filled_spec.rb +0 -38
- data/spec/unit/predicates/format_spec.rb +0 -21
- data/spec/unit/predicates/gt_spec.rb +0 -40
- data/spec/unit/predicates/gteq_spec.rb +0 -40
- data/spec/unit/predicates/inclusion_spec.rb +0 -35
- data/spec/unit/predicates/int_spec.rb +0 -34
- data/spec/unit/predicates/key_spec.rb +0 -29
- data/spec/unit/predicates/lt_spec.rb +0 -40
- data/spec/unit/predicates/lteq_spec.rb +0 -40
- data/spec/unit/predicates/max_size_spec.rb +0 -49
- data/spec/unit/predicates/min_size_spec.rb +0 -49
- data/spec/unit/predicates/nil_spec.rb +0 -28
- data/spec/unit/predicates/size_spec.rb +0 -49
- data/spec/unit/predicates/str_spec.rb +0 -32
- data/spec/unit/rule/each_spec.rb +0 -20
- data/spec/unit/rule/key_spec.rb +0 -27
- data/spec/unit/rule/set_spec.rb +0 -32
- data/spec/unit/rule/value_spec.rb +0 -42
- data/spec/unit/rule_compiler_spec.rb +0 -86
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/monads/result"
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Validation
|
7
|
+
# Monad extension for contract results
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# Dry::Validation.load_extensions(:monads)
|
11
|
+
#
|
12
|
+
# contract = Dry::Validation::Contract.build do
|
13
|
+
# schema do
|
14
|
+
# required(:name).filled(:string)
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# contract.call(name: nil).to_monad
|
19
|
+
#
|
20
|
+
# @api public
|
21
|
+
class Result
|
22
|
+
include Dry::Monads::Result::Mixin
|
23
|
+
|
24
|
+
# Returns a result monad
|
25
|
+
#
|
26
|
+
# @return [Dry::Monads::Result]
|
27
|
+
#
|
28
|
+
# @api public
|
29
|
+
def to_monad
|
30
|
+
success? ? Success(self) : Failure(self)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/schema/predicate_registry"
|
4
|
+
require "dry/validation/contract"
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Validation
|
8
|
+
# Predicate registry with additional needed methods.
|
9
|
+
class PredicateRegistry < Schema::PredicateRegistry
|
10
|
+
# List of predicates to be imported by `:predicates_as_macros`
|
11
|
+
# extension.
|
12
|
+
#
|
13
|
+
# @see Dry::Validation::Contract
|
14
|
+
WHITELIST = %i[
|
15
|
+
filled? format? gt? gteq? included_in? includes? inclusion? is? lt?
|
16
|
+
lteq? max_size? min_size? not_eql? odd? respond_to? size? true?
|
17
|
+
uuid_v4?
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
# @api private
|
21
|
+
def arg_names(name)
|
22
|
+
arg_list(name).map(&:first)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @api private
|
26
|
+
def call(name, args)
|
27
|
+
self[name].(*args)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @api private
|
31
|
+
def message_opts(name, arg_values)
|
32
|
+
arg_names(name).zip(arg_values).to_h
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Extension to use dry-logic predicates as macros.
|
37
|
+
#
|
38
|
+
# @see Dry::Validation::PredicateRegistry::WHITELIST Available predicates
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# Dry::Validation.load_extensions(:predicates_as_macros)
|
42
|
+
#
|
43
|
+
# class ApplicationContract < Dry::Validation::Contract
|
44
|
+
# import_predicates_as_macros
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# class AgeContract < ApplicationContract
|
48
|
+
# schema do
|
49
|
+
# required(:age).filled(:integer)
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# rule(:age).validate(gteq?: 18)
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# AgeContract.new.(age: 17).errors.first.text
|
56
|
+
# # => 'must be greater than or equal to 18'
|
57
|
+
#
|
58
|
+
# @api public
|
59
|
+
class Contract
|
60
|
+
# Make macros available for self and its descendants.
|
61
|
+
def self.import_predicates_as_macros
|
62
|
+
registry = PredicateRegistry.new
|
63
|
+
|
64
|
+
PredicateRegistry::WHITELIST.each do |name|
|
65
|
+
register_macro(name) do |macro:|
|
66
|
+
predicate_args = [*macro.args, value]
|
67
|
+
message_opts = registry.message_opts(name, predicate_args)
|
68
|
+
|
69
|
+
key.failure(name, message_opts) unless registry.(name, predicate_args)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/schema/path"
|
4
|
+
require "dry/validation/constants"
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Validation
|
8
|
+
# Failure accumulator object
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Failures
|
12
|
+
# The path for messages accumulated by failures object
|
13
|
+
#
|
14
|
+
# @return [Dry::Schema::Path]
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
attr_reader :path
|
18
|
+
|
19
|
+
# Options for messages
|
20
|
+
#
|
21
|
+
# These options are used by MessageResolver
|
22
|
+
#
|
23
|
+
# @return [Hash]
|
24
|
+
#
|
25
|
+
# @api private
|
26
|
+
attr_reader :opts
|
27
|
+
|
28
|
+
# @api private
|
29
|
+
def initialize(path = ROOT_PATH)
|
30
|
+
@path = Dry::Schema::Path[path]
|
31
|
+
@opts = EMPTY_ARRAY.dup
|
32
|
+
end
|
33
|
+
|
34
|
+
# Set failure
|
35
|
+
#
|
36
|
+
# @overload failure(message)
|
37
|
+
# Set message text explicitly
|
38
|
+
# @param message [String] The message text
|
39
|
+
# @example
|
40
|
+
# failure('this failed')
|
41
|
+
#
|
42
|
+
# @overload failure(id)
|
43
|
+
# Use message identifier (needs localized messages setup)
|
44
|
+
# @param id [Symbol] The message id
|
45
|
+
# @example
|
46
|
+
# failure(:taken)
|
47
|
+
#
|
48
|
+
# @overload failure(meta_hash)
|
49
|
+
# Use meta_hash[:text] as a message (either explicitely or as an identifier),
|
50
|
+
# setting the rest of the hash as error meta attribute
|
51
|
+
# @param meta [Hash] The hash containing the message as value for the :text key
|
52
|
+
# @example
|
53
|
+
# failure({text: :invalid, key: value})
|
54
|
+
#
|
55
|
+
# @see Evaluator#key
|
56
|
+
# @see Evaluator#base
|
57
|
+
#
|
58
|
+
# @api public
|
59
|
+
def failure(message, tokens = EMPTY_HASH)
|
60
|
+
opts << {message: message, tokens: tokens, path: path}
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
# @api private
|
65
|
+
def empty?
|
66
|
+
opts.empty?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/initializer"
|
4
|
+
require "dry/validation/constants"
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Validation
|
8
|
+
# Abstract class for handling rule blocks
|
9
|
+
#
|
10
|
+
# @see Rule
|
11
|
+
# @see Macro
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
class Function
|
15
|
+
extend Dry::Initializer
|
16
|
+
|
17
|
+
# @!attribute [r] block
|
18
|
+
# @return [Proc]
|
19
|
+
# @api private
|
20
|
+
option :block
|
21
|
+
|
22
|
+
# @!attribute [r] block_options
|
23
|
+
# @return [Hash]
|
24
|
+
# @api private
|
25
|
+
option :block_options, default: -> { block ? map_keywords(block) : EMPTY_HASH }
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Extract options for the block kwargs
|
30
|
+
#
|
31
|
+
# @param [Proc] block Callable
|
32
|
+
# @return Hash
|
33
|
+
#
|
34
|
+
# @api private
|
35
|
+
def map_keywords(block)
|
36
|
+
block
|
37
|
+
.parameters
|
38
|
+
.select { |arg,| arg.equal?(:keyreq) }
|
39
|
+
.to_h { [_2, BLOCK_OPTIONS_MAPPINGS[_2]] }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/validation/constants"
|
4
|
+
require "dry/validation/function"
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Validation
|
8
|
+
# A wrapper for macro validation blocks
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Macro < Function
|
12
|
+
# @!attribute [r] name
|
13
|
+
# @return [Symbol]
|
14
|
+
# @api public
|
15
|
+
param :name
|
16
|
+
|
17
|
+
# @!attribute [r] args
|
18
|
+
# @return [Array]
|
19
|
+
# @api public
|
20
|
+
option :args
|
21
|
+
|
22
|
+
# @!attribute [r] block
|
23
|
+
# @return [Proc]
|
24
|
+
# @api private
|
25
|
+
option :block
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
def with(args)
|
29
|
+
self.class.new(name, args: args, block: block)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @api private
|
33
|
+
def extract_block_options(options)
|
34
|
+
block_options.transform_values { options[_1] }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/container"
|
4
|
+
require "dry/validation/macro"
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Validation
|
8
|
+
# API for registering and accessing Rule macros
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
module Macros
|
12
|
+
module Registrar
|
13
|
+
# Register a macro
|
14
|
+
#
|
15
|
+
# @example register a global macro
|
16
|
+
# Dry::Validation.register_macro(:even_numbers) do
|
17
|
+
# key.failure('all numbers must be even') unless values[key_name].all?(&:even?)
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# @example register a contract macro
|
21
|
+
# class MyContract < Dry::Validation::Contract
|
22
|
+
# register_macro(:even_numbers) do
|
23
|
+
# key.failure('all numbers must be even') unless values[key_name].all?(&:even?)
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# @param [Symbol] name The name of the macro
|
28
|
+
# @param [Array] args Optional default positional arguments for the macro
|
29
|
+
#
|
30
|
+
# @return [self]
|
31
|
+
#
|
32
|
+
# @see Macro
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
def register_macro(name, *args, &block)
|
36
|
+
macros.register(name, *args, &block)
|
37
|
+
self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Registry for macros
|
42
|
+
#
|
43
|
+
# @api public
|
44
|
+
class Container
|
45
|
+
include Dry::Container::Mixin
|
46
|
+
|
47
|
+
# Register a new macro
|
48
|
+
#
|
49
|
+
# @example in a contract class
|
50
|
+
# class MyContract < Dry::Validation::Contract
|
51
|
+
# register_macro(:even_numbers) do
|
52
|
+
# key.failure('all numbers must be even') unless values[key_name].all?(&:even?)
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# @param [Symbol] name The name of the macro
|
57
|
+
#
|
58
|
+
# @return [self]
|
59
|
+
#
|
60
|
+
# @api public
|
61
|
+
def register(name, *args, &block)
|
62
|
+
macro = Macro.new(name, args: args, block: block)
|
63
|
+
super(name, macro, call: false, &nil)
|
64
|
+
self
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return a registered macro
|
69
|
+
#
|
70
|
+
# @param [Symbol] name The name of the macro
|
71
|
+
#
|
72
|
+
# @return [Proc]
|
73
|
+
#
|
74
|
+
# @api public
|
75
|
+
def self.[](name)
|
76
|
+
container[name]
|
77
|
+
end
|
78
|
+
|
79
|
+
# Register a global macro
|
80
|
+
#
|
81
|
+
# @see Container#register
|
82
|
+
#
|
83
|
+
# @return [Macros]
|
84
|
+
#
|
85
|
+
# @api public
|
86
|
+
def self.register(name, *args, &block)
|
87
|
+
container.register(name, *args, &block)
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
# @api private
|
92
|
+
def self.container
|
93
|
+
@container ||= Container.new
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Acceptance macro
|
98
|
+
#
|
99
|
+
# @api public
|
100
|
+
Macros.register(:acceptance) do
|
101
|
+
key.failure(:acceptance, key: key_name) unless values[key_name].equal?(true)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/core/equalizer"
|
4
|
+
|
5
|
+
require "dry/schema/constants"
|
6
|
+
require "dry/schema/message"
|
7
|
+
|
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))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
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)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Initialize a new error object
|
71
|
+
#
|
72
|
+
# @api private
|
73
|
+
# rubocop: disable Lint/MissingSuper
|
74
|
+
def initialize(text, path:, meta: EMPTY_HASH)
|
75
|
+
@text = text
|
76
|
+
@path = Array(path)
|
77
|
+
@meta = meta
|
78
|
+
end
|
79
|
+
# rubocop: enable Lint/MissingSuper
|
80
|
+
|
81
|
+
# Check if this is a base error not associated with any key
|
82
|
+
#
|
83
|
+
# @return [Boolean]
|
84
|
+
#
|
85
|
+
# @api public
|
86
|
+
def base?
|
87
|
+
@base ||= path.compact.empty?
|
88
|
+
end
|
89
|
+
|
90
|
+
# Dump error to a string
|
91
|
+
#
|
92
|
+
# @return [String]
|
93
|
+
#
|
94
|
+
# @api public
|
95
|
+
def to_s
|
96
|
+
text
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/schema/message_set"
|
4
|
+
|
5
|
+
require "dry/validation/constants"
|
6
|
+
require "dry/validation/message"
|
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
|
29
|
+
def initialize(messages, options = EMPTY_HASH)
|
30
|
+
@locale = options[:locale]
|
31
|
+
@source_messages = options.fetch(:source) { messages.dup }
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
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
|
47
|
+
end
|
48
|
+
|
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
|
61
|
+
end
|
62
|
+
|
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)
|
83
|
+
end
|
84
|
+
|
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
|
91
|
+
end
|
92
|
+
to_h
|
93
|
+
self
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,129 @@
|
|
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
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
65
|
+
def message(rule, path:, tokens: EMPTY_HASH, locale: nil, full: false)
|
66
|
+
keys = path.to_a.compact
|
67
|
+
msg_opts = tokens.merge(path: keys, locale: locale || messages.default_locale)
|
68
|
+
|
69
|
+
if keys.empty?
|
70
|
+
template, meta = messages["rules.#{rule}", msg_opts]
|
71
|
+
else
|
72
|
+
template, meta = messages[rule, msg_opts.merge(path: keys.join(DOT))]
|
73
|
+
template, meta = messages[rule, msg_opts.merge(path: keys.last)] unless template
|
74
|
+
end
|
75
|
+
|
76
|
+
if !template && keys.size > 1
|
77
|
+
non_index_keys = keys.reject { |k| k.is_a?(Integer) }
|
78
|
+
template, meta = messages[rule, msg_opts.merge(path: non_index_keys.join(DOT))]
|
79
|
+
end
|
80
|
+
|
81
|
+
unless template
|
82
|
+
raise MissingMessageError, <<~STR
|
83
|
+
Message template for #{rule.inspect} under #{keys.join(DOT).inspect} was not found
|
84
|
+
STR
|
85
|
+
end
|
86
|
+
|
87
|
+
parsed_tokens = parse_tokens(tokens)
|
88
|
+
text = template.(template.data(parsed_tokens))
|
89
|
+
|
90
|
+
[message_text(text, path: path, locale: locale, full: full), meta]
|
91
|
+
end
|
92
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
93
|
+
# rubocop:enable Metrics/AbcSize
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def message_text(text, path:, locale: nil, full: false)
|
98
|
+
return text unless full
|
99
|
+
|
100
|
+
key = key_text(path: path, locale: locale)
|
101
|
+
|
102
|
+
[key, text].compact.join(FULL_MESSAGE_WHITESPACE[locale])
|
103
|
+
end
|
104
|
+
|
105
|
+
def key_text(path:, locale: nil)
|
106
|
+
locale ||= messages.default_locale
|
107
|
+
|
108
|
+
keys = path.to_a.compact
|
109
|
+
msg_opts = {path: keys, locale: locale}
|
110
|
+
|
111
|
+
messages.rule(keys.last, msg_opts) || keys.last
|
112
|
+
end
|
113
|
+
|
114
|
+
def parse_tokens(tokens)
|
115
|
+
tokens.transform_values { parse_token(_1) }
|
116
|
+
end
|
117
|
+
|
118
|
+
def parse_token(token)
|
119
|
+
case token
|
120
|
+
when Array
|
121
|
+
token.join(", ")
|
122
|
+
else
|
123
|
+
token
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|