sleeping_king_studios-tools 1.0.2 → 1.1.0.rc.0

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: 00a0525024c0fc1f2999486b96860169b8d8d09921d5fc289c53f2ee7960a796
4
- data.tar.gz: '0788cece6dd1fa8ea4eb88f2bdd047f1f4804fbf610b4c4312badd1dcf7e8800'
3
+ metadata.gz: 8e06daa0cd40dd75a70bd47807132708527dbee294dc0abef34d870180ca3e66
4
+ data.tar.gz: 4ee981bc8dd164c52d399cbf73f14b07b2ccc027759e894a12502b3d7f7347fc
5
5
  SHA512:
6
- metadata.gz: a54d291f7f55d911b732c2be690f51e8d33931ee353ba3504ccf68c726a2398a46dd059ae3eeb9d1253619f6e704ab557af75cc4a3900c326451afb696afbcb6
7
- data.tar.gz: 784980ce5bdb0e5266e14ac5c2e6c3747aaca88ba085a7ee1983bfdb4b625a483c8e266f688e41e6d7c4e624796661253d64d93232975c84fceb64a2d7ad3c51
6
+ metadata.gz: 8be13ff1e4b825a1f449d0b4f073336a3b86b9616f2abd2dd196eb33f5d169ab7e1bf7611a036794a1b4991a363a5b438c1145aec6a741cea4a9fec5481465ca
7
+ data.tar.gz: 717bbc4c705590b75786933480570c7dd55b792a17616888bc10c4eb459bb99b7c78b2d285dba83125774cf2a40beca54151eada287dbfc69ccbf074f666170c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ### 1.1.0
4
+
5
+ Files in the `Toolbox` are now autoloaded.
6
+
7
+ #### Tools
8
+
9
+ Implemented `SleepingKingStudios::Tools::Assertions` with the following methods:
10
+
11
+ - `#assert`
12
+ - `#assert_boolean`
13
+ - `#assert_class`
14
+ - `#assert_instance_of`
15
+ - `#assert_matches`
16
+ - `#assert_name`
17
+
18
+ Each method raises an exception if its condition is not met. `Assertions` also defines equivalent `#validate` methods, which always raise an `ArgumentError`.
19
+
20
+ Added :deprecation_caller_depth option to CoreTools. When calling #deprecate with the default strategy of 'warn', will print the specified number of lines from the caller. The default value is 3.
21
+
22
+ #### Toolbelt
23
+
24
+ - Added `#assertions` to Toolbelt.
25
+
26
+ #### Toolbox
27
+
28
+ Implemented `SleepingKingStudios::Tools::Toolbox::Subclass`, which implements partial application for constructor parameters.
29
+
3
30
  ### 1.0.2
4
31
 
5
32
  Updated gem metadata.
data/README.md CHANGED
@@ -4,7 +4,7 @@ A library of utility services and concerns to expand the functionality of core c
4
4
 
5
5
  ## About
6
6
 
7
- SleepingKingStudios::Tools is tested against MRI Ruby 2.5 through 3.0.
7
+ SleepingKingStudios::Tools is tested against MRI Ruby 2.6 through 3.1.
8
8
 
9
9
  ### Documentation
10
10
 
@@ -30,18 +30,20 @@ Please note that the `SleepingKingStudios::Tools` project is released with a [Co
30
30
 
31
31
  The tools can be accessed in a convenient form using the Toolbelt class.
32
32
 
33
- require 'sleeping_king_studios/tools'
33
+ ```ruby
34
+ require 'sleeping_king_studios/tools'
34
35
 
35
- tools = ::SleepingKingStudios::Tools::Toolbelt.instance
36
+ tools = ::SleepingKingStudios::Tools::Toolbelt.instance
36
37
 
37
- tools.array.humanize_list 'one', 'two', 'three'
38
- #=> calls ArrayTools#humanize_list
38
+ tools.ary.humanize_list 'one', 'two', 'three'
39
+ #=> calls ArrayTools#humanize_list
39
40
 
40
- tools.core.deprecate 'my_method'
41
- #=> calls CoreTools#deprecate
41
+ tools.core.deprecate 'my_method'
42
+ #=> calls CoreTools#deprecate
42
43
 
43
- tools.string.underscore 'MyModuleName'
44
- #=> calls StringTools#underscore
44
+ tools.str.underscore 'MyModuleName'
45
+ #=> calls StringTools#underscore
46
+ ```
45
47
 
46
48
  ### Array Tools
47
49
 
@@ -181,6 +183,256 @@ Accepts an array, a start value, a number of items to delete, and zero or more i
181
183
  values
182
184
  #=> ['shortbow', 'longbow', 'arbalest', 'chu-ko-nu']
183
185
 
186
+ ### Assertions
187
+
188
+ Tools for validating the current application state.
189
+
190
+ #### `#assert`
191
+
192
+ Raises an exception unless the given block returns a truthy value.
193
+
194
+ ```ruby
195
+ Assertions.assert { true == false }
196
+ #=> raises an AssertionError with message 'block returned a falsy value'
197
+
198
+ Assertions.assert { true == true }
199
+ #=> does not raise an exception
200
+ ```
201
+
202
+ It accepts the following options:
203
+
204
+ - `error_class:` The class of exception to raise. Defaults to `SleepingKingStudios::Tools::Assertions::AssertionError`.
205
+ - `message`: The error message to display.
206
+
207
+ #### `#assert_boolean`
208
+
209
+ Raises an exception unless the given value is either `true` or `false`.
210
+
211
+ ```ruby
212
+ Assertions.assert_boolean(nil)
213
+ #=> raises an AssertionError with message 'value must be true or false'
214
+
215
+ Assertions.assert_boolean(Object.new)
216
+ #=> raises an AssertionError with message 'value must be true or false'
217
+
218
+ Assertions.assert_boolean(false)
219
+ #=> does not raise an exception
220
+
221
+ Assertions.assert_boolean(true)
222
+ #=> does not raise an exception
223
+ ```
224
+
225
+ It accepts the following options:
226
+
227
+ - `as:` A short descriptor of the given value. Defaults to `"value"`.
228
+ - `error_class:` The class of exception to raise. Defaults to `SleepingKingStudios::Tools::Assertions::AssertionError`.
229
+ - `message`: The error message to display.
230
+
231
+ #### `#assert_class`
232
+
233
+ Raises an exception unless the given value is a Class.
234
+
235
+ ```ruby
236
+ Assertions.assert_class(Object.new)
237
+ #=> raises an AssertionError with message 'value is not a class'
238
+
239
+ Assertions.assert_class(String)
240
+ #=> does not raise an exception
241
+ ```
242
+
243
+ It accepts the following options:
244
+
245
+ - `as:` A short descriptor of the given value. Defaults to `"value"`.
246
+ - `error_class:` The class of exception to raise. Defaults to `SleepingKingStudios::Tools::Assertions::AssertionError`.
247
+ - `message`: The error message to display.
248
+
249
+ #### `#assert_instance_of`
250
+
251
+ Raises an exception unless the given value is an instance of the expected Class or Module.
252
+
253
+ ```ruby
254
+ Assertions.assert_instance_of(:foo, expected: String)
255
+ #=> raises an AssertionError with message 'value is not an instance of String'
256
+
257
+ Assertions.assert_instance_of('foo', expected: String)
258
+ #=> does not raise an exception
259
+ ```
260
+
261
+ It accepts the following options:
262
+
263
+ - `as:` A short descriptor of the given value. Defaults to `"value"`.
264
+ - `error_class:` The class of exception to raise. Defaults to `SleepingKingStudios::Tools::Assertions::AssertionError`.
265
+ - `message`: The error message to display.
266
+ - `optional`: If true, accepts `nil` values as well as instances of the Class or Module.
267
+
268
+ #### `#assert_matches`
269
+
270
+ Raises an exception unless the given value matches the expected value using case equality (`#===`).
271
+
272
+
273
+ ```ruby
274
+ Assertions.assert_matches('bar', expected: /foo/)
275
+ #=> raises an AssertionError with message 'value does not match the pattern /foo/'
276
+
277
+ Assertions.assert_matches('foo', expected: /foo/)
278
+ #=> does not raise an exception
279
+ ```
280
+
281
+ It accepts the following options:
282
+
283
+ - `as:` A short descriptor of the given value. Defaults to `"value"`.
284
+ - `error_class:` The class of exception to raise. Defaults to `SleepingKingStudios::Tools::Assertions::AssertionError`.
285
+ - `message`: The error message to display.
286
+ - `optional`: If true, accepts `nil` values as well as matching values.
287
+
288
+ #### `#assert_name`
289
+
290
+ Raises an exception unless the given value is non-empty a String or Symbol.
291
+
292
+ ```ruby
293
+ Assertions.assert_name(nil)
294
+ #=> raises an AssertionError with message "value can't be blank"
295
+
296
+ Assertions.assert_name(Object.new)
297
+ #=> raises an AssertionError with message 'value is not a String or a Symbol'
298
+
299
+ Assertions.assert_name('')
300
+ #=> raises an AssertionError with message "value can't be blank"
301
+
302
+ Assertions.assert_name('foo')
303
+ #=> does not raise an exception
304
+
305
+ Assertions.assert_name(:bar)
306
+ #=> does not raise an exception
307
+ ```
308
+
309
+ It accepts the following options:
310
+
311
+ - `as:` A short descriptor of the given value. Defaults to `"value"`.
312
+ - `error_class:` The class of exception to raise. Defaults to `SleepingKingStudios::Tools::Assertions::AssertionError`.
313
+ - `message`: The error message to display.
314
+ - `optional`: If true, accepts `nil` values as well as valid Symbols and Strings.
315
+
316
+ #### `#validate`
317
+
318
+ Raises an `ArgumentError` unless the given block returns a truthy value.
319
+
320
+ ```ruby
321
+ Assertions.validate { true == false }
322
+ #=> raises an ArgumentError with message 'block returned a falsy value'
323
+
324
+ Assertions.validate { true == true }
325
+ #=> does not raise an exception
326
+ ```
327
+
328
+ It accepts the following options:
329
+
330
+ - `message`: The error message to display.
331
+
332
+ #### `#validate_boolean`
333
+
334
+ Raises an exception unless the given value is either `true` or `false`.
335
+
336
+ ```ruby
337
+ Assertions.validate_boolean(nil)
338
+ #=> raises an ArgumentError with message 'value must be true or false'
339
+
340
+ Assertions.validate_boolean(Object.new)
341
+ #=> raises an ArgumentError with message 'value must be true or false'
342
+
343
+ Assertions.validate_boolean(false)
344
+ #=> does not raise an exception
345
+
346
+ Assertions.validate_boolean(true)
347
+ #=> does not raise an exception
348
+ ```
349
+
350
+ It accepts the following options:
351
+
352
+ - `as:` A short descriptor of the given value. Defaults to `"value"`.
353
+ - `message`: The error message to display.
354
+
355
+ #### `#validate_class`
356
+
357
+ Raises an `ArgumentError` unless the given value is a Class.
358
+
359
+ ```ruby
360
+ Assertions.validate_class(Object.new)
361
+ #=> raises an ArgumentError with message 'value is not a class'
362
+
363
+ Assertions.validate_class(String)
364
+ #=> does not raise an exception
365
+ ```
366
+
367
+ It accepts the following options:
368
+
369
+ - `as:` A short descriptor of the given value. Defaults to `"value"`.
370
+ - `message`: The error message to display.
371
+
372
+ #### `#validate_instance_of`
373
+
374
+ Raises an `ArgumentError` unless the given value is an instance of the expected Class or Module.
375
+
376
+ ```ruby
377
+ Assertions.validate_instance_of(:foo, expected: String)
378
+ #=> raises an AssertionError with message 'value is not an instance of String'
379
+
380
+ Assertions.validate_instance_of('foo', expected: String)
381
+ #=> does not raise an exception
382
+ ```
383
+
384
+ It accepts the following options:
385
+
386
+ - `as:` A short descriptor of the given value. Defaults to `"value"`.
387
+ - `message`: The error message to display.
388
+ - `optional`: If true, accepts `nil` values as well as instances of the Class or Module.
389
+
390
+ #### `#validate_matches`
391
+
392
+ Raises an `ArgumentError` unless the given value matches the expected value using case equality (`#===`).
393
+
394
+
395
+ ```ruby
396
+ Assertions.validate_matches('bar', expected: /foo/)
397
+ #=> raises an ArgumentError with message 'value does not match the pattern /foo/'
398
+
399
+ Assertions.validate_matches('foo', expected: /foo/)
400
+ #=> does not raise an exception
401
+ ```
402
+
403
+ It accepts the following options:
404
+
405
+ - `as:` A short descriptor of the given value. Defaults to `"value"`.
406
+ - `message`: The error message to display.
407
+ - `optional`: If true, accepts `nil` values as well as matching values.
408
+
409
+ #### `#validate_name`
410
+
411
+ Raises an `ArgumentError` unless the given value is non-empty a String or Symbol.
412
+
413
+ ```ruby
414
+ Assertions.validate_name(nil)
415
+ #=> raises an ArgumentError with message "value can't be blank"
416
+
417
+ Assertions.validate_name(Object.new)
418
+ #=> raises an AssertionError with message 'value is not a String or a Symbol'
419
+
420
+ Assertions.validate_name('')
421
+ #=> raises an ArgumentError with message "value can't be blank"
422
+
423
+ Assertions.validate_name('foo')
424
+ #=> does not raise an exception
425
+
426
+ Assertions.validate_name(:bar)
427
+ #=> does not raise an exception
428
+ ```
429
+
430
+ It accepts the following options:
431
+
432
+ - `as:` A short descriptor of the given value. Defaults to `"value"`.
433
+ - `message`: The error message to display.
434
+ - `optional`: If true, accepts `nil` values as well as valid Symbols and Strings.
435
+
184
436
  ### Core Tools
185
437
 
186
438
  Tools for working with an application or working environment.
@@ -189,17 +441,40 @@ Tools for working with an application or working environment.
189
441
 
190
442
  Prints a deprecation warning.
191
443
 
192
- CoreTools.deprecate 'ObjectTools#old_method'
193
- #=> prints to stderr:
194
-
195
- [WARNING] ObjectTools#old_method is deprecated.
196
- called from /path/to/file.rb:4: in something_or_other
197
-
198
- CoreTools.deprecate 'ObjectTools#old_method', '0.1.0', :format => '%s was deprecated in version %s.'
199
- #=> prints to stderr:
200
-
201
- ObjectTools#old_method was deprecated in version 0.1.0.
202
- called from /path/to/file.rb:4: in something_or_other
444
+ ```ruby
445
+ CoreTools.deprecate 'ObjectTools#old_method'
446
+ #=> prints to stderr:
447
+ #
448
+ # [WARNING] ObjectTools#old_method is deprecated.
449
+ # called from /path/to/file.rb:4: in something_or_other
450
+ ```
451
+
452
+ You can also specify an additional message to display:
453
+
454
+ ```ruby
455
+ CoreTools.deprecate 'ObjectTools#old_method',
456
+ 'Use #new_method instead.'
457
+ #=> prints to stderr:
458
+ #
459
+ # [WARNING] ObjectTools#old_method is deprecated. Use #new_method instead.
460
+ # called from /path/to/file.rb:4: in something_or_other
461
+ ```
462
+
463
+ You can specify a custom format for the deprecation message:
464
+
465
+ ```ruby
466
+ CoreTools.deprecate 'ObjectTools#old_method',
467
+ '0.1.0',
468
+ format: '%s was deprecated in version %s.'
469
+ #=> prints to stderr:
470
+ #
471
+ # ObjectTools#old_method was deprecated in version 0.1.0.
472
+ # called from /path/to/file.rb:4: in something_or_other
473
+ ```
474
+
475
+ By default, `#deprecate` will print the last 3 lines of the caller, excluding
476
+ any lines from `Forwardable` and from `SleepingKingStudios::Tools` itself. To
477
+ print a different number of lines, pass a custom `deprecation_caller_depth` parameter to `CoreTools.new` or set the `DEPRECATION_CALLER_DEPTH` environment variable.
203
478
 
204
479
  #### `#empty_binding`
205
480
 
@@ -825,3 +1100,102 @@ Concatenates the MAJOR, MINOR, and PATCH constant values with PRERELEASE and BUI
825
1100
  #### `#to_version`
826
1101
 
827
1102
  Concatenates the MAJOR, MINOR, and PATCH constant values with PRERELEASE and BUILD (if available) to generate a semantic version string. The major, minor, and patch values are separated by dots `.`, then the prerelease (if available) preceded by a hyphen `-`, and the build (if available) preceded by a plus sign `+`.
1103
+
1104
+ ### Subclass
1105
+
1106
+ ```ruby
1107
+ require 'sleeping_king_studios/tools/toolbox/subclass'
1108
+ ```
1109
+
1110
+ The `Subclass` module provides a mechanism for performing partial application (or "currying") of constructor parameters. This is useful for defining subclasses that inject pre-defined values into the constructor.
1111
+
1112
+ Let's consider an example. We'll start by defining a base class.
1113
+
1114
+ ```ruby
1115
+ class Query
1116
+ extend SleepingKingStudios::Tools::Toolbox::Subclass
1117
+
1118
+ def initialize(entity_class, **options)
1119
+ @entity_class = entity_class
1120
+ @options = options
1121
+ end
1122
+
1123
+ attr_reader :entity_class
1124
+
1125
+ attr_reader :options
1126
+
1127
+ def call
1128
+ # Querying logic here.
1129
+ end
1130
+ end
1131
+
1132
+ query = Query.new(Book, limit: 10)
1133
+ query.entity_class
1134
+ #=> Book
1135
+ query.options
1136
+ #=> { limit: 10 }
1137
+ ```
1138
+
1139
+ Our `Query` class is used to perform queries on some data source - a relational table, an API, or some in-memory data structure. To perform a query, we need to know what data to request. This is represented by the `:entity_class` argument. Additionally, we can pass arbitrary `:options` as keywords.
1140
+
1141
+ ```ruby
1142
+ BooksQuery = Query.subclass(Book)
1143
+
1144
+ query = BooksQuery.new(order: :title)
1145
+ query.entity_class
1146
+ #=> Book
1147
+ query.options
1148
+ #=> { order: :title }
1149
+ ```
1150
+
1151
+ By calling `.subclass` on our `Query` class, we are defining a new subclass of `Query` that injects the given parameters and partially applies them to the constructor. In this case, we are injecting the `Book` class into our query.
1152
+
1153
+ We can also use `.subclass` to partially apply keywords or a block.
1154
+
1155
+ ```ruby
1156
+ RecentItemsQuery = Query.subclass(order: { created_at: :desc })
1157
+
1158
+ query = RecentItemsQuery.new(Book)
1159
+ query.entity_class
1160
+ #=> Book
1161
+ query.options
1162
+ #=> { order: { created_at: :desc } }
1163
+ ```
1164
+
1165
+ When you call the subclass's constructor with additional parameters, they are applied in addition to the configured values (if any).
1166
+
1167
+ - Any arguments passed to `.new` are added *after* the configured arguments.
1168
+ - Any keywords passed to `.new` are merged into the configured keywords, with the values passed to `.new` taking precedence.
1169
+ - A block passed to `.new` will take precedence over a configured block.
1170
+
1171
+ ```ruby
1172
+ class Character
1173
+ extend SleepingKingStudios::Tools::Toolbox::Subclass
1174
+
1175
+ def initialize(*traits, **stats, &special)
1176
+ @traits = traits
1177
+ @stats = stats
1178
+ @special = special
1179
+ end
1180
+
1181
+ attr_reader :special
1182
+
1183
+ attr_reader :stats
1184
+
1185
+ attr_reader :traits
1186
+ end
1187
+
1188
+ Bard = Character.subclass(:musical, performance: 5, persuasion: 10) do
1189
+ 'The bard sings a cheerful tune.'
1190
+ end
1191
+
1192
+ aoife = Bard.new(:sorcerous, magic: 5, performance: 10) do
1193
+ 'Aoife drops her lute!'
1194
+ end
1195
+ aoife.traits
1196
+ #=> [:musical, :sorcerous]
1197
+ aoife.stats
1198
+ #=> { magic: 5, performance: 10, persuasion: 10 }
1199
+ aoife.special.call
1200
+ #=> 'Aoife drops her lute!'
1201
+ ```
@@ -302,7 +302,7 @@ module SleepingKingStudios::Tools
302
302
  def require_array!(value)
303
303
  return if array?(value)
304
304
 
305
- raise ArgumentError, 'argument must be an array', caller[1..-1]
305
+ raise ArgumentError, 'argument must be an array', caller[1..]
306
306
  end
307
307
  end
308
308
  end
@@ -0,0 +1,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools'
4
+
5
+ module SleepingKingStudios::Tools
6
+ # Methods for asserting on the state of a function or application.
7
+ class Assertions < Base # rubocop:disable Metrics/ClassLength
8
+ # Error class for handling a failed assertion.
9
+ class AssertionError < StandardError; end
10
+
11
+ # Asserts that the block returns a truthy value.
12
+ #
13
+ # @param error_class [Class] The exception class to raise on a failure.
14
+ # @param message [String] The exception message to raise on a failure.
15
+ #
16
+ # @yield The block to evaluate.
17
+ # @yieldreturn [Object] The returned value of the block.
18
+ #
19
+ # @raise AssertionError if the block does not return a truthy value.
20
+ def assert(error_class: AssertionError, message: nil, &block)
21
+ return if block.call
22
+
23
+ raise error_class,
24
+ message || 'block returned a falsy value',
25
+ caller(1..-1)
26
+ end
27
+
28
+ # Asserts that the value is either true or false.
29
+ #
30
+ # @param value [Object] The value to assert on.
31
+ # @param as [String] The name of the asserted value.
32
+ # @param error_class [Class] The exception class to raise on a failure.
33
+ # @param message [String] The exception message to raise on a failure.
34
+ #
35
+ # @raise AssertionError if the value is not a Class.
36
+ def assert_boolean(
37
+ value,
38
+ as: 'value',
39
+ error_class: AssertionError,
40
+ message: nil
41
+ )
42
+ return if value.equal?(true) || value.equal?(false)
43
+
44
+ raise error_class,
45
+ message || "#{as} must be true or false",
46
+ caller(1..-1)
47
+ end
48
+
49
+ # Asserts that the value is a Class.
50
+ #
51
+ # @param value [Object] The value to assert on.
52
+ # @param as [String] The name of the asserted value.
53
+ # @param error_class [Class] The exception class to raise on a failure.
54
+ # @param message [String] The exception message to raise on a failure.
55
+ #
56
+ # @raise AssertionError if the value is not a Class.
57
+ def assert_class(
58
+ value,
59
+ as: 'value',
60
+ error_class: AssertionError,
61
+ message: nil
62
+ )
63
+ return if value.is_a?(Class)
64
+
65
+ raise error_class,
66
+ message || "#{as} is not a Class",
67
+ caller(1..-1)
68
+ end
69
+
70
+ # Asserts that the value is an example of the given Class.
71
+ #
72
+ # @param value [Object] The value to assert on.
73
+ # @param as [String] The name of the asserted value.
74
+ # @param error_class [Class] The exception class to raise on a failure.
75
+ # @param expected [Class] The expected class.
76
+ # @param message [String] The exception message to raise on a failure.
77
+ # @param optional [true, false] If true, allows nil values.
78
+ #
79
+ # @raise ArgumentError if the expected class is not a Class.
80
+ # @raise AssertionError if the value is not an instance of the expected
81
+ # class.
82
+ def assert_instance_of( # rubocop:disable Metrics/ParameterLists
83
+ value,
84
+ expected:,
85
+ as: 'value',
86
+ error_class: AssertionError,
87
+ message: nil,
88
+ optional: false
89
+ )
90
+ unless expected.is_a?(Class)
91
+ raise ArgumentError, 'expected must be a Class'
92
+ end
93
+
94
+ return if optional && value.nil?
95
+ return if value.is_a?(expected)
96
+
97
+ raise error_class,
98
+ message || "#{as} is not an instance of #{class_name(expected)}",
99
+ caller(1..-1)
100
+ end
101
+
102
+ # Asserts that the value matches the expected object using #===.
103
+ #
104
+ # @param value [Object] The value to assert on.
105
+ # @param as [String] The name of the asserted value.
106
+ # @param error_class [Class] The exception class to raise on a failure.
107
+ # @param expected [#===] The expected object.
108
+ # @param message [String] The exception message to raise on a failure.
109
+ # @param optional [true, false] If true, allows nil values.
110
+ #
111
+ # @raise AssertionError if the value does not match the expected object.
112
+ def assert_matches( # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists
113
+ value,
114
+ expected:,
115
+ as: 'value',
116
+ error_class: AssertionError,
117
+ message: nil,
118
+ optional: false
119
+ )
120
+ return if optional && value.nil?
121
+ return if expected === value # rubocop:disable Style/CaseEquality
122
+
123
+ message ||=
124
+ case expected
125
+ when Module
126
+ "#{as} is not an instance of #{class_name(expected)}"
127
+ when Proc
128
+ "#{as} does not match the Proc"
129
+ when Regexp
130
+ "#{as} does not match the pattern #{expected.inspect}"
131
+ else
132
+ "#{as} does not match the expected value"
133
+ end
134
+
135
+ raise error_class, message, caller(1..-1)
136
+ end
137
+
138
+ # Asserts that the value is a non-empty String or Symbol.
139
+ #
140
+ # @param value [Object] The value to assert on.
141
+ # @param as [String] The name of the asserted value.
142
+ # @param error_class [Class] The exception class to raise on a failure.
143
+ # @param message [String] The exception message to raise on a failure.
144
+ # @param optional [true, false] If true, allows nil values.
145
+ #
146
+ # @raise AssertionError if the value is not a String or a Symbol, or if the
147
+ # value is empty.
148
+ def assert_name( # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
149
+ value,
150
+ as: 'value',
151
+ error_class: AssertionError,
152
+ message: nil,
153
+ optional: false
154
+ )
155
+ if value.nil?
156
+ return if optional
157
+
158
+ raise error_class, message || "#{as} can't be blank", caller(1..-1)
159
+ end
160
+
161
+ unless value.is_a?(String) || value.is_a?(Symbol)
162
+ raise error_class,
163
+ message || "#{as} is not a String or a Symbol",
164
+ caller(1..-1)
165
+ end
166
+
167
+ return unless value.empty?
168
+
169
+ raise error_class, message || "#{as} can't be blank", caller(1..-1)
170
+ end
171
+
172
+ # Asserts that the block returns a truthy value.
173
+ #
174
+ # @param message [String] The exception message to raise on a failure.
175
+ #
176
+ # @yield The block to evaluate.
177
+ # @yieldreturn [Object] The returned value of the block.
178
+ #
179
+ # @raise ArgumentError if the block does not return a truthy value.
180
+ def validate(message: nil, &block)
181
+ return if block.call
182
+
183
+ raise ArgumentError,
184
+ message || 'block returned a falsy value',
185
+ caller(1..-1)
186
+ end
187
+
188
+ # Asserts that the value is either true or false.
189
+ #
190
+ # @param value [Object] The value to assert on.
191
+ # @param as [String] The name of the asserted value.
192
+ # @param message [String] The exception message to raise on a failure.
193
+ #
194
+ # @raise AssertionError if the value is not a Class.
195
+ def validate_boolean(value, as: 'value', message: nil)
196
+ return if value.equal?(true) || value.equal?(false)
197
+
198
+ raise ArgumentError,
199
+ message || "#{as} must be true or false",
200
+ caller(1..-1)
201
+ end
202
+
203
+ # Asserts that the value is a Class.
204
+ #
205
+ # @param value [Object] The value to assert on.
206
+ # @param as [String] The name of the asserted value.
207
+ # @param message [String] The exception message to raise on a failure.
208
+ #
209
+ # @raise ArgumentError if the value is not a Class.
210
+ def validate_class(value, as: 'value', message: nil)
211
+ return if value.is_a?(Class)
212
+
213
+ raise ArgumentError,
214
+ message || "#{as} is not a Class",
215
+ caller(1..-1)
216
+ end
217
+
218
+ # Asserts that the value is an example of the given Class.
219
+ #
220
+ # @param value [Object] The value to assert on.
221
+ # @param as [String] The name of the asserted value.
222
+ # @param expected [Class] The expected class.
223
+ # @param message [String] The exception message to raise on a failure.
224
+ # @param optional [true, false] If true, allows nil values.
225
+ #
226
+ # @raise ArgumentError if the expected class is not a Class.
227
+ # @raise AssertionError if the value is not an instance of the expected
228
+ # class.
229
+ def validate_instance_of(
230
+ value,
231
+ expected:,
232
+ as: 'value',
233
+ message: nil,
234
+ optional: false
235
+ )
236
+ unless expected.is_a?(Class)
237
+ raise ArgumentError, 'expected must be a Class'
238
+ end
239
+
240
+ return if optional && value.nil?
241
+ return if value.is_a?(expected)
242
+
243
+ raise ArgumentError,
244
+ message || "#{as} is not an instance of #{class_name(expected)}",
245
+ caller(1..-1)
246
+ end
247
+
248
+ # Asserts that the value matches the expected object using #===.
249
+ #
250
+ # @param value [Object] The value to assert on.
251
+ # @param as [String] The name of the asserted value.
252
+ # @param expected [#===] The expected object.
253
+ # @param message [String] The exception message to raise on a failure.
254
+ # @param optional [true, false] If true, allows nil values.
255
+ #
256
+ # @raise ArgumentError if the value does not match the expected object.
257
+ def validate_matches( # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
258
+ value,
259
+ expected:,
260
+ as: 'value',
261
+ message: nil,
262
+ optional: false
263
+ )
264
+ return if optional && value.nil?
265
+ return if expected === value # rubocop:disable Style/CaseEquality
266
+
267
+ message ||=
268
+ case expected
269
+ when Module
270
+ "#{as} is not an instance of #{class_name(expected)}"
271
+ when Proc
272
+ "#{as} does not match the Proc"
273
+ when Regexp
274
+ "#{as} does not match the pattern #{expected.inspect}"
275
+ else
276
+ "#{as} does not match the expected value"
277
+ end
278
+
279
+ raise ArgumentError, message, caller(1..-1)
280
+ end
281
+
282
+ # Asserts that the value is a non-empty String or Symbol.
283
+ #
284
+ # @param value [Object] The value to assert on.
285
+ # @param as [String] The name of the asserted value.
286
+ # @param message [String] The exception message to raise on a failure.
287
+ # @param optional [true, false] If true, allows nil values.
288
+ #
289
+ # @raise ArgumentError if the value is not a String or a Symbol, or if the
290
+ # value is empty.
291
+ def validate_name( # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
292
+ value,
293
+ as: 'value',
294
+ message: nil,
295
+ optional: false
296
+ )
297
+ if value.nil?
298
+ return if optional
299
+
300
+ raise ArgumentError, message || "#{as} can't be blank", caller(1..-1)
301
+ end
302
+
303
+ unless value.is_a?(String) || value.is_a?(Symbol)
304
+ raise ArgumentError,
305
+ message || "#{as} is not a String or a Symbol",
306
+ caller(1..-1)
307
+ end
308
+
309
+ return unless value.empty?
310
+
311
+ raise ArgumentError, message || "#{as} can't be blank", caller(1..-1)
312
+ end
313
+
314
+ private
315
+
316
+ def class_name(expected)
317
+ return expected.name if expected.name
318
+
319
+ "#{expected.inspect} (#{expected.ancestors.find(&:name).name})"
320
+ end
321
+ end
322
+ end
@@ -16,16 +16,28 @@ module SleepingKingStudios::Tools
16
16
  :require_each
17
17
  end
18
18
 
19
+ # @param deprecation_caller_depth [Integer] The number of backtrace lines to
20
+ # display when outputting a deprecation warning.
19
21
  # @param deprecation_strategy [String] The name of the strategy used when
20
22
  # deprecated code is called. Must be 'ignore', 'raise', or 'warn'.
21
- def initialize(deprecation_strategy: nil)
23
+ def initialize(
24
+ deprecation_caller_depth: nil,
25
+ deprecation_strategy: nil
26
+ )
22
27
  super()
23
28
 
29
+ @deprecation_caller_depth =
30
+ deprecation_caller_depth ||
31
+ ENV.fetch('DEPRECATION_CALLER_DEPTH', '3').to_i
24
32
  @deprecation_strategy =
25
33
  deprecation_strategy || ENV.fetch('DEPRECATION_STRATEGY', 'warn')
26
34
  end
27
35
 
28
- # @return [String] The current deprecation strategy.
36
+ # @return [Integer] the number of backtrace lines to display when outputting
37
+ # a deprecation warning.
38
+ attr_reader :deprecation_caller_depth
39
+
40
+ # @return [String] the current deprecation strategy.
29
41
  attr_reader :deprecation_strategy
30
42
 
31
43
  # @overload deprecate(name, message: nil)
@@ -92,19 +104,21 @@ module SleepingKingStudios::Tools
92
104
 
93
105
  str = format % args
94
106
  str << ' ' << message if message
95
-
96
- str << "\n called from #{external_caller}"
107
+ str << format_caller
97
108
 
98
109
  Kernel.warn str
99
110
  end
100
111
 
101
- def external_caller
102
- caller.find do |line|
103
- !(
104
- line.include?('forwardable.rb') ||
105
- line.include?('sleeping_king_studios-tools')
106
- )
112
+ def format_caller
113
+ lines = caller
114
+ start = lines.find_index do |line|
115
+ !line.include?('forwardable.rb') &&
116
+ !line.include?('sleeping_king_studios-tools')
107
117
  end
118
+
119
+ lines[start...(start + deprecation_caller_depth)]
120
+ .map { |line| "\n called from #{line}" }
121
+ .join
108
122
  end
109
123
  end
110
124
  end
@@ -168,7 +168,7 @@ module SleepingKingStudios::Tools
168
168
  def require_hash!(value)
169
169
  return if hash?(value)
170
170
 
171
- raise ArgumentError, 'argument must be a hash', caller[1..-1]
171
+ raise ArgumentError, 'argument must be a hash', caller[1..]
172
172
  end
173
173
  end
174
174
  end
@@ -108,7 +108,7 @@ module SleepingKingStudios::Tools
108
108
  # @return [Array<String>] The digits of the decomposed integer,
109
109
  # represented as a bigendian array of strings.
110
110
  def digits(integer, base: 10)
111
- integer.to_s(base).split('')
111
+ integer.to_s(base).chars
112
112
  end
113
113
 
114
114
  # Returns true if the object is an Integer.
@@ -167,7 +167,7 @@ module SleepingKingStudios::Tools
167
167
 
168
168
  return value.to_s if value.is_a?(Symbol)
169
169
 
170
- raise ArgumentError, 'argument must be a string', caller[1..-1]
170
+ raise ArgumentError, 'argument must be a string', caller[1..]
171
171
  end
172
172
  end
173
173
  end
@@ -18,6 +18,7 @@ module SleepingKingStudios::Tools
18
18
  # ActiveSupport::Inflector .
19
19
  def initialize(deprecation_strategy: nil, inflector: nil)
20
20
  @array_tools = ::SleepingKingStudios::Tools::ArrayTools.new
21
+ @assertions = ::SleepingKingStudios::Tools::Assertions.new
21
22
  @core_tools = ::SleepingKingStudios::Tools::CoreTools.new(
22
23
  deprecation_strategy: deprecation_strategy
23
24
  )
@@ -30,6 +31,8 @@ module SleepingKingStudios::Tools
30
31
 
31
32
  attr_reader :array_tools
32
33
 
34
+ attr_reader :assertions
35
+
33
36
  attr_reader :core_tools
34
37
 
35
38
  attr_reader :hash_tools
@@ -47,7 +47,7 @@ module SleepingKingStudios::Tools::Toolbox
47
47
 
48
48
  word = word.to_s.gsub(/(\b|[_-])([a-z])/) { Regexp.last_match(2).upcase }
49
49
 
50
- (uppercase_first_letter ? word[0].upcase : word[0].downcase) + word[1..-1]
50
+ (uppercase_first_letter ? word[0].upcase : word[0].downcase) + word[1..]
51
51
  end
52
52
 
53
53
  # rubocop:disable Metrics/AbcSize
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools/toolbox'
4
+
5
+ module SleepingKingStudios::Tools::Toolbox
6
+ # Mixin for partially applying constructor parameters.
7
+ #
8
+ # @example
9
+ # class Character
10
+ # extend SleepingKingStudios::Tools::Toolbox::Subclass
11
+ #
12
+ # def initialize(*traits)
13
+ # @traits = traits
14
+ # end
15
+ #
16
+ # attr_reader :traits
17
+ # end
18
+ #
19
+ # Bard = Character.subclass(:charismatic, :musical)
20
+ #
21
+ # aoife = Bard.new(:sorcerous)
22
+ # aoife.traits
23
+ # #=> [:charismatic, :musical, :sorcerous]
24
+ module Subclass
25
+ # Creates a subclass with partially applied constructor parameters.
26
+ #
27
+ # @param class_arguments [Array] The arguments, if any, to apply to the
28
+ # constructor. These arguments will be added before any args passed
29
+ # directly to the constructor.
30
+ # @param class_keywords [Hash] The keywords, if any, to apply to the
31
+ # constructor. These keywords will be added before any kwargs passed
32
+ # directly to the constructor.
33
+ #
34
+ # @yield The block, if any, to pass to the constructor. This will be
35
+ # overriden by a block passed directly to the constructor.
36
+ #
37
+ # @return [Class] the generated subclass.
38
+ def subclass(*class_arguments, **class_keywords, &class_block) # rubocop:disable Metrics/MethodLength
39
+ subclass = Class.new(self)
40
+
41
+ subclass.define_method :initialize do |*args, **kwargs, &block|
42
+ super(
43
+ *class_arguments,
44
+ *args,
45
+ **class_keywords,
46
+ **kwargs,
47
+ &(block || class_block)
48
+ )
49
+ end
50
+
51
+ subclass
52
+ end
53
+ end
54
+ end
@@ -6,5 +6,16 @@ module SleepingKingStudios::Tools
6
6
  # Namespace for common objects or patterns that are useful across projects but
7
7
  # are larger than or do not fit the functional paradigm of the tools.*
8
8
  # pattern.
9
- module Toolbox; end
9
+ module Toolbox
10
+ autoload :ConstantMap,
11
+ 'sleeping_king_studios/tools/toolbox/constant_map'
12
+ autoload :Inflector,
13
+ 'sleeping_king_studios/tools/toolbox/inflector'
14
+ autoload :Mixin,
15
+ 'sleeping_king_studios/tools/toolbox/mixin'
16
+ autoload :SemanticVersion,
17
+ 'sleeping_king_studios/tools/toolbox/semantic_version'
18
+ autoload :Subclass,
19
+ 'sleeping_king_studios/tools/toolbox/subclass'
20
+ end
10
21
  end
@@ -13,13 +13,13 @@ module SleepingKingStudios
13
13
  # Major version.
14
14
  MAJOR = 1
15
15
  # Minor version.
16
- MINOR = 0
16
+ MINOR = 1
17
17
  # Patch version.
18
- PATCH = 2
18
+ PATCH = 0
19
19
  # Prerelease version.
20
- PRERELEASE = nil
20
+ PRERELEASE = :rc
21
21
  # Build metadata.
22
- BUILD = nil
22
+ BUILD = 0
23
23
  end
24
24
 
25
25
  # The current version of the gem.
@@ -7,12 +7,14 @@ module SleepingKingStudios
7
7
  module Tools
8
8
  autoload :Base, 'sleeping_king_studios/tools/base'
9
9
  autoload :ArrayTools, 'sleeping_king_studios/tools/array_tools'
10
+ autoload :Assertions, 'sleeping_king_studios/tools/assertions'
10
11
  autoload :CoreTools, 'sleeping_king_studios/tools/core_tools'
11
12
  autoload :HashTools, 'sleeping_king_studios/tools/hash_tools'
12
13
  autoload :IntegerTools, 'sleeping_king_studios/tools/integer_tools'
13
14
  autoload :ObjectTools, 'sleeping_king_studios/tools/object_tools'
14
15
  autoload :StringTools, 'sleeping_king_studios/tools/string_tools'
15
16
  autoload :Toolbelt, 'sleeping_king_studios/tools/toolbelt'
17
+ autoload :Toolbox, 'sleeping_king_studios/tools/toolbox'
16
18
  autoload :Version, 'sleeping_king_studios/tools/version'
17
19
  end
18
20
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sleeping_king_studios-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.0.rc.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob "Merlin" Smith
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-14 00:00:00.000000000 Z
11
+ date: 2022-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: byebug
@@ -58,42 +58,42 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '1.6'
61
+ version: '1.29'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '1.6'
68
+ version: '1.29'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rubocop-rake
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0.5'
75
+ version: '0.6'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0.5'
82
+ version: '0.6'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rubocop-rspec
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '2.1'
89
+ version: '2.10'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '2.1'
96
+ version: '2.10'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: simplecov
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -128,28 +128,14 @@ dependencies:
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '2.5'
131
+ version: '2.7'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '2.5'
139
- - !ruby/object:Gem::Dependency
140
- name: sleeping_king_studios-tasks
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - "~>"
144
- - !ruby/object:Gem::Version
145
- version: '0.4'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - "~>"
151
- - !ruby/object:Gem::Version
152
- version: '0.4'
138
+ version: '2.7'
153
139
  description: |
154
140
  A library of utility services and concerns to expand the functionality of
155
141
  core classes without polluting the global namespace.
@@ -166,6 +152,7 @@ files:
166
152
  - README.md
167
153
  - lib/sleeping_king_studios/tools.rb
168
154
  - lib/sleeping_king_studios/tools/array_tools.rb
155
+ - lib/sleeping_king_studios/tools/assertions.rb
169
156
  - lib/sleeping_king_studios/tools/base.rb
170
157
  - lib/sleeping_king_studios/tools/core_tools.rb
171
158
  - lib/sleeping_king_studios/tools/hash_tools.rb
@@ -179,6 +166,7 @@ files:
179
166
  - lib/sleeping_king_studios/tools/toolbox/inflector/rules.rb
180
167
  - lib/sleeping_king_studios/tools/toolbox/mixin.rb
181
168
  - lib/sleeping_king_studios/tools/toolbox/semantic_version.rb
169
+ - lib/sleeping_king_studios/tools/toolbox/subclass.rb
182
170
  - lib/sleeping_king_studios/tools/version.rb
183
171
  homepage: http://sleepingkingstudios.com
184
172
  licenses:
@@ -186,6 +174,7 @@ licenses:
186
174
  metadata:
187
175
  bug_tracker_uri: https://github.com/sleepingkingstudios/sleeping_king_studios-tools/issues
188
176
  source_code_uri: https://github.com/sleepingkingstudios/sleeping_king_studios-tools
177
+ rubygems_mfa_required: 'true'
189
178
  post_install_message:
190
179
  rdoc_options: []
191
180
  require_paths:
@@ -194,12 +183,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
194
183
  requirements:
195
184
  - - ">="
196
185
  - !ruby/object:Gem::Version
197
- version: 2.5.0
186
+ version: 2.7.0
198
187
  required_rubygems_version: !ruby/object:Gem::Requirement
199
188
  requirements:
200
- - - ">="
189
+ - - ">"
201
190
  - !ruby/object:Gem::Version
202
- version: '0'
191
+ version: 1.3.1
203
192
  requirements: []
204
193
  rubygems_version: 3.2.3
205
194
  signing_key: