nrser 0.0.24 → 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/nrser/enumerable.rb +12 -6
- data/lib/nrser/hash.rb +182 -0
- data/lib/nrser/meta/props/base.rb +22 -0
- data/lib/nrser/meta/props/prop.rb +84 -12
- data/lib/nrser/meta/props.rb +36 -15
- data/lib/nrser/refinements/hash.rb +13 -0
- data/lib/nrser/refinements/pathname.rb +11 -0
- data/lib/nrser/refinements.rb +2 -0
- data/lib/nrser/spex.rb +68 -0
- data/lib/nrser/types/attrs.rb +83 -30
- data/lib/nrser/types/combinators.rb +54 -6
- data/lib/nrser/types/paths.rb +62 -2
- data/lib/nrser/types/type.rb +122 -2
- data/lib/nrser/types.rb +2 -0
- data/lib/nrser/version.rb +1 -1
- data/lib/nrser.rb +5 -1
- data/spec/nrser/hash/bury_spec.rb +31 -0
- data/spec/nrser/hash/guess_name_type_spec.rb +47 -0
- data/spec/nrser/types/attrs_spec.rb +41 -0
- data/spec/nrser/types/paths_spec.rb +69 -0
- data/spec/nrser/types_spec.rb +0 -10
- data/spec/spec_helper.rb +46 -1
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 150628e4ec282944b3bfa4d667e0c678cfe9b6e3
|
4
|
+
data.tar.gz: d31edbf013cde3c160d3105e0ac0770efae26e88
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d170a8aeba6ed6940aa53a0d92a8f674e6b0b0bcaed15b8808b43dcde4ae690665fbd7f4bad2bc7ae72b641e675dd3e67228d935f0e0ba18e30605385bec54da
|
7
|
+
data.tar.gz: c71d68a17bb981b5638f9c28521d093fbf69266aa94323e96969c38301ad4293ba65ca1857cc6efaacc938f46a4857faff8ddfa737b6a200cac11dc8914e49a9
|
data/lib/nrser/enumerable.rb
CHANGED
@@ -59,21 +59,29 @@ module NRSER
|
|
59
59
|
|
60
60
|
|
61
61
|
|
62
|
-
#
|
62
|
+
# Find all entries in an {Enumerable} for which `&block` returns a truthy
|
63
|
+
# value, then check the amount of results found against the
|
64
|
+
# {NRSER::Types.length} created from `bounds`, raising a {TypeError} if
|
65
|
+
# the results' length doesn't satisfy the bounds type.
|
63
66
|
#
|
64
|
-
# @param [
|
65
|
-
#
|
67
|
+
# @param [Enumerable] enum
|
68
|
+
# The entries to search and check.
|
69
|
+
#
|
70
|
+
# @param []
|
66
71
|
#
|
67
72
|
# @return [return_type]
|
68
73
|
# @todo Document return value.
|
69
74
|
#
|
75
|
+
# @raise
|
76
|
+
#
|
70
77
|
def find_bounded enum, bounds, &block
|
71
78
|
NRSER::Types.
|
72
79
|
length(bounds).
|
73
80
|
check(enum.find_all &block) { |type:, value:|
|
74
81
|
NRSER.dedent <<-END
|
75
82
|
|
76
|
-
Length of found elements (#{ value.length }) FAILED to
|
83
|
+
Length of found elements (#{ value.length }) FAILED to
|
84
|
+
satisfy #{ type.to_s }
|
77
85
|
|
78
86
|
Found:
|
79
87
|
#{ NRSER.indent value.pretty_inspect }
|
@@ -86,7 +94,6 @@ module NRSER
|
|
86
94
|
end # #find_bounded
|
87
95
|
|
88
96
|
|
89
|
-
|
90
97
|
# @todo Document find_only method.
|
91
98
|
#
|
92
99
|
# @param [type] arg_name
|
@@ -100,7 +107,6 @@ module NRSER
|
|
100
107
|
end # #find_only
|
101
108
|
|
102
109
|
|
103
|
-
|
104
110
|
# @todo Document to_h_by method.
|
105
111
|
#
|
106
112
|
# @param [type] arg_name
|
data/lib/nrser/hash.rb
CHANGED
@@ -291,4 +291,186 @@ module NRSER
|
|
291
291
|
}
|
292
292
|
end
|
293
293
|
|
294
|
+
|
295
|
+
# Eigenclass (Singleton Class)
|
296
|
+
# ========================================================================
|
297
|
+
#
|
298
|
+
class << self
|
299
|
+
|
300
|
+
|
301
|
+
|
302
|
+
# @todo Document select_map method.
|
303
|
+
#
|
304
|
+
# @param [type] arg_name
|
305
|
+
# @todo Add name param description.
|
306
|
+
#
|
307
|
+
# @return [return_type]
|
308
|
+
# @todo Document return value.
|
309
|
+
#
|
310
|
+
def select_map arg_name
|
311
|
+
# method body...
|
312
|
+
end # #select_map
|
313
|
+
|
314
|
+
|
315
|
+
# Guess which type of "name" key - strings or symbols - a hash (or other
|
316
|
+
# object that responds to `#keys` and `#empty`) uses.
|
317
|
+
#
|
318
|
+
# @param [#keys & #empty] keyed
|
319
|
+
# Hash or similar object that responds to `#keys` and `#empty` to guess
|
320
|
+
# about.
|
321
|
+
#
|
322
|
+
# @return [nil]
|
323
|
+
# If we can't determine the type of name keys used.
|
324
|
+
#
|
325
|
+
# @return [Class]
|
326
|
+
# If we can determine that {String} or {Symbol} keys are exclusively
|
327
|
+
# used returns that class.
|
328
|
+
#
|
329
|
+
def guess_name_type keyed
|
330
|
+
# We can't tell shit if the hash is empty
|
331
|
+
return nil if keyed.empty?
|
332
|
+
|
333
|
+
name_types = keyed.
|
334
|
+
keys.
|
335
|
+
map( &:class ).
|
336
|
+
select { |klass| klass == String || klass == Symbol }.
|
337
|
+
uniq
|
338
|
+
|
339
|
+
return name_types[0] if name_types.length == 1
|
340
|
+
|
341
|
+
# There are both string and symbol keys present, we can't guess
|
342
|
+
nil
|
343
|
+
end # #guess_key_type
|
344
|
+
|
345
|
+
|
346
|
+
# @todo Document bury method.
|
347
|
+
#
|
348
|
+
# @param [Hash] hash
|
349
|
+
# Hash to bury the value in.
|
350
|
+
#
|
351
|
+
# @param [Array | #to_s] key_path
|
352
|
+
# - When an {Array}, each entry is used exactly as-is for each key.
|
353
|
+
#
|
354
|
+
# - Otherwise, the `key_path` is converted to a string and split by
|
355
|
+
# `.` to produce the key array, and the actual keys used depend on
|
356
|
+
# the `parsed_key_type` option.
|
357
|
+
#
|
358
|
+
# @param [Object] value
|
359
|
+
# The value to set at the end of the path.
|
360
|
+
#
|
361
|
+
# @param [Class | :guess] parsed_key_type:
|
362
|
+
# How to handle parsed key path segments:
|
363
|
+
#
|
364
|
+
# - `String` - use the strings that naturally split from a parsed
|
365
|
+
# key path.
|
366
|
+
#
|
367
|
+
# Note that this is the *String class itself, **not** a value that
|
368
|
+
# is a String*.
|
369
|
+
#
|
370
|
+
# - `Symbol` - convert the strings that are split from the key path
|
371
|
+
# to symbols.
|
372
|
+
#
|
373
|
+
# Note that this is the *Symbol class itself, **not** a value that
|
374
|
+
# is a Symbol*.``
|
375
|
+
#
|
376
|
+
# - `:guess` (default) -
|
377
|
+
#
|
378
|
+
# @return [return_type]
|
379
|
+
# @todo Document return value.
|
380
|
+
#
|
381
|
+
def bury! hash,
|
382
|
+
key_path,
|
383
|
+
value,
|
384
|
+
parsed_key_type: :guess,
|
385
|
+
clobber: false
|
386
|
+
|
387
|
+
# Parse the key if it's not an array
|
388
|
+
unless key_path.is_a?( Array )
|
389
|
+
key_path = key_path.to_s.split '.'
|
390
|
+
|
391
|
+
# Convert the keys to symbols now if that's what we want to use
|
392
|
+
if parsed_key_type == Symbol
|
393
|
+
key_path.map! &:to_sym
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
_internal_bury! \
|
398
|
+
hash,
|
399
|
+
key_path,
|
400
|
+
value,
|
401
|
+
guess_key_type: ( parsed_key_type == :guess ),
|
402
|
+
clobber: clobber
|
403
|
+
end # #bury
|
404
|
+
|
405
|
+
|
406
|
+
|
407
|
+
private
|
408
|
+
# ========================================================================
|
409
|
+
|
410
|
+
|
411
|
+
# @todo Document _internal_bury! method.
|
412
|
+
#
|
413
|
+
# @param [type] arg_name
|
414
|
+
# @todo Add name param description.
|
415
|
+
#
|
416
|
+
# @return [return_type]
|
417
|
+
# @todo Document return value.
|
418
|
+
#
|
419
|
+
def _internal_bury! hash,
|
420
|
+
key_path,
|
421
|
+
value,
|
422
|
+
guess_key_type:,
|
423
|
+
clobber:
|
424
|
+
|
425
|
+
# Split the key path into the current key and the rest of the keys
|
426
|
+
key, *rest = key_path
|
427
|
+
|
428
|
+
# If we are guessing the key type and the hash uses some {Symbol}
|
429
|
+
# (and no {String}) keys then convert the key to a symbol.
|
430
|
+
if guess_key_type && guess_name_type( hash ) == Symbol
|
431
|
+
key = key.to_sym
|
432
|
+
end
|
433
|
+
|
434
|
+
# Terminating case: we're at the last segment
|
435
|
+
if rest.empty?
|
436
|
+
# Set the value
|
437
|
+
hash[key] = value
|
438
|
+
|
439
|
+
else
|
440
|
+
# Go deeper...
|
441
|
+
|
442
|
+
# See if there is a hash in place
|
443
|
+
unless hash[key].is_a?( Hash )
|
444
|
+
# There is not... so we need to do some figurin'
|
445
|
+
|
446
|
+
# If we're clobbering or the hash has no value, we're good:
|
447
|
+
# assign a new hash to set in
|
448
|
+
if clobber || ! hash.key?( key )
|
449
|
+
hash[key] = {}
|
450
|
+
|
451
|
+
else
|
452
|
+
# We've got an intractable state conflict; raise
|
453
|
+
raise NRSER::ConflictError.new squish <<-END
|
454
|
+
can not set key #{ key.inspect } due to conflicting value
|
455
|
+
#{ hash[key].inspect } in hash #{ hash.inspect } (:clobber
|
456
|
+
option not set)
|
457
|
+
END
|
458
|
+
|
459
|
+
end
|
460
|
+
end # unless hash[key].is_a?( Hash )
|
461
|
+
|
462
|
+
# Dive in...
|
463
|
+
bury! hash[key], rest, value
|
464
|
+
|
465
|
+
end # if rest.empty? / else
|
466
|
+
end # #_internal_bury!
|
467
|
+
|
468
|
+
|
469
|
+
# end private
|
470
|
+
|
471
|
+
|
472
|
+
|
473
|
+
end # class << self (Eigenclass)
|
474
|
+
|
475
|
+
|
294
476
|
end # module NRSER
|
@@ -1,3 +1,10 @@
|
|
1
|
+
# Refinements
|
2
|
+
# =======================================================================
|
3
|
+
|
4
|
+
require 'nrser/refinements'
|
5
|
+
using NRSER
|
6
|
+
|
7
|
+
|
1
8
|
module NRSER
|
2
9
|
module Meta
|
3
10
|
module Props
|
@@ -8,6 +15,21 @@ class Base
|
|
8
15
|
def initialize **values
|
9
16
|
initialize_props values
|
10
17
|
end
|
18
|
+
|
19
|
+
# @todo Prob wanna improve this at some point, but it's better than nothing.
|
20
|
+
#
|
21
|
+
# @return [String]
|
22
|
+
# a short string describing the instance.
|
23
|
+
#
|
24
|
+
def to_s
|
25
|
+
props_str = self.class.props( only_primary: true ).sort.map { |name, prop|
|
26
|
+
"#{ name }=#{ prop.get( self ).inspect }"
|
27
|
+
}.join ' '
|
28
|
+
|
29
|
+
<<-END.squish
|
30
|
+
#<#{ self.class.name } #{ props_str }>
|
31
|
+
END
|
32
|
+
end # #to_s
|
11
33
|
end # class Base
|
12
34
|
|
13
35
|
end # module Props
|
@@ -1,8 +1,9 @@
|
|
1
1
|
require 'nrser/no_arg'
|
2
2
|
|
3
3
|
require 'nrser/refinements'
|
4
|
-
|
4
|
+
using NRSER
|
5
5
|
|
6
|
+
require 'nrser/refinements/types'
|
6
7
|
using NRSER::Types
|
7
8
|
|
8
9
|
module NRSER
|
@@ -20,13 +21,15 @@ class Prop
|
|
20
21
|
name,
|
21
22
|
type: t.any,
|
22
23
|
default: NRSER::NO_ARG,
|
23
|
-
source: nil
|
24
|
+
source: nil,
|
25
|
+
to_data: nil
|
24
26
|
|
25
27
|
@defined_in = defined_in
|
26
28
|
@name = name
|
27
29
|
@type = t.make type
|
28
30
|
@source = source
|
29
31
|
@default = default
|
32
|
+
@to_data = to_data
|
30
33
|
|
31
34
|
if @source.nil?
|
32
35
|
@instance_variable_source = false
|
@@ -113,7 +116,17 @@ class Prop
|
|
113
116
|
if instance_variable_source?
|
114
117
|
instance.instance_variable_get source
|
115
118
|
else
|
116
|
-
|
119
|
+
case source
|
120
|
+
when String, Symbol
|
121
|
+
instance.send source
|
122
|
+
when Proc
|
123
|
+
instance.instance_exec &source
|
124
|
+
else
|
125
|
+
raise TypeError.squished <<-END
|
126
|
+
Expected `Prop#source` to be a String, Symbol or Proc;
|
127
|
+
found #{ source.inspect }
|
128
|
+
END
|
129
|
+
end
|
117
130
|
end
|
118
131
|
else
|
119
132
|
values(instance)[name]
|
@@ -166,26 +179,85 @@ class Prop
|
|
166
179
|
end # #set_from_hash
|
167
180
|
|
168
181
|
|
169
|
-
|
170
|
-
#
|
182
|
+
# Get the "data" value - a basic scalar or structure of hashes, arrays and
|
183
|
+
# scalars, suitable for JSON encoding, etc. - for the property from an
|
184
|
+
# instance.
|
171
185
|
#
|
172
|
-
#
|
173
|
-
#
|
186
|
+
# The process depends on the `to_data:` keyword provided at property
|
187
|
+
# declaration:
|
174
188
|
#
|
175
|
-
#
|
176
|
-
#
|
189
|
+
# 1. {nil} *default*
|
190
|
+
# - If the property value responds to `#to_data`, the result of
|
191
|
+
# invoking that method will be returned.
|
192
|
+
#
|
193
|
+
# **WARNING**
|
194
|
+
#
|
195
|
+
# This can cause infinite recursion if an instance has
|
196
|
+
# a property value that is also an instance of the same class (as
|
197
|
+
# as other more complicated scenarios that boil down to the same
|
198
|
+
# problem), but, really, what else would it do in this situation?
|
199
|
+
#
|
200
|
+
# This problem can be avoided by by providing a `to_data:` keyword
|
201
|
+
# when declaring the property that dictates how to handle it's value.
|
202
|
+
# In fact, that was the motivation for adding `to_data:`.
|
203
|
+
#
|
204
|
+
# - Otherwise, the value itself is returned, under the assumption that
|
205
|
+
# it is already suitable as data.
|
206
|
+
#
|
207
|
+
# 2. {Symbol} | {String}
|
208
|
+
# - The `to_data:` string or symbol is sent to the property value
|
209
|
+
# (the method with this name is called via {Object#send}).
|
210
|
+
#
|
211
|
+
# 3. {Proc}
|
212
|
+
# - The `to_data:` proc is called with the property value as the sole
|
213
|
+
# argument and the result is returned as the data.
|
214
|
+
#
|
215
|
+
# @param [NRSER::Meta::Props] instance
|
216
|
+
# Instance to get the property value form.
|
217
|
+
#
|
218
|
+
# @return [Object]
|
219
|
+
# Data representation of the property value (hopefully - the value itself
|
220
|
+
# is returned if we don't have any better options, see above).
|
221
|
+
#
|
222
|
+
# @raise [TypeError]
|
223
|
+
# If {@to_data} (provided via the `to_data:` keyword at property
|
224
|
+
# declaration) is anything other than {nil}, {String}, {Symbol} or {Proc}.
|
177
225
|
#
|
178
226
|
def to_data instance
|
179
227
|
value = get instance
|
180
228
|
|
181
|
-
|
182
|
-
|
229
|
+
case @to_data
|
230
|
+
when nil
|
231
|
+
if value.respond_to? :to_data
|
232
|
+
value.to_data
|
233
|
+
elsif type.respond_to? :to_data
|
234
|
+
type.to_data value
|
235
|
+
else
|
236
|
+
value
|
237
|
+
end
|
238
|
+
when Symbol, String
|
239
|
+
value.send @to_data
|
240
|
+
when Proc
|
241
|
+
@to_data.call value
|
183
242
|
else
|
184
|
-
|
243
|
+
raise TypeError.squished <<-END
|
244
|
+
Expected `@to_data` to be Symbol, String or Proc;
|
245
|
+
found #{ @to_data.inspect }
|
246
|
+
END
|
185
247
|
end
|
186
248
|
end # #to_data
|
187
249
|
|
188
250
|
|
251
|
+
# @return [String]
|
252
|
+
# a short string describing the instance.
|
253
|
+
#
|
254
|
+
def to_s
|
255
|
+
<<-END.squish
|
256
|
+
#<#{ self.class.name }
|
257
|
+
#{ @defined_in.name }##{ @name }:#{ @type }>
|
258
|
+
END
|
259
|
+
end # #to_s
|
260
|
+
|
189
261
|
|
190
262
|
private
|
191
263
|
|
data/lib/nrser/meta/props.rb
CHANGED
@@ -87,13 +87,16 @@ module Props
|
|
87
87
|
#
|
88
88
|
module ClassMethods
|
89
89
|
|
90
|
-
#
|
90
|
+
# Get a map of property names to property instances.
|
91
91
|
#
|
92
|
-
# @param [
|
93
|
-
#
|
92
|
+
# @param [Boolean] only_own:
|
93
|
+
# Don't include super-class properties.
|
94
94
|
#
|
95
|
-
# @
|
96
|
-
#
|
95
|
+
# @param [Boolean] only_primary:
|
96
|
+
# Don't include properties that have a {NRSER::Meta::Props::Prop#source}.
|
97
|
+
#
|
98
|
+
# @return [Hash{ Symbol => NRSER::Meta::Props::Prop }]
|
99
|
+
# Hash mapping property name to property instance.
|
97
100
|
#
|
98
101
|
def props only_own: false, only_primary: false
|
99
102
|
result = if !only_own && superclass.respond_to?(:props)
|
@@ -118,13 +121,17 @@ module Props
|
|
118
121
|
end # #own_props
|
119
122
|
|
120
123
|
|
121
|
-
#
|
124
|
+
# Define a property.
|
125
|
+
#
|
126
|
+
# @param [Symbol] name
|
127
|
+
# The name of the property.
|
122
128
|
#
|
123
|
-
# @param [
|
124
|
-
#
|
129
|
+
# @param [Hash{ Symbol => Object }] **opts
|
130
|
+
# Constructor options for {NRSER::Meta::Props::Prop}.
|
125
131
|
#
|
126
|
-
# @return [
|
127
|
-
#
|
132
|
+
# @return [NRSER::Meta::Props::Prop]
|
133
|
+
# The newly created prop, thought you probably don't need it (it's
|
134
|
+
# already all bound up on the class at this point), but why not?
|
128
135
|
#
|
129
136
|
def prop name, **opts
|
130
137
|
ref = NRSER::Meta::Props.get_props_ref self
|
@@ -153,6 +160,8 @@ module Props
|
|
153
160
|
# end
|
154
161
|
end
|
155
162
|
end
|
163
|
+
|
164
|
+
prop
|
156
165
|
end # #prop
|
157
166
|
|
158
167
|
|
@@ -212,6 +221,7 @@ module Props
|
|
212
221
|
map_values { |name, prop| prop.get self }
|
213
222
|
end # #to_h
|
214
223
|
|
224
|
+
|
215
225
|
# Create a "data" representation suitable for transport, storage, etc.
|
216
226
|
#
|
217
227
|
# The result is meant to consist of only basic data types and structures -
|
@@ -241,19 +251,30 @@ module Props
|
|
241
251
|
end # #to_data
|
242
252
|
|
243
253
|
|
244
|
-
|
254
|
+
|
255
|
+
# Language Inter-Op
|
256
|
+
# ---------------------------------------------------------------------
|
257
|
+
|
258
|
+
# Get a JSON {String} encoding the instance's data.
|
245
259
|
#
|
246
|
-
# @param [
|
247
|
-
#
|
260
|
+
# @param [Array] *args
|
261
|
+
# I really don't know. `#to_json` takes at last one argument, but I've
|
262
|
+
# had trouble finding a spec for it :/
|
248
263
|
#
|
249
|
-
# @return [
|
250
|
-
# @todo Document return value.
|
264
|
+
# @return [String]
|
251
265
|
#
|
252
266
|
def to_json *args
|
253
267
|
to_data.to_json *args
|
254
268
|
end # #to_json
|
255
269
|
|
256
270
|
|
271
|
+
# Get a YAML {String} encoding the instance's data.
|
272
|
+
#
|
273
|
+
# @param [Array] *args
|
274
|
+
# I really don't know... whatever {YAML.dump} sends to it i guess.
|
275
|
+
#
|
276
|
+
# @return [String]
|
277
|
+
#
|
257
278
|
def to_yaml *args
|
258
279
|
to_data.to_yaml *args
|
259
280
|
end
|
@@ -76,5 +76,18 @@ module NRSER
|
|
76
76
|
NRSER.map_keys self, &block
|
77
77
|
end
|
78
78
|
|
79
|
+
|
80
|
+
# See {NRSER.bury!}
|
81
|
+
def bury! key_path,
|
82
|
+
value,
|
83
|
+
parsed_key_type: :guess,
|
84
|
+
clobber: false
|
85
|
+
NRSER.bury! self,
|
86
|
+
key_path,
|
87
|
+
value,
|
88
|
+
parsed_key_type: parsed_key_type,
|
89
|
+
clobber: clobber
|
90
|
+
end
|
91
|
+
|
79
92
|
end # refine ::Hash
|
80
93
|
end # NRSER
|
@@ -33,5 +33,16 @@ module NRSER
|
|
33
33
|
super pattern, replacement
|
34
34
|
end
|
35
35
|
end
|
36
|
+
|
37
|
+
|
38
|
+
# Just returns `self`. Implemented to match the {String#to_pn} API so it
|
39
|
+
# can be called on an argument that may be either one.
|
40
|
+
#
|
41
|
+
# @return [Pathname]
|
42
|
+
#
|
43
|
+
def to_pn
|
44
|
+
Pathname.new self
|
45
|
+
end
|
46
|
+
|
36
47
|
end # Pathname
|
37
48
|
end # NRSER
|
data/lib/nrser/refinements.rb
CHANGED
data/lib/nrser/spex.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
##############################################################################
|
2
|
+
# RSpec helpers, shared examples, and other goodies.
|
3
|
+
#
|
4
|
+
# This file is *not* required by default when `nrser` is since it **defines
|
5
|
+
# global methods** and is not needed unless you're in [Rspec][].
|
6
|
+
#
|
7
|
+
# [Rspec]: http://rspec.info/
|
8
|
+
#
|
9
|
+
##############################################################################
|
10
|
+
|
11
|
+
|
12
|
+
# Helpers
|
13
|
+
# =====================================================================
|
14
|
+
|
15
|
+
# Merge "expectation" hashes by appending all clauses for each state.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
#
|
19
|
+
#
|
20
|
+
# @param [Array<Hash>] *expectations
|
21
|
+
# Splat of "expectation" hashes - see the examples.
|
22
|
+
#
|
23
|
+
def merge_expectations *expectations
|
24
|
+
Hash.new { |result, state|
|
25
|
+
result[state] = []
|
26
|
+
}.tap { |result|
|
27
|
+
expectations.each { |ex|
|
28
|
+
ex.each { |state, clauses|
|
29
|
+
result[state] += clauses.to_a
|
30
|
+
}
|
31
|
+
}
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# Refine the subject of the parent scope by `#send`ing it a message and setting
|
37
|
+
# the result as the new subject.
|
38
|
+
#
|
39
|
+
# @param [Symbol | String] method_name
|
40
|
+
# Name of the method to send to.
|
41
|
+
#
|
42
|
+
# @param [Array] args*
|
43
|
+
# Arguments to send to the method.
|
44
|
+
#
|
45
|
+
# @return [void]
|
46
|
+
# Seems to be what RSpec's `subject` method returns.
|
47
|
+
#
|
48
|
+
def refine_subject method_name, *args
|
49
|
+
subject {
|
50
|
+
super().send method_name, *args
|
51
|
+
}
|
52
|
+
end # #refine_subject
|
53
|
+
|
54
|
+
|
55
|
+
# Shared Examples
|
56
|
+
# =====================================================================
|
57
|
+
|
58
|
+
shared_examples "expect subject" do |*expectations|
|
59
|
+
merge_expectations( *expectations ).each { |state, specs|
|
60
|
+
specs.each { |verb, noun|
|
61
|
+
it {
|
62
|
+
# like: is_expected.to(include(noun))
|
63
|
+
is_expected.send state, self.send(verb, noun)
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
end # is expected
|
68
|
+
|