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.
Files changed (82) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +969 -1
  3. data/LICENSE +1 -1
  4. data/README.md +19 -286
  5. data/config/errors.yml +4 -35
  6. data/dry-validation.gemspec +38 -22
  7. data/lib/dry/validation/config.rb +24 -0
  8. data/lib/dry/validation/constants.rb +43 -0
  9. data/lib/dry/validation/contract/class_interface.rb +230 -0
  10. data/lib/dry/validation/contract.rb +173 -0
  11. data/lib/dry/validation/evaluator.rb +233 -0
  12. data/lib/dry/validation/extensions/hints.rb +67 -0
  13. data/lib/dry/validation/extensions/monads.rb +34 -0
  14. data/lib/dry/validation/extensions/predicates_as_macros.rb +75 -0
  15. data/lib/dry/validation/failures.rb +70 -0
  16. data/lib/dry/validation/function.rb +43 -0
  17. data/lib/dry/validation/macro.rb +38 -0
  18. data/lib/dry/validation/macros.rb +104 -0
  19. data/lib/dry/validation/message.rb +100 -0
  20. data/lib/dry/validation/message_set.rb +97 -0
  21. data/lib/dry/validation/messages/resolver.rb +129 -0
  22. data/lib/dry/validation/result.rb +206 -38
  23. data/lib/dry/validation/rule.rb +116 -106
  24. data/lib/dry/validation/schema_ext.rb +19 -0
  25. data/lib/dry/validation/values.rb +108 -0
  26. data/lib/dry/validation/version.rb +3 -1
  27. data/lib/dry/validation.rb +55 -7
  28. data/lib/dry-validation.rb +3 -1
  29. metadata +80 -106
  30. data/.gitignore +0 -8
  31. data/.rspec +0 -3
  32. data/.rubocop.yml +0 -16
  33. data/.rubocop_todo.yml +0 -7
  34. data/.travis.yml +0 -29
  35. data/Gemfile +0 -11
  36. data/Rakefile +0 -12
  37. data/examples/basic.rb +0 -21
  38. data/examples/nested.rb +0 -30
  39. data/examples/rule_ast.rb +0 -33
  40. data/lib/dry/validation/error.rb +0 -43
  41. data/lib/dry/validation/error_compiler.rb +0 -116
  42. data/lib/dry/validation/messages.rb +0 -71
  43. data/lib/dry/validation/predicate.rb +0 -39
  44. data/lib/dry/validation/predicate_set.rb +0 -22
  45. data/lib/dry/validation/predicates.rb +0 -88
  46. data/lib/dry/validation/rule_compiler.rb +0 -57
  47. data/lib/dry/validation/schema/definition.rb +0 -15
  48. data/lib/dry/validation/schema/key.rb +0 -39
  49. data/lib/dry/validation/schema/rule.rb +0 -28
  50. data/lib/dry/validation/schema/value.rb +0 -31
  51. data/lib/dry/validation/schema.rb +0 -74
  52. data/rakelib/rubocop.rake +0 -18
  53. data/spec/fixtures/errors.yml +0 -4
  54. data/spec/integration/custom_error_messages_spec.rb +0 -35
  55. data/spec/integration/custom_predicates_spec.rb +0 -57
  56. data/spec/integration/validation_spec.rb +0 -118
  57. data/spec/shared/predicates.rb +0 -31
  58. data/spec/spec_helper.rb +0 -18
  59. data/spec/unit/error_compiler_spec.rb +0 -165
  60. data/spec/unit/predicate_spec.rb +0 -37
  61. data/spec/unit/predicates/empty_spec.rb +0 -38
  62. data/spec/unit/predicates/eql_spec.rb +0 -21
  63. data/spec/unit/predicates/exclusion_spec.rb +0 -35
  64. data/spec/unit/predicates/filled_spec.rb +0 -38
  65. data/spec/unit/predicates/format_spec.rb +0 -21
  66. data/spec/unit/predicates/gt_spec.rb +0 -40
  67. data/spec/unit/predicates/gteq_spec.rb +0 -40
  68. data/spec/unit/predicates/inclusion_spec.rb +0 -35
  69. data/spec/unit/predicates/int_spec.rb +0 -34
  70. data/spec/unit/predicates/key_spec.rb +0 -29
  71. data/spec/unit/predicates/lt_spec.rb +0 -40
  72. data/spec/unit/predicates/lteq_spec.rb +0 -40
  73. data/spec/unit/predicates/max_size_spec.rb +0 -49
  74. data/spec/unit/predicates/min_size_spec.rb +0 -49
  75. data/spec/unit/predicates/nil_spec.rb +0 -28
  76. data/spec/unit/predicates/size_spec.rb +0 -49
  77. data/spec/unit/predicates/str_spec.rb +0 -32
  78. data/spec/unit/rule/each_spec.rb +0 -20
  79. data/spec/unit/rule/key_spec.rb +0 -27
  80. data/spec/unit/rule/set_spec.rb +0 -32
  81. data/spec/unit/rule/value_spec.rb +0 -42
  82. 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