dry-validation 0.1.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
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