sleeping_king_studios-tools 1.2.1 → 1.3.0.rc.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -0
  3. data/README.md +29 -1
  4. data/lib/sleeping_king_studios/tools/array_tools.rb +136 -59
  5. data/lib/sleeping_king_studios/tools/assertions/aggregator.rb +132 -0
  6. data/lib/sleeping_king_studios/tools/assertions/messages_strategy.rb +77 -0
  7. data/lib/sleeping_king_studios/tools/assertions.rb +329 -260
  8. data/lib/sleeping_king_studios/tools/base.rb +23 -0
  9. data/lib/sleeping_king_studios/tools/core_tools.rb +49 -18
  10. data/lib/sleeping_king_studios/tools/hash_tools.rb +109 -9
  11. data/lib/sleeping_king_studios/tools/integer_tools.rb +1 -1
  12. data/lib/sleeping_king_studios/tools/messages/registry.rb +149 -0
  13. data/lib/sleeping_king_studios/tools/messages/strategies/file_strategy.rb +81 -0
  14. data/lib/sleeping_king_studios/tools/messages/strategies/hash_strategy.rb +72 -0
  15. data/lib/sleeping_king_studios/tools/messages/strategies.rb +13 -0
  16. data/lib/sleeping_king_studios/tools/messages/strategy.rb +77 -0
  17. data/lib/sleeping_king_studios/tools/messages.rb +75 -0
  18. data/lib/sleeping_king_studios/tools/object_tools.rb +185 -49
  19. data/lib/sleeping_king_studios/tools/string_tools.rb +20 -5
  20. data/lib/sleeping_king_studios/tools/toolbelt.rb +76 -30
  21. data/lib/sleeping_king_studios/tools/toolbox/constant_map.rb +11 -9
  22. data/lib/sleeping_king_studios/tools/toolbox/inflector/rules.rb +7 -9
  23. data/lib/sleeping_king_studios/tools/toolbox/initializer.rb +63 -0
  24. data/lib/sleeping_king_studios/tools/toolbox/mixin.rb +5 -3
  25. data/lib/sleeping_king_studios/tools/toolbox/semantic_version.rb +11 -11
  26. data/lib/sleeping_king_studios/tools/toolbox.rb +2 -0
  27. data/lib/sleeping_king_studios/tools/undefined.rb +50 -0
  28. data/lib/sleeping_king_studios/tools/version.rb +37 -8
  29. data/lib/sleeping_king_studios/tools.rb +37 -1
  30. metadata +14 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6723d647d646ad1812b8e3ab3b290502d54bb39d8725e494f046291ff0fb6f6f
4
- data.tar.gz: e87fd6269f7b9db3bf6c19b029d0757edc43c70cae8118cf3b9df2373bb2a39a
3
+ metadata.gz: ddf9748934072d1e7acf8ae748029ed8d616c46fd7d0a502499f0b54bc4e5d1b
4
+ data.tar.gz: bae72b5b57afe8404d45aab67ee59a5e8643b3656820e915c535b934016ee0d3
5
5
  SHA512:
6
- metadata.gz: de56319af6a174d979e267a2de6589e4f129bee02e5fbcc47d89b011234a6e365c91bc336ae682247e812a98d0f735b594d655354ba89c1fd32f71dad983da7a
7
- data.tar.gz: e690eb9b13e6125c784881024d1731695903873791647c1cba77164eaff5eb2190e1ab790b0e18ad6d7430052faee3b172eb8e71b821a719b2d5aa4826ff2b7b
6
+ metadata.gz: 6de4c073168e77a204afc0a1b4cc7e873619fbb9796df24177b7c2125be120df29e22034a690da73f1998822e10667d473bebfcdfb4adcd4da0a6dbea3f796c5
7
+ data.tar.gz: 999e795763737b1f79be6d0884f162cf436d116c7319cd6d66d4934e3bbb2048249bf4d7e9c632379d2d2e5aa01525bf0ddcc52318e5d146153af7971fa945e6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,104 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Tools
6
+
7
+ Tools objects now define a circular `#toolbelt` reference.
8
+
9
+ #### ArrayTools
10
+
11
+ Added `ArrayTools#fetch`, which retrieves the value at the requested index or raises an `IndexError`.
12
+
13
+ Deprecated methods with native equivalents:
14
+
15
+ - Deprecated `#bisect` - use `Enumerable#partition` instead.
16
+ - Deprecated `#count_values` - use `Enumerable#tally` instead.
17
+ - Deprecated `#splice` - use `Array#[]=` with a range instead.
18
+
19
+ #### Assertions
20
+
21
+ Added the following methods to `Assertions`.
22
+
23
+ - `#assert_exclusion`
24
+ - `#assert_inclusion`
25
+ - `#assert_inherits_from` (aliased as `#assert_subclass`)
26
+ - `#validate_exclusion`
27
+ - `#validate_inclusion`
28
+ - `#validate_inherits_from` (aliased as `#validate_subclass`)
29
+
30
+ Updated `Assertions#assert_instance_of` to accept a `Module` as the expected value.
31
+
32
+ Assertions error messages now use `Messages`.
33
+
34
+ - `Assertions#error_message_for` can be called with an unscoped key, e.g. `error_message_for(:blank)`.
35
+ - Error messages can be overwritten by changing the messaging strategy for scope `'sleeping_king_studios.tools.assertions'`.
36
+
37
+ #### CoreTools
38
+
39
+ Updated `CoreTools` deprecations:
40
+
41
+ - Now supports passing `deprecate(message, caller:)` to override displayed backtrace.
42
+ - Initializing `CoreTools` with an invalid deprecation strategy now raises an `ArgumentError`.
43
+
44
+ Deprecated `#require_each` method.
45
+
46
+ #### HashTools
47
+
48
+ Added `HashTools#fetch`, which retrieves the value at the requested key or raises a `KeyError`.
49
+
50
+ #### Messages
51
+
52
+ Added `Messages` tool, which allows generating user-readable messages.
53
+
54
+ - Added `Messages::Registry` for defining scoped message strategies.
55
+ - Added `Messages::Strategy` and subclasses `FileStrategy` and `HashStrategy`.
56
+
57
+ #### ObjectTools
58
+
59
+ Added `#[]` support to `ObjectTools#dig`, allowing access through indexed data structures.
60
+
61
+ - Added `:indifferent_keys` option - if set, treats `String` and `Symbol` values as interchangeable when traversing a `Hash` using `ObjectTools#dig`.
62
+
63
+ Added `ObjectTools#fetch`, which tries to find the requested value via the named method call or by calling `#[]`.
64
+
65
+ Deprecated methods with native equivalents:
66
+
67
+ - Deprecated `#eigenclass` - use `Object#singleton_class` instead.
68
+ - Deprecated `#try` - use the safe access operator `&.` instead.
69
+
70
+ #### StringTools
71
+
72
+ Deprecated methods with native equivalents:
73
+
74
+ - Deprecated `#chain` - use `Object#then {}` instead.
75
+
76
+ Deprecated calling `#pluralize` with multiple arguments.
77
+
78
+ ### Toolbelt
79
+
80
+ Implemented `Toolbelt.global`, a thread-safe singleton instance with default configuration.
81
+
82
+ - Calling `Toolbelt.instance` now returns the singleton.
83
+
84
+ Tools objects are now defined lazily.
85
+
86
+ ### Toolbox
87
+
88
+ Added keyword support to `ConstantMap.new`.
89
+
90
+ - The value returned by `ConstantMap#to_h` is now immutable.
91
+
92
+ Corrected the scope of `Mixin#included` and `Mixin#prepended` to be `private`.
93
+
94
+ #### Initializer
95
+
96
+ Added `Toolbox::Initializer`, used for defining a one-time initialization for a library or project.
97
+
98
+ ### Undefined
99
+
100
+ Added `SleepingKingStudios::Tools::Undefined`, used to represent undefined method parameters.
101
+
3
102
  ## 1.2.1
4
103
 
5
104
  Added support for Ruby 4.0.
data/README.md CHANGED
@@ -11,7 +11,7 @@ A library of utility services and concerns to expand the functionality of core c
11
11
 
12
12
  ## About
13
13
 
14
- SleepingKingStudios::Tools is tested against MRI Ruby 3.1 through 3.4.
14
+ SleepingKingStudios::Tools is tested against MRI Ruby 3.2 through 4.0.
15
15
 
16
16
  ### Documentation
17
17
 
@@ -30,3 +30,31 @@ The canonical repository for this gem is on [GitHub](https://github.com/sleeping
30
30
  ### Code of Conduct
31
31
 
32
32
  Please note that the `SleepingKingStudios::Tools` project is released with a [Contributor Code of Conduct](https://github.com/sleepingkingstudios/sleeping_king_studios-tools/blob/master/CODE_OF_CONDUCT.md). By contributing to this project, you agree to abide by its terms.
33
+
34
+ ## Getting Started
35
+
36
+ Add the gem to your `Gemfile` or `gemspec`:
37
+
38
+ ```ruby
39
+ gem 'sleeping_king_studios-tools'
40
+ ```
41
+
42
+ Require `SleepingKingStudios::Tools` in your code:
43
+
44
+ ```ruby
45
+ require 'sleeping_king_studios/tools'
46
+ ```
47
+
48
+ To ensure that [message definitions](./tools/messages) are loaded, call the `SleepingKingStudios::Tools` initializer:
49
+
50
+ - In the [initializer](./initializers) for your project:
51
+
52
+ ```ruby
53
+ module Space
54
+ @initializer = SleepingKingStudios::Tools::Toolbox::Initializer.new do
55
+ SleepingKingStudios::Tools::Toolbox.initializer.call
56
+ end
57
+ end
58
+ ```
59
+
60
+ - Or, in the entry points of your application (such as a `bin` script or `spec_helper.rb`).
@@ -4,7 +4,7 @@ require 'sleeping_king_studios/tools'
4
4
 
5
5
  module SleepingKingStudios::Tools
6
6
  # Tools for working with array-like enumerable objects.
7
- class ArrayTools < SleepingKingStudios::Tools::Base
7
+ class ArrayTools < SleepingKingStudios::Tools::Base # rubocop:disable Metrics/ClassLength
8
8
  # Expected methods that an Array-like object should implement.
9
9
  ARRAY_METHODS = %i[[] count each].freeze
10
10
 
@@ -18,6 +18,7 @@ module SleepingKingStudios::Tools
18
18
  :count_values,
19
19
  :deep_dup,
20
20
  :deep_freeze,
21
+ :fetch,
21
22
  :humanize_list,
22
23
  :immutable?,
23
24
  :mutable?,
@@ -82,19 +83,19 @@ module SleepingKingStudios::Tools
82
83
  # #=> [0, 2, 4, 6, 8]
83
84
  # rejected
84
85
  # #=> [1, 3, 5, 7, 9]
85
- def bisect(ary)
86
+ #
87
+ # @deprecated v1.3.0 Use Enumerable#partition instead.
88
+ def bisect(ary, &)
89
+ toolbelt.core_tools.deprecate(
90
+ "#{self.class.name}#bisect",
91
+ message: 'Use Enumerable#partition instead.'
92
+ )
93
+
86
94
  require_array!(ary)
87
95
 
88
96
  raise ArgumentError, 'no block given' unless block_given?
89
97
 
90
- selected = []
91
- rejected = []
92
-
93
- ary.each do |item|
94
- (yield(item) ? selected : rejected) << item
95
- end
96
-
97
- [selected, rejected]
98
+ ary.each.partition(&)
98
99
  end
99
100
 
100
101
  # Counts the number of times each item or result appears in the object.
@@ -130,18 +131,21 @@ module SleepingKingStudios::Tools
130
131
  # @example
131
132
  # ArrayTools.count_values([1, 1, 1, 2, 2, 3]) { |i| i ** 2 }
132
133
  # #=> { 1 => 3, 4 => 2, 9 => 1 }
133
- def count_values(ary, &block)
134
- require_array!(ary)
134
+ #
135
+ # @deprecated v1.3.0 Use Enumerable#tally instead.
136
+ def count_values(ary, &)
137
+ toolbelt.core_tools.deprecate(
138
+ "#{self.class.name}#count_values",
139
+ message: 'Use Enumerable#tally instead.'
140
+ )
135
141
 
136
- ary.each.with_object({}) do |item, hsh|
137
- value = block_given? ? block.call(item) : item
142
+ require_array!(ary)
138
143
 
139
- hsh[value] = hsh.fetch(value, 0) + 1
140
- end
144
+ (block_given? ? ary.map(&) : ary.to_a).tally
141
145
  end
142
146
  alias tally count_values
143
147
 
144
- # Creates a deep copy of the object.
148
+ # Creates a deep copy of the array and its contents.
145
149
  #
146
150
  # Iterates over the array and returns a new Array with deep copies of each
147
151
  # array item.
@@ -171,7 +175,7 @@ module SleepingKingStudios::Tools
171
175
  def deep_dup(ary)
172
176
  require_array!(ary)
173
177
 
174
- ary.map { |obj| ObjectTools.deep_dup obj }
178
+ ary.map { |obj| toolbelt.object_tools.deep_dup obj }
175
179
  end
176
180
 
177
181
  # Freezes the array and performs a deep freeze on each array item.
@@ -197,52 +201,110 @@ module SleepingKingStudios::Tools
197
201
 
198
202
  ary.freeze
199
203
 
200
- ary.each { |obj| ObjectTools.deep_freeze obj }
204
+ ary.each { |obj| toolbelt.object_tools.deep_freeze obj }
201
205
  end
202
206
 
203
- # Generates a human-readable string representation of the list items.
207
+ # @overload fetch(ary, index, default = nil)
208
+ # Retrieves the value at the specified index.
204
209
  #
205
- # Accepts a list of values and returns a human-readable string of the
206
- # values, with the format based on the number of items.
210
+ # If the value does not exist, returns the default value, or raises an
211
+ # IndexError if there is no default value. If the object defines a native
212
+ # #fetch method, delegates to the native implementation.
207
213
  #
208
- # @param ary [Array<String>] the list of values to format. Will be
209
- # coerced to strings using #to_s.
210
- # @param options [Hash] optional configuration hash.
211
- # @option options [String] :last_separator the value to use to separate
212
- # the final pair of values. Defaults to " and " (note the leading and
213
- # trailing spaces). Will be combined with the :separator for lists of
214
- # length 3 or greater.
215
- # @option options [String] :separator the value to use to separate pairs
216
- # of values before the last in lists of length 3 or greater. Defaults to
217
- # ", " (note the trailing space).
214
+ # @param ary [Array] the array or array-like object.
215
+ # @param index [Integer] the index to retrieve.
216
+ # @param default [Object] the default value.
218
217
  #
219
- # @return [String] the formatted string.
218
+ # @return [Object] the value at the specified index.
220
219
  #
221
- # @raise [ArgumentError] if the first argument is not an Array-like object.
220
+ # @raise [IndexError] if the array does not have a value at that index
221
+ # and there is no default value.
222
222
  #
223
- # @example With Zero Items
224
- # ArrayTools.humanize_list([])
225
- # #=> ''
226
- #
227
- # @example With One Item
228
- # ArrayTools.humanize_list(['spam'])
229
- # #=> 'spam'
230
- #
231
- # @example With Two Items
232
- # ArrayTools.humanize_list(['spam', 'eggs'])
233
- # #=> 'spam and eggs'
234
- #
235
- # @example With Three Or More Items
236
- # ArrayTools.humanize_list(['spam', 'eggs', 'bacon', 'spam'])
237
- # #=> 'spam, eggs, bacon, and spam'
238
- #
239
- # @example With Three Or More Items And Options
240
- # ArrayTools.humanize_list(
241
- # ['spam', 'eggs', 'bacon', 'spam'],
242
- # :last_separator => ' or '
243
- # )
244
- # #=> 'spam, eggs, bacon, or spam'
245
- def humanize_list(ary, **options, &)
223
+ # @overload fetch(ary, index, &default)
224
+ # Retrieves the value at the specified index.
225
+ #
226
+ # If the value does not exist, returns the value of the default block, or
227
+ # raises an IndexError if there is no default block. If the object defines
228
+ # a native #fetch method, delegates to the native implementation.
229
+ #
230
+ # @param ary [Array] the array or array-like object.
231
+ # @param index [Integer] the index to retrieve.
232
+ #
233
+ # @yield generates the default value if there is no value at the index.
234
+ #
235
+ # @yieldparam index [Integer] the requested index.
236
+ #
237
+ # @yieldreturn [Object] the default value.
238
+ #
239
+ # @return [Object] the value at the specified index.
240
+ #
241
+ # @raise [IndexError] if the array does not have a value at that index
242
+ # and there is no default value.
243
+ def fetch(ary, index, default = UNDEFINED, &block)
244
+ require_array!(ary)
245
+
246
+ if ary.respond_to?(:fetch)
247
+ return native_fetch(ary, index, default, &block)
248
+ end
249
+
250
+ size = ary.respond_to?(:size) ? ary.size : ary.count
251
+
252
+ return ary[index] if index < size && index >= -size
253
+
254
+ return block.call(index) if block_given?
255
+
256
+ return default unless default == UNDEFINED
257
+
258
+ raise IndexError,
259
+ "index #{index} outside of array bounds: -#{size}...#{size}"
260
+ end
261
+
262
+ # @overload def humanize_list(ary, **options, &)
263
+ # Generates a human-readable string representation of the list items.
264
+ #
265
+ # Accepts a list of values and returns a human-readable string of the
266
+ # values, with the format based on the number of items.
267
+ #
268
+ # @param ary [Array<String>] the list of values to format. Will be
269
+ # coerced to strings using #to_s.
270
+ # @param options [Hash] optional configuration hash.
271
+ #
272
+ # @option options [String] :last_separator the value to use to separate
273
+ # the final pair of values. Defaults to " and " (note the leading and
274
+ # trailing spaces). Will be combined with the :separator for lists of
275
+ # length 3 or greater.
276
+ # @option options [String] :separator the value to use to separate pairs
277
+ # of values before the last in lists of length 3 or greater. Defaults to
278
+ # ", " (note the trailing space).
279
+ #
280
+ # @return [String] the formatted string.
281
+ #
282
+ # @raise [ArgumentError] if the first argument is not an Array-like
283
+ # object.
284
+ #
285
+ # @example With Zero Items
286
+ # ArrayTools.humanize_list([])
287
+ # #=> ''
288
+ #
289
+ # @example With One Item
290
+ # ArrayTools.humanize_list(['spam'])
291
+ # #=> 'spam'
292
+ #
293
+ # @example With Two Items
294
+ # ArrayTools.humanize_list(['spam', 'eggs'])
295
+ # #=> 'spam and eggs'
296
+ #
297
+ # @example With Three Or More Items
298
+ # ArrayTools.humanize_list(['spam', 'eggs', 'bacon', 'spam'])
299
+ # #=> 'spam, eggs, bacon, and spam'
300
+ #
301
+ # @example With Three Or More Items And Options
302
+ # ArrayTools.humanize_list(
303
+ # ['spam', 'eggs', 'bacon', 'spam'],
304
+ # :last_separator => ' or '
305
+ # )
306
+ # #=> 'spam, eggs, bacon, or spam'
307
+ def humanize_list(ary, **, &)
246
308
  require_array!(ary)
247
309
 
248
310
  return '' if ary.empty?
@@ -253,7 +315,7 @@ module SleepingKingStudios::Tools
253
315
  return ary[0].to_s if size == 1
254
316
 
255
317
  separator, last_separator =
256
- options_for_humanize_list(size:, **options)
318
+ options_for_humanize_list(size:, **)
257
319
 
258
320
  return "#{ary[0]}#{last_separator}#{ary[1]}" if size == 2
259
321
 
@@ -295,7 +357,9 @@ module SleepingKingStudios::Tools
295
357
 
296
358
  return false unless ary.frozen?
297
359
 
298
- ary.each { |item| return false unless ObjectTools.immutable?(item) }
360
+ ary.each do |item|
361
+ return false unless toolbelt.object_tools.immutable?(item)
362
+ end
299
363
 
300
364
  true
301
365
  end
@@ -347,7 +411,14 @@ module SleepingKingStudios::Tools
347
411
  # #=> ['crossbow']
348
412
  # values
349
413
  # #=> ['shortbow', 'longbow', 'arbalest', 'chu-ko-nu']
414
+ #
415
+ # @deprecated v1.3.0 Use Array#[]= with a range instead.
350
416
  def splice(ary, start, delete_count, *insert)
417
+ toolbelt.core_tools.deprecate(
418
+ "#{self.class.name}#splice",
419
+ message: 'Use Array#[]= with a range instead.'
420
+ )
421
+
351
422
  require_array!(ary)
352
423
 
353
424
  start += ary.count if start.negative?
@@ -361,6 +432,12 @@ module SleepingKingStudios::Tools
361
432
 
362
433
  private
363
434
 
435
+ def native_fetch(ary, index, default, &)
436
+ return ary.fetch(index, &) if default == UNDEFINED
437
+
438
+ ary.fetch(index, default, &)
439
+ end
440
+
364
441
  def options_for_humanize_list(
365
442
  size:,
366
443
  last_separator: ' and ',
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools/assertions'
4
+
5
+ module SleepingKingStudios::Tools
6
+ # Utility for grouping multiple assertion statements.
7
+ #
8
+ # @example
9
+ # rocket = Struct.new(:fuel, :launched).new(0.0, true)
10
+ # aggregator = SleepingKingStudios::Tools::Assertions::Aggregator.new
11
+ # aggregator.empty?
12
+ # #=> true
13
+ #
14
+ # aggregator.assert(message: 'is out of fuel') { rocket.fuel > 0 }
15
+ # aggregator.assert(message: 'has already launched') { !rocket.launched }
16
+ # aggregator.empty?
17
+ # #=> false
18
+ # aggregator.failure_message
19
+ # #=> 'is out of fuel, has already launched'
20
+ class Assertions::Aggregator < Assertions
21
+ extend Forwardable
22
+
23
+ def initialize
24
+ super
25
+
26
+ @failures = []
27
+ end
28
+
29
+ # @!method <<(message)
30
+ # Appends the message to the failure messages.
31
+ #
32
+ # @param message [String] the message to append.
33
+ #
34
+ # @return [Array] the updated failure messages.
35
+ #
36
+ # @see Array#<<.
37
+
38
+ # @!method clear
39
+ # Removes all items from the failure messages.
40
+ #
41
+ # @return [Array] the empty failure messages.
42
+ #
43
+ # @see Array#clear.
44
+
45
+ # @!method count
46
+ # Returns a count of the failure message.
47
+ #
48
+ # @return [Integer] the number of failure messages.
49
+ #
50
+ # @see Array#count.
51
+
52
+ # @!method each
53
+ # Iterates over the failure messages.
54
+ #
55
+ # @overload each
56
+ # Returns an enumerator that iterates over the failure messages.
57
+ #
58
+ # @return [Enumerator] an enumerator over the messages.
59
+ #
60
+ # @see Enumerable#each.
61
+ #
62
+ # @overload each(&block)
63
+ # Yields each failure message to the block.
64
+ #
65
+ # @yieldparam message [String] the current failure message.
66
+ #
67
+ # @see Enumerable#each.
68
+
69
+ # @!method empty?
70
+ # Checks if there are any failure messages.
71
+ #
72
+ # @return [true, false] true if there are no failure messages; otherwise
73
+ # false.
74
+ #
75
+ # @see Enumerable#empty?
76
+
77
+ # @!method size
78
+ # Returns a count of the failure message.
79
+ #
80
+ # @return [Integer] the number of failure messages.
81
+ #
82
+ # @see Array#size.
83
+ def_delegators :@failures,
84
+ :<<,
85
+ :clear,
86
+ :count,
87
+ :each,
88
+ :empty?,
89
+ :size
90
+
91
+ # (see SleepingKingStudios::Tools::Assertions#assert_group)
92
+ def assert_group(error_class: AssertionError, message: nil, &assertions)
93
+ return super if message
94
+
95
+ raise ArgumentError, 'no block given' unless block_given?
96
+
97
+ assertions.call(self)
98
+ end
99
+ alias aggregate assert_group
100
+
101
+ # Generates a combined failure message from the configured messages.
102
+ #
103
+ # @return [String] the combined messages for each failed assertion.
104
+ #
105
+ # @example With an empty aggregator.
106
+ # aggregator = SleepingKingStudios::Tools::Assertions::Aggregator.new
107
+ #
108
+ # aggregator.failure_message
109
+ # #=> ''
110
+ #
111
+ # @example With an aggregator with failure messages.
112
+ # aggregator = SleepingKingStudios::Tools::Assertions::Aggregator.new
113
+ # aggrgator << 'rocket is out of fuel'
114
+ # aggrgator << 'rocket is not pointed toward space'
115
+ #
116
+ # aggregator.failure_message
117
+ # #=> 'rocket is out of fuel, rocket is not pointed toward space'
118
+ def failure_message
119
+ failures.join(', ')
120
+ end
121
+
122
+ private
123
+
124
+ attr_reader :failures
125
+
126
+ def handle_error(message:, **_)
127
+ failures << message
128
+
129
+ message
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools/assertions'
4
+
5
+ module SleepingKingStudios::Tools
6
+ # Messages strategy for displaying assertions errors.
7
+ #
8
+ # Defines its own internal message templates, and automatically handles value
9
+ # name option (:as option) when generating messages.
10
+ class Assertions::MessagesStrategy < SleepingKingStudios::Tools::Messages::Strategies::HashStrategy
11
+ # rubocop:disable Layout/HashAlignment
12
+ ERROR_MESSAGES =
13
+ {
14
+ 'blank' =>
15
+ 'must be nil or empty',
16
+ 'block' =>
17
+ 'block returned a falsy value',
18
+ 'boolean' =>
19
+ 'must be true or false',
20
+ 'class' =>
21
+ 'is not a Class',
22
+ 'class_or_module' =>
23
+ 'is not a Class or Module',
24
+ 'exclusion' =>
25
+ 'is one of %<expected>s',
26
+ 'exclusion_range' =>
27
+ 'is within %<range_expr>s',
28
+ 'inclusion' =>
29
+ 'is not one of %<expected>s',
30
+ 'inclusion_range' =>
31
+ 'is outside %<range_expr>s',
32
+ 'inherit_from' =>
33
+ 'does not inherit from %<expected>s',
34
+ 'instance_of' =>
35
+ 'is not an instance of %<expected>s',
36
+ 'instance_of_anonymous' =>
37
+ 'is not an instance of %<expected>s (%<parent>s)',
38
+ 'matches' =>
39
+ 'does not match the expected value',
40
+ 'matches_proc' =>
41
+ 'does not match the Proc',
42
+ 'matches_regexp' =>
43
+ 'does not match the pattern %<pattern>s',
44
+ 'name' =>
45
+ 'is not a String or a Symbol',
46
+ 'nil' =>
47
+ 'must be nil',
48
+ 'not_nil' =>
49
+ 'must not be nil',
50
+ # @note: This value will be changed in a future version.
51
+ 'presence' =>
52
+ "can't be blank"
53
+ }.freeze
54
+ # rubocop:enable Layout/HashAlignment
55
+
56
+ def initialize
57
+ templates =
58
+ ERROR_MESSAGES
59
+ .transform_keys do |key|
60
+ "sleeping_king_studios.tools.assertions.#{key}"
61
+ end
62
+ .freeze
63
+
64
+ super(templates)
65
+ end
66
+
67
+ private
68
+
69
+ def generate(template, as: nil, parameters: {}, **)
70
+ message = super
71
+
72
+ return message unless as
73
+
74
+ "#{as} #{message}"
75
+ end
76
+ end
77
+ end