key_dial 1.0.0 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80905cbb9d3d2375534b67233544440b17c3a9aa2e092089c304db1d7f48161e
4
- data.tar.gz: 6fca290e125cf5cc240e7d420b9bf504c0932bb31bf916ab30cb4fe2f73e4de0
3
+ metadata.gz: adcb8d79a70615b55ef7096cb21d1df639a35fa8e83e42eff35e59dbfa2b466a
4
+ data.tar.gz: 5eed7fe134f7bf05227012c4a0242a5c89cc76d8c62afacc978ff17a1e09e76a
5
5
  SHA512:
6
- metadata.gz: e1c107522a250b151d0ff974d5acf41712adfeba5e1294464cd689c97265d488497c385f4eb181695e607c2bd353b7250a96dba5de80e0fa60f02b3f87ba9744
7
- data.tar.gz: 523ccc88302b8ec64ae711b91b06556bf5a29d414accee9095ee1df0675ee37e7814628d7920cc13d9c0e7e871b5583954d7c6f5f6a930591ed49c403eeb9ec5
6
+ metadata.gz: 28a754fba139b866e22a2184782ceba09aaed757384b92aa3607d4f9bff0b551162818dfac4ea01919509d9625e030a2b27c33184ca8c9aa938076995e2f0613
7
+ data.tar.gz: cac3c202ca11e2c398d35cf89079e1ead4dc2e95e83f6aed0ea281b672f5b2916bbadd63d1241418c0f46cf757f3d6268f753f695770443873b725481a9f547f
@@ -0,0 +1,189 @@
1
+ module KeyDial
2
+
3
+ module Coercion
4
+
5
+ module Hashes
6
+
7
+ # Convert a Hash to a Struct. {a: 1, b: 2, c: 3} will become <Struct :a=1, :b=2, :c=3>
8
+ #
9
+ def to_struct(type_class = nil)
10
+ return Coercion::Structs.create(self, type_class)
11
+ end
12
+
13
+ def self.included(base)
14
+ base.extend ClassMethods
15
+ end
16
+
17
+ module ClassMethods
18
+
19
+ # Allows you to do Hash.from(obj) to create a Hash from any object intelligently.
20
+ #
21
+ def from(obj)
22
+ return obj if obj.is_a?(Hash)
23
+ return obj.to_hash if obj.is_a?(Array)
24
+ return obj.to_h if obj.is_a?(Struct)
25
+ return {0 => obj}
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+
32
+ module Arrays
33
+
34
+ # Convert an Array to a Hash, providing an alternative to the native to_h() method. to_hash() is more forgiving and avoids errors. ['a', 'b', 'c'] will become {0 => 'a', 1 => 'b', 2 => 'c'}
35
+ #
36
+ def to_hash
37
+ self.each_with_index.map { |k, i|
38
+ if k.is_a?(Array)
39
+ if k.empty?
40
+ [i, nil]
41
+ elsif k.size == 2
42
+ k # k in this case is a keyval pair, e.g. [k, v]
43
+ else
44
+ [i, k]
45
+ end
46
+ else
47
+ [i, k]
48
+ end
49
+ }.to_h
50
+ end
51
+
52
+ # Convert an Array to a Struct. ['a', 'b', 'c'] will become <Struct :'0'='a', :'1'='b', :'2'='c'>
53
+ #
54
+ # @param type_class If a sub-class of Struct is provided, this sub-class will be instantiated
55
+ #
56
+ def to_struct(type_class = nil)
57
+ return Coercion::Structs.create(self, type_class)
58
+ end
59
+
60
+ def self.included(base)
61
+ base.extend ClassMethods
62
+ end
63
+
64
+ module ClassMethods
65
+
66
+ # Allows you to do Array.from(obj) to create an Array from any object intelligently.
67
+ #
68
+ def from(obj)
69
+ return obj if obj.is_a?(Array)
70
+ return obj.to_a if obj.is_a?(Hash)
71
+ return obj.to_h.to_a if obj.is_a?(Struct)
72
+ return [obj]
73
+ end
74
+
75
+ end
76
+
77
+ end
78
+
79
+ module Structs
80
+
81
+ EMPTY = Struct.new(:'0').new.freeze
82
+
83
+ # Convert a Struct to another Struct.
84
+ #
85
+ # @param type_class If a sub-class of Struct is provided, the Struct will be converted to this sub-class
86
+ #
87
+ def to_struct(type_class = nil)
88
+ if type_class.is_a?(Class) && type_class < Struct
89
+ return Struct.from(self, type_class)
90
+ else
91
+ return self
92
+ end
93
+ end
94
+
95
+ def self.included(base)
96
+ base.extend ClassMethods
97
+ end
98
+
99
+ module ClassMethods
100
+
101
+ # Allows you to do Struct.from(obj) to instantiate a Struct using keys/values from any object intelligently.
102
+ #
103
+ # @param type_class If a sub-class of Struct is provided, this sub-class will be instantiated
104
+ #
105
+ def from(obj, type_class = nil)
106
+ if !obj.is_a?(Struct) && !obj.is_a?(Hash) && !obj.is_a?(Array) && type_class == nil
107
+ s = EMPTY.dup
108
+ s[0] = obj
109
+ return s
110
+ else
111
+ return Coercion::Structs.create(obj, type_class)
112
+ end
113
+ end
114
+
115
+ end
116
+
117
+ # Struct creation function not really meant to be used directly. Prefer Struct.from(obj).
118
+ #
119
+ # @param from_obj Keys/values from this object will be used to fill out the new Struct.
120
+ # @param type_class If a sub-class of Struct is provided, this sub-class will be instantiated
121
+ #
122
+ def self.create(from_obj, type_class = nil)
123
+ if from_obj.is_a?(Hash) || from_obj.is_a?(Array) || from_obj.is_a?(Struct)
124
+ return EMPTY.dup if from_obj.empty? && type_class == nil
125
+ from = from_obj
126
+ else
127
+ from = [from_obj]
128
+ end
129
+
130
+ # Has a specific type of Struct been specified?
131
+ if type_class.is_a?(Class) && type_class < Struct
132
+ if from.is_a?(type_class)
133
+ # Has an instantiation of that type of Struct been passed in? If so, just return it
134
+ return from
135
+ else
136
+ values = []
137
+ if from.is_a?(Array)
138
+ # Get as many elements of array as this Struct can handle - discard the rest
139
+ values = from.first(type_class.members.size)
140
+ else
141
+ # Not an Array, so must be another Struct or Hash
142
+ type_class.members.each { |key|
143
+ if from.key?(key)
144
+ # If the object has this expected key, use it
145
+ values << from[key]
146
+ else
147
+ # Otherwise, fill this key with nil
148
+ values << nil
149
+ # Keys in the from object which don't match expected keys are discarded
150
+ end
151
+ }
152
+ end
153
+ # values now contains a value or nil for each of this class's expected keys
154
+ return type_class.new(*values)
155
+ end
156
+ else
157
+ # Anonymous Struct
158
+ new_values = from.is_a?(Array) ? from : from.values
159
+ # Iterate over the keys of the from object
160
+ # (Array.keys is monkeypatched in)
161
+ new_keys = from.keys.each_with_index.map { |k, i|
162
+ if k.respond_to?(:to_sym) && k != ''
163
+ k.to_sym
164
+ elsif k.respond_to?(:to_s) && !k.nil?
165
+ k.to_s.to_sym
166
+ else
167
+ # If we can't construct a valid Struct key for this key, we discard the corresponding value
168
+ new_values.delete_at(i)
169
+ nil
170
+ end
171
+ }.reject(&:nil?)
172
+ if new_keys.size > 0
173
+ # Create anonymous Struct with the specified keys and values
174
+ return Struct.new(*new_keys).new(*new_values)
175
+ else
176
+ # Return the Empty Struct
177
+ return EMPTY.dup
178
+ end
179
+ end
180
+
181
+ rescue
182
+ return EMPTY.dup
183
+ end
184
+
185
+ end
186
+
187
+ end
188
+
189
+ end
@@ -1,76 +1,462 @@
1
- module KeyDial
2
-
3
- class KeyDialler
4
-
5
- @obj_with_keys
6
- @lookup
7
- @default = nil
8
-
9
- def initialize(obj_with_keys, *lookup)
10
- if obj_with_keys.respond_to?(:dig)
11
- @obj_with_keys = obj_with_keys
12
- else
13
- raise ArgumentError.new('HashDialler must be initialized on a Hash, Array or Struct, or an object that responds to :dig.')
14
- end
15
- @lookup = []
16
- if lookup.length > 0
17
- dial!(*lookup)
18
- end
19
- end
20
-
21
- # Adds a key to the list of nested keys to try, one level deeper.
22
- #
23
- # @param keys The key to add. Multiple arguments would add multiple keys.
24
- #
25
- def dial!(*keys)
26
- #unless key.is_a(Symbol) || key.is_a(String)
27
- @lookup += keys
28
- return self
29
- end
30
-
31
- # Digs into the object to the list of keys specified by dialling. Returns nil or default if specified.
32
- #
33
- # @param default What to return if no key is found.
34
- #
35
- def call(default = nil)
36
- begin
37
- value = @obj_with_keys.dig(*@lookup)
38
- rescue
39
- value = default
40
- end
41
- return value
42
- end
43
-
44
- # Return the original keyed object.
45
- def hangup
46
- return @obj_with_keys
47
- end
48
-
49
- # Remove keys from the dialling list.
50
- #
51
- # @param keys If specified, these keys would be removed from wherever they appear in the dialling list. Otherwise, the last added key is removed.
52
- #
53
- def undial!(*keys)
54
- if keys.length > 0
55
- @lookup -= keys
56
- elsif @lookup.length > 0
57
- @lookup.pop
58
- end
59
- return self
60
- end
61
-
62
- # The preferred way to build up your dialling list. Access KeyDialler as if it were a keyed object, e.g. keydialler[a][b][c]. This does not actually return any value, rather it dials those keys (awaiting a call).
63
- #
64
- def [](key)
65
- return dial!(key)
66
- end
67
- def +(key)
68
- return dial!(key)
69
- end
70
- def -(key)
71
- return undial!(key)
72
- end
73
-
74
- end
75
-
76
- end
1
+ module KeyDial
2
+
3
+ class KeyDialler
4
+
5
+ DEFAULT_OBJECT = {}.freeze
6
+
7
+ @obj_with_keys
8
+ @lookup
9
+ @default
10
+
11
+ def initialize(obj_with_keys = DEFAULT_OBJECT, *lookup)
12
+ self.object = obj_with_keys
13
+ @lookup = []
14
+ @default = nil
15
+ if lookup.length > 0
16
+ dial!(*lookup)
17
+ end
18
+ end
19
+
20
+ attr_accessor :default
21
+
22
+ # Adds a key to the list of nested keys to try, one level deeper.
23
+ #
24
+ # @param keys_array The key(s) to add. Multiple arguments would add multiple keys.
25
+ #
26
+ def dial!(*keys_array)
27
+ keys_array = use_keys(keys_array)
28
+ @lookup += keys_array
29
+ return self
30
+ end
31
+
32
+ # Remove keys from the dialling list.
33
+ #
34
+ # @param keys_array If specified, these keys would be removed from wherever they appear in the dialling list. Otherwise, the last added key is removed.
35
+ #
36
+ def undial!(*keys_array)
37
+ keys_array = use_keys(keys_array)
38
+ if keys_array.length > 0
39
+ @lookup -= keys_array
40
+ elsif @lookup.length > 0
41
+ @lookup.pop
42
+ end
43
+ return self
44
+ end
45
+
46
+ # Digs into the object to the list of keys specified by dialling. Returns nil, or default if specified, if the key can't be found.
47
+ #
48
+ # @param default What to return if no key is found.
49
+ #
50
+ def set?
51
+ begin
52
+
53
+ value = @lookup.inject(@obj_with_keys) { |deep_obj, this_key|
54
+ # Has to be an object that can have keys
55
+ return false unless deep_obj.respond_to?(:[])
56
+
57
+ if deep_obj.respond_to?(:fetch)
58
+ # Hash, Array and Struct all respond to fetch
59
+ # We've monkeypatched fetch to Struct
60
+ if deep_obj.is_a?(Array)
61
+ # Check array separately as must fetch numeric key
62
+ return false unless Keys.index?(this_key)
63
+ end
64
+ next_obj = deep_obj.fetch(this_key, Keys::MISSING)
65
+ else
66
+ return false
67
+ end
68
+
69
+ # No need to go any further
70
+ return false if Keys::MISSING == next_obj
71
+
72
+ # Reinject value to next loop
73
+ next_obj
74
+ }
75
+
76
+ rescue
77
+ # If fetch throws a wobbly at any point, fail gracefully
78
+ return false
79
+ end
80
+ # No errors - yield the value if desired
81
+ if block_given?
82
+ yield(value)
83
+ end
84
+ # Return true
85
+ return true
86
+ end
87
+
88
+ def fetch(default = (default_skipped = true; @default))
89
+ value = nil
90
+ if set? { |exists| value = exists }
91
+ return value
92
+ else
93
+ if block_given?
94
+ warn 'warning: block supersedes default value argument' if !default_skipped
95
+ return yield
96
+ else
97
+ return default
98
+ end
99
+ end
100
+ end
101
+
102
+ def call(default = (default_skipped = true; @default))
103
+ value = nil
104
+ if set? { |exists| value = exists }
105
+ # Key exists at key list, and we've captured it to value
106
+ if block_given?
107
+ # If block given, yield value to the block
108
+ return yield(value)
109
+ else
110
+ # Otherwise, just return the value
111
+ return value
112
+ end
113
+ else
114
+ # Key does not exist
115
+ if default.is_a?(Proc)
116
+ # If default provided is a Proc, don't just return this as a value - run it
117
+ return default.call
118
+ else
119
+ # Return the default
120
+ return default
121
+ end
122
+ end
123
+ end
124
+
125
+ # Return the array of keys dialled so far.
126
+ def keys
127
+ return @lookup
128
+ end
129
+
130
+ # Set the key list directly.
131
+ def keys=(keys_array)
132
+ if keys_array.is_a?(Array)
133
+ @lookup = []
134
+ dial!(*keys_array)
135
+ else
136
+ raise ArgumentError, 'Key list must be set to an array.'
137
+ end
138
+ end
139
+
140
+ # Return the original keyed object.
141
+ def object
142
+ return @obj_with_keys
143
+ end
144
+ alias hangup object
145
+
146
+ # Set/change the keyed object.
147
+ #
148
+ # @param obj_with_keys The object that should be dialled, e.g. a Hash, Array or Struct.
149
+ #
150
+ def object=(obj_with_keys)
151
+ obj_with_keys = DEFAULT_OBJECT if obj_with_keys.nil?
152
+ if obj_with_keys.respond_to?(:fetch)
153
+ @obj_with_keys = obj_with_keys
154
+ else
155
+ raise ArgumentError, 'KeyDialler must be used on a Hash, Array or Struct, or object that responds to the fetch method.'
156
+ end
157
+ end
158
+
159
+ # The preferred way to build up your dialling list. Access KeyDialler as if it were a keyed object, e.g. keydialler[a][b][c]. This does not actually return any value, rather it dials those keys (awaiting a call).
160
+ #
161
+ # @param key The key to dial, determined via [key] syntax
162
+ #
163
+ def [](key)
164
+ return dial!(key)
165
+ end
166
+
167
+ # The preferred way to set a value at the end of a set of keys. Will create or coerce intermediate keys if required.
168
+ #
169
+ # @param key_obj The last key to dial, determined via [key] syntax
170
+ # @param value_obj What to set it to.
171
+ #
172
+ def []=(key_obj, value_obj)
173
+ # Dial the key to be set - @lookup can never be empty
174
+ dial!(key_obj)
175
+ # Set the value
176
+ return set!(value_obj)
177
+ end
178
+
179
+ # The preferred way to add to an array at the end of a set of keys. Will create or coerce the array if required.
180
+ #
181
+ # @param value_obj The value to add to the array at the dialled location.
182
+ #
183
+ def <<(value_obj)
184
+ array = call(Keys::MISSING)
185
+ # Dial the next array key index - @lookup can never be empty before set!()
186
+ if array.is_a?(Array) || array.is_a?(Hash) || array.is_a?(Struct)
187
+ dial!(array.size)
188
+ elsif array == Keys::MISSING
189
+ dial!(0)
190
+ else
191
+ dial!(1)
192
+ end
193
+ return set!(value_obj)
194
+ end
195
+
196
+ # Set any deep key. If keys along the way don't exist, empty Hashes or Arrays will be created. Warning: this method will try to coerce your main object to match the structure implied by your keys.
197
+ #
198
+ # @param key_obj The key to alter, determined via [key_obj] syntax
199
+ # @param value_obj What to set this key to, determined via [key_obj] = value_obj syntax
200
+ #
201
+ def set!(value_obj)
202
+ insist!()
203
+ @lookup[0...-1].inject(@obj_with_keys) { |deep_obj, this_key|
204
+ deep_obj[this_key]
205
+ }[@lookup[-1]] = value_obj
206
+ end
207
+
208
+ # Forces the current list of dialled keys to be instantiated on the object.
209
+ #
210
+ # @param type_class The object class that must be instantiated at the end of the key list. Either Hash, Array or Struct (or Struct::Type). Will create a new object if the key does not exist, or coerce existing values if it does.
211
+ #
212
+ def insist!(type_class = (type_class_skipped = true; nil))
213
+
214
+ return @obj_with_keys if @lookup.empty?
215
+ # Hashes can be accessed at [Object] of any kind
216
+ # Structs can be accessed at [String] and [Symbol], and [Integer] for the nth member (or [Float] which rounds down)
217
+ # Arrays can be accessed at [Integer] or [Float] which rounds down
218
+
219
+ index = 0
220
+ # Will run at least twice, as:
221
+ # Always runs once for @obj_with_keys itself
222
+ # Then at least one more time because @lookup is not empty
223
+ return @lookup.inject(@obj_with_keys) { |deep_obj, this_key|
224
+ last_index = index >= @lookup.size - 1
225
+
226
+ # this = object to be accessed
227
+ # key = key to access on this
228
+ # access = what kind of key is key
229
+
230
+ key = {
231
+ this: {
232
+ type: nil,
233
+ value: this_key
234
+ },
235
+ next: {
236
+ type: nil,
237
+ value: last_index ? Keys::MISSING : @lookup[index + 1]
238
+ },
239
+ last: {
240
+ type: nil,
241
+ value: index == 0 ? Keys::MISSING : @lookup[index - 1]
242
+ }
243
+ }
244
+
245
+ key.each { |pos, _|
246
+ if Keys.index?(key[pos][:value])
247
+ key[pos][:type] = :index
248
+ key[pos][:max] = key[pos][:value].magnitude.floor + (key[pos][:value] <= -1 ? 0 : 1)
249
+ else
250
+ key[pos][:type] = :object
251
+ key[pos][:type] = :string if key[pos][:value].is_a?(String)
252
+ key[pos][:type] = :symbol if key[pos][:value].is_a?(Symbol)
253
+ end
254
+ }
255
+
256
+ reconstruct = false
257
+
258
+ # Ensure this object is a supported type - always true for index == 0 i.e. @obj_with_keys itself
259
+ if !(deep_obj.respond_to?(:fetch) && deep_obj.respond_to?(:[]))
260
+ # Not a supported type! e.g. a string
261
+ if key[:this][:type] == :index
262
+ # If we'll access an array here, re-embed the unsupported object in an array as [0 => original]
263
+ deep_obj = Array.new(key[:this][:max] - 1).unshift(deep_obj)
264
+ else
265
+ # Otherwise, embed the unsupported object in a hash with the key 0
266
+ deep_obj = {0 => deep_obj}
267
+ end
268
+ # Will never run on @obj_with_keys itself
269
+ reconstruct = true
270
+ else
271
+ # Supported type, but what if this doesn't accept that kind of key? Then...
272
+
273
+ # "You asked for it!"(TM)
274
+ # In a Struct, if accessing a member that doesn't exist, we'll replace the struct with a redefined anonymous one containing the members you wanted. This is dangerous but it's your fault.
275
+ if deep_obj.is_a?(Struct)
276
+ if key[:this][:type] == :string || key[:this][:type] == :symbol
277
+ if !deep_obj.members.include?(key[:this][:value].to_sym)
278
+ # You asked for it!
279
+ # Add the member you requested
280
+ new_members = deep_obj.members.push(key[:this][:value].to_sym)
281
+ deep_obj = Struct.new(*new_members).new(*deep_obj.values)
282
+ reconstruct = true
283
+ end
284
+ elsif key[:this][:type] == :index
285
+ if key[:this][:max] > deep_obj.size
286
+ # You asked for it!
287
+ # Create new numeric members up to key requested
288
+ if key[:this][:value] <= -1
289
+ range = 0..((key[:this][:max] - deep_obj.size) - 1)
290
+ else
291
+ range = deep_obj.size..(key[:this][:max] - 1)
292
+ end
293
+ new_keys = (range).to_a.map { |num| num.to_s.to_sym }
294
+ # Shove them in
295
+ if key[:this][:value] <= -1
296
+ # Prepend
297
+ new_members = new_keys.concat(deep_obj.members)
298
+ new_values = Array.new(new_keys.size - deep_obj.values.size, nil).concat(deep_obj.values)
299
+ else
300
+ # Append
301
+ new_members = deep_obj.members.concat(new_keys)
302
+ new_values = deep_obj.values
303
+ end
304
+ deep_obj = Struct.new(*new_members).new(*new_values)
305
+ reconstruct = true
306
+ end
307
+ end
308
+ end
309
+
310
+ # "You asked for it!"(TM)
311
+ # If accessing an array with a key that doesn't exist, we'll add elements to the array or change the array to a hash. This is dangerous but it's your fault.
312
+ if deep_obj.is_a?(Array)
313
+ if key[:this][:type] == :index
314
+ if key[:this][:value] <= -1 && key[:this][:max] > deep_obj.size
315
+ # You asked for it!
316
+ # The only time an Array will break is if you try to set a negative key larger than the size of the array. In this case we'll prepend your array with nils.
317
+ deep_obj = Array.new(key[:this][:max] - deep_obj.size, nil).concat(deep_obj)
318
+ reconstruct = true
319
+ end
320
+ else
321
+ # You asked for it!
322
+ # Trying to access non-numeric key on an array, so will convert the array into a hash with integer keys.
323
+ deep_obj = deep_obj.each_with_index.map { |v, index| [index, v] }.to_h
324
+ reconstruct = true
325
+ end
326
+ end
327
+
328
+ end
329
+
330
+ if reconstruct
331
+ # Go back and reinject this altered value into the array
332
+ @lookup[0...(index-1)].inject(@obj_with_keys) { |deep_obj2, this_key2|
333
+ deep_obj2[this_key2]
334
+ }[key[:last][:value]] = deep_obj
335
+ end
336
+
337
+ # Does this object already have this key?
338
+ if !deep_obj.dial[key[:this][:value]].set?
339
+ # If not, create empty array/hash dependant on upcoming key
340
+ if type_class_skipped
341
+ if key[:next][:type] == :index
342
+ if key[:next][:value] <= -1
343
+ # Ensure new array big enough to address a negative key
344
+ deep_obj[key[:this][:value]] = Array.new(key[:next][:max])
345
+ else
346
+ # Otherwise, can just create an empty array
347
+ deep_obj[key[:this][:value]] = []
348
+ end
349
+ else
350
+ # Create an empty hash awaiting keys/values
351
+ deep_obj[key[:this][:value]] = {}
352
+ end
353
+ else
354
+ if type_class == Array
355
+ deep_obj[key[:this][:value]] = []
356
+ elsif type_class == Hash
357
+ deep_obj[key[:this][:value]] = {}
358
+ elsif type_class == Struct
359
+ # Why would you do this?
360
+ deep_obj[key[:this][:value]] = Coercion::Structs::EMPTY.dup
361
+ elsif type_class.is_a?(Class) && type_class < Struct
362
+ deep_obj[key[:this][:value]] = type_class.new
363
+ elsif type_class.respond_to?(:new)
364
+ begin
365
+ deep_obj[key[:this][:value]] = type_class.new
366
+ rescue
367
+ deep_obj[key[:this][:value]] = nil
368
+ end
369
+ else
370
+ deep_obj[key[:this][:value]] = nil
371
+ end
372
+ end
373
+ elsif !type_class_skipped && last_index && !deep_obj[key[:this][:value]].is_a?(type_class)
374
+ #Key already exists, but we must ensure it's of the right type
375
+ if type_class == Array
376
+ deep_obj[key[:this][:value]] = Array.from(deep_obj[key[:this][:value]])
377
+ elsif type_class == Hash
378
+ deep_obj[key[:this][:value]] = Hash.from(deep_obj[key[:this][:value]])
379
+ elsif type_class == Struct
380
+ # Why would you do this?
381
+ deep_obj[key[:this][:value]] = Struct.from(deep_obj[key[:this][:value]])
382
+ elsif type_class.is_a?(Class) && type_class < Struct
383
+ deep_obj[key[:this][:value]] = Struct.from(deep_obj[key[:this][:value]], type_class)
384
+ elsif type_class == String && deep_obj[key[:this][:value]].respond_to?(:to_s)
385
+ deep_obj[key[:this][:value]] = deep_obj[key[:this][:value]].to_s
386
+ elsif type_class == Symbol
387
+ if deep_obj[key[:this][:value]].respond_to?(:to_sym)
388
+ deep_obj[key[:this][:value]] = deep_obj[key[:this][:value]].to_s
389
+ elsif deep_obj[key[:this][:value]].respond_to?(:to_s)
390
+ deep_obj[key[:this][:value]] = deep_obj[key[:this][:value]].to_s.to_sym
391
+ else
392
+ warn "Could not coerce value to #{type_class}"
393
+ end
394
+ elsif type_class.respond_to?(:new)
395
+ begin
396
+ deep_obj[key[:this][:value]] = type_class.new
397
+ rescue
398
+ warn "Could not coerce value to #{type_class}"
399
+ end
400
+ else
401
+ warn "Could not coerce value to #{type_class}"
402
+ end
403
+ end
404
+
405
+ # Quit if this is the penultimate or last iteration
406
+ #next deep_obj if last_index
407
+
408
+ # Increment index manually
409
+ index += 1
410
+
411
+ # Before here, we must make sure we can access key on deep_obj
412
+ # Return the value at this key for the next part of inject loop
413
+ deep_obj[key[:this][:value]]
414
+
415
+ }
416
+
417
+ # Final access (and set) of last key in the @lookup - by this point should be guaranteed to work!
418
+ #if value_obj_skipped
419
+ # return obj_to_set[@lookup[-1]]
420
+ #else
421
+ # return obj_to_set[@lookup[-1]] = value_obj
422
+ #end
423
+
424
+ end
425
+
426
+ # Add a key to the dialling chain. If an array is passed, each item in the array will be added in order.
427
+ def +(key)
428
+ return dial!(*key)
429
+ end
430
+
431
+ # Remove keys that have been dialled.
432
+ #
433
+ # @param key If an integer n, the last n keys will be removed. Otherwise, all keys matching this argument will be removed from any point in the dialing chain. If an array is passed, each item in the array will be removed.
434
+ #
435
+ def -(key)
436
+ if key.is_a?(Integer) && key > 0
437
+ return key.times { undial! }
438
+ else
439
+ return undial!(*key)
440
+ end
441
+ end
442
+
443
+ # Private class method to reduce KeyDialler objects to their contained key arrays if a KeyDialler object itself is passed as a potential key to dial
444
+ private def use_keys(keys_array)
445
+ keys_array = [keys_array] if !keys_array.is_a?(Array)
446
+ keys_array.flatten
447
+ keys_return = []
448
+ keys_array.each { |key|
449
+ if key.is_a?(KeyDialler)
450
+ # Add returned keys inline (flattened into array)
451
+ keys_return += key.keys
452
+ else
453
+ # Add any other key as a whole object
454
+ keys_return.push(key)
455
+ end
456
+ }
457
+ return keys_return
458
+ end
459
+
460
+ end
461
+
462
+ end
@@ -1,3 +1,3 @@
1
- module KeyDial
2
- VERSION = "1.0.0"
3
- end
1
+ module KeyDial
2
+ VERSION = "1.1.0"
3
+ end
data/lib/key_dial.rb CHANGED
@@ -1,37 +1,145 @@
1
- require "key_dial/version"
2
- require "key_dial/key_dialler"
3
-
4
- module KeyDial
5
-
6
- # Called on a Hash, Array or Struct, returns a KeyDialler object.
7
- #
8
- # @param lookup Parameters to this method form initial keys to dial. This is unnecessary but works anyway. For simplicity, dial keys by accessing them as if KeyDialler were a keyed object itself.
9
- #
10
- def to_dial(*lookup)
11
- return KeyDialler.new(self, *lookup)
12
- end
13
-
14
- alias_method :dial, :to_dial
15
-
16
- # Called directly on a keyed object, immediately dials and calls the keys specified as arguments. Returns the value found, or nil.
17
- #
18
- # @param lookup The keys to attempt to retrieve.
19
- #
20
- def call(*lookup)
21
- return KeyDialler.new(self, *lookup).call
22
- end
23
-
24
- end
25
-
26
- # Extend core class so that .dial can be called seamlessly
27
- class Hash
28
- include KeyDial
29
- end
30
-
31
- class Array
32
- include KeyDial
33
- end
34
-
35
- class Struct
36
- include KeyDial
37
- end
1
+ require "key_dial/version"
2
+ require "key_dial/key_dialler"
3
+ require "key_dial/coercion"
4
+
5
+ module KeyDial
6
+
7
+ # Called on a Hash, Array or Struct, returns a KeyDialler object, ready to dial keys against that Hash, Array or Struct.
8
+ #
9
+ # @param lookup Parameters to this method form initial keys to dial. This is unnecessary but works anyway. For simplicity, dial keys by accessing them as if KeyDialler were a keyed object itself.
10
+ #
11
+ def to_dial(*lookup)
12
+ return KeyDialler.new(self, *lookup)
13
+ end
14
+
15
+ alias_method :dial, :to_dial
16
+
17
+ # Called directly on a keyed object, immediately dials and calls the keys specified as arguments. Returns the value found, or nil. A default cannot be specified.
18
+ #
19
+ # @param lookup The keys to attempt to retrieve.
20
+ #
21
+ def call(*lookup)
22
+ return KeyDialler.new(self, *lookup).call
23
+ end
24
+
25
+ end
26
+
27
+ # Extend core classes so that .dial can be called seamlessly
28
+ class Hash
29
+ include KeyDial
30
+ include KeyDial::Coercion::Hashes
31
+
32
+ alias to_array to_a
33
+ alias to_hash to_h
34
+ end
35
+
36
+ # Bring Array and Struct into parity with Hash for key? and fetch
37
+ # Will not redefine these methods if they already exist, either from some future Ruby version or another gem
38
+
39
+ class Array
40
+ include KeyDial
41
+ include KeyDial::Coercion::Arrays
42
+
43
+ # Returns true if this Array has the specified index.
44
+ def key?(key_obj)
45
+ if key_obj.is_a?(Numeric) && key_obj.respond_to?(:to_i)
46
+ key = key_obj.to_i
47
+ return key.magnitude + (key <= -1 ? 0 : 1) <= self.size
48
+ else
49
+ return false
50
+ end
51
+ end if !method_defined?(:key?)
52
+
53
+ # Returns an Array of all the valid indices for this Array
54
+ def keys
55
+ if self.size > 0
56
+ return Array(0..(self.size - 1))
57
+ else
58
+ return []
59
+ end
60
+ end
61
+
62
+ alias values to_ary
63
+ alias to_array to_a
64
+
65
+ end
66
+
67
+ class Struct
68
+ include KeyDial
69
+ include KeyDial::Coercion::Structs
70
+
71
+ # Extend Struct to give it a key? method
72
+ def key?(key_obj)
73
+ # These would be valid keys in struct[key] syntax
74
+ if key_obj.is_a?(Symbol)
75
+ key = key_obj
76
+ elsif key_obj.is_a?(String)
77
+ key = key_obj.to_sym
78
+ elsif key_obj.is_a?(Numeric) && key_obj.respond_to?(:to_i)
79
+ key = key_obj.to_i
80
+ else
81
+ return false #raise TypeError, "no implicit conversion of #{key_obj.class} into Symbol"
82
+ end
83
+
84
+ if key.is_a?(Symbol)
85
+ # Does the struct have the identified key?
86
+ return self.members.include?(key)
87
+ elsif key.is_a?(Integer)
88
+ # Does the struct have this numbered key?
89
+ return key.magnitude + (key <= -1 ? 0 : 1) <= self.size
90
+ end
91
+ end if !method_defined?(:key?)
92
+
93
+ # Extend Struct to give it a fetch method
94
+ def fetch(key_obj, default = (default_skipped = true; nil))
95
+ if key?(key_obj)
96
+ # Use key? method to check this struct has the requested key
97
+ # key? method ensures that key_obj is valid inside struct[key] syntax
98
+ return self[key_obj]
99
+ else
100
+ # Struct doesn't contain this key - proceed to defaults
101
+ if block_given?
102
+ # Warn if both block and default supplied
103
+ warn 'warning: block supersedes default value argument' if !default_skipped
104
+ # Return result of block as default
105
+ return yield(key_obj)
106
+ elsif !default_skipped
107
+ return default
108
+ else
109
+ raise KeyError, "key not found: #{key_obj.to_s}"
110
+ end
111
+ end
112
+ end if !method_defined?(:fetch)
113
+
114
+ alias keys members
115
+ alias to_array to_a
116
+ alias to_hash to_h
117
+
118
+ # Structs are not empty by definition
119
+ def empty?; false; end
120
+
121
+ end
122
+
123
+ # Ability to create anonymous key lists (on no particular object) with Keys[a][b][c]
124
+ module Keys
125
+
126
+ class Missing; end
127
+ MISSING = Missing.new.freeze
128
+ MISSING.freeze
129
+
130
+ # Create a new key list (KeyDialler) object using the syntax Keys[...][...]
131
+ def self.[](first_key)
132
+ return KeyDial::KeyDialler.new(nil, first_key)
133
+ end
134
+
135
+ # Checks if a key is a valid numeric index, i.e. can be used in the syntax object[index]
136
+ #
137
+ # @param key The key to check.
138
+ #
139
+ # @return True if the key is a valid numeric index, otherwise false.
140
+ #
141
+ def self.index?(key)
142
+ return key.is_a?(Numeric) && key.respond_to?(:to_i)
143
+ end
144
+
145
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: key_dial
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Convincible
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-09 00:00:00.000000000 Z
11
+ date: 2019-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.17'
19
+ version: '2.0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.17'
26
+ version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '12.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '12.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +52,48 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-nav
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
55
97
  description: 'Avoid all errors when accessing (deeply nested) Hash, Array or Struct
56
98
  keys. Safer than dig(), as will quietly return nil (or your default) if the keys
57
99
  requested are invalid for any reason at all. Bonus: you don''t even need to fiddle
@@ -63,17 +105,8 @@ executables: []
63
105
  extensions: []
64
106
  extra_rdoc_files: []
65
107
  files:
66
- - ".gitignore"
67
- - ".rspec"
68
- - ".travis.yml"
69
- - Gemfile
70
- - Gemfile.lock
71
- - README.md
72
- - Rakefile
73
- - bin/console
74
- - bin/setup
75
- - key_dial.gemspec
76
108
  - lib/key_dial.rb
109
+ - lib/key_dial/coercion.rb
77
110
  - lib/key_dial/key_dialler.rb
78
111
  - lib/key_dial/version.rb
79
112
  homepage: https://github.com/ConvincibleMedia/ruby-gem-key_dial
@@ -87,14 +120,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
87
120
  requirements:
88
121
  - - ">="
89
122
  - !ruby/object:Gem::Version
90
- version: '2.3'
123
+ version: 1.8.6
91
124
  required_rubygems_version: !ruby/object:Gem::Requirement
92
125
  requirements:
93
126
  - - ">="
94
127
  - !ruby/object:Gem::Version
95
128
  version: '0'
96
129
  requirements: []
97
- rubygems_version: 3.0.0
130
+ rubygems_version: 3.0.6
98
131
  signing_key:
99
132
  specification_version: 4
100
133
  summary: Access (deeply nested) Hash, Array or Struct keys. Get the value, or nil/default
data/.gitignore DELETED
@@ -1,17 +0,0 @@
1
- .git
2
- .env
3
- *.lnk
4
- *.db
5
- *.ini
6
-
7
- /.bundle/
8
- /.yardoc
9
- /_yardoc/
10
- /coverage/
11
- /doc/
12
- /pkg/
13
- /spec/reports/
14
- /tmp/
15
-
16
- # rspec failure tracking
17
- .rspec_status
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.travis.yml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- sudo: false
3
- language: ruby
4
- cache: bundler
5
- rvm:
6
- - 2.4.3
7
- before_install: gem install bundler -v 1.17.2
data/Gemfile DELETED
@@ -1,6 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
-
5
- # Specify your gem's dependencies in hash_dial.gemspec
6
- gemspec
data/Gemfile.lock DELETED
@@ -1,35 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- key_dial (1.0.0)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- diff-lcs (1.3)
10
- rake (10.5.0)
11
- rspec (3.8.0)
12
- rspec-core (~> 3.8.0)
13
- rspec-expectations (~> 3.8.0)
14
- rspec-mocks (~> 3.8.0)
15
- rspec-core (3.8.0)
16
- rspec-support (~> 3.8.0)
17
- rspec-expectations (3.8.2)
18
- diff-lcs (>= 1.2.0, < 2.0)
19
- rspec-support (~> 3.8.0)
20
- rspec-mocks (3.8.0)
21
- diff-lcs (>= 1.2.0, < 2.0)
22
- rspec-support (~> 3.8.0)
23
- rspec-support (3.8.0)
24
-
25
- PLATFORMS
26
- ruby
27
-
28
- DEPENDENCIES
29
- bundler (~> 1.17)
30
- key_dial!
31
- rake (~> 10.0)
32
- rspec (~> 3.0)
33
-
34
- BUNDLED WITH
35
- 1.17.2
data/README.md DELETED
@@ -1,82 +0,0 @@
1
- # KeyDial (Ruby Gem)
2
-
3
- **Avoid all errors when accessing a deeply nested Hash key,** or Array or Struct key. KeyDial goes one step beyond Ruby 2.3's `dig()` method by quietly returning `nil` (or your default) if the keys requested are invalid for any reason, and never an error.
4
-
5
- In particular, if you try to access a key on a value that can't have keys, `dig()` will cause an error where KeyDial will not.
6
-
7
- ```ruby
8
- hash = {a: {b: {c: true}, d: 5}}
9
-
10
- hash.dig( :a, :d, :c) #=> TypeError: Integer does not have #dig method
11
- hash.call(:a, :d, :c) #=> nil
12
- hash.call(:a, :b, :c) #=> true
13
- ```
14
-
15
- **Bonus: you don't even need to fiddle with existing code.** If you have already written something to access a deeply nested key, just surround this with `dial` and `call` (rather than changing it to the form above as function parameters).
16
-
17
- ```ruby
18
- hash[:a][:d][:c] #=> TypeError: no implicit conversion of Symbol into Integer
19
-
20
- #hash → [:a][:d][:c]
21
- # ↓ ↓
22
- hash.dial[:a][:d][:c].call #=> nil
23
- ```
24
-
25
- **KeyDial will work on mixed objects**, such as structs containing arrays containing hashes. It can be called from any hash, array or struct.
26
-
27
- ## Explanation
28
-
29
- We use the concept of placing a phone-call: you can 'dial' any set of keys regardless of whether they exist (like entering a phone number), then finally place the 'call'. If the key is invalid for any reason you get nil/default (like a wrong number); otherwise you get the value (you're connected).
30
-
31
- This works by intermediating your request with a KeyDialler object. Trying to access keys on this object simply builds up a list of keys to use when you later place the 'call'. The call then `dig`s for the keys safely.
32
-
33
- ## Usage
34
-
35
- ```ruby
36
- require 'hash_dial'
37
- ```
38
-
39
- ### Use it like `dig()`
40
-
41
- If you want to follow this pattern, it works in the same way. You can't change the default return value when using this pattern.
42
-
43
- ```ruby
44
- array = [0, {a: [true, false], b: 'foo'}, 2]
45
-
46
- array.call(1, :a, 0) #=> true (i.e. array[1][:a][0])
47
- array.call(1, :b, 0) #=> nil (i.e. array[1][:b][0] doesn't exist)
48
- ```
49
-
50
- You can `call` on any Hash, Array or Struct after requiring this gem.
51
-
52
- Note that KeyDial does not treat strings as arrays. Trying to access a key on a string will return nil or your default. (This is also how `dig()` works.)
53
-
54
- ### Use key access syntax (allows default return value)
55
-
56
- ```ruby
57
- hash.dial[:a][4][:c].call # Returns the value at hash[:a][4][:c] or nil
58
- hash.dial[:a][4][:c].call('Ooops') # Returns the value at hash[:a][4][:c] or 'Ooops'
59
- ```
60
-
61
- You can `dial` on any Hash, Array or Struct after requiring this gem.
62
-
63
- ### Use the KeyDialler object
64
-
65
- If you don't do this all in one line, you can access the KeyDialler object should you want to manipulate it:
66
-
67
- ```ruby
68
- dialler = KeyDial::KeyDialler.new(struct) # Returns a KeyDialler object referencing struct
69
- dialler[:a] # Adds :a to the list of keys to dial (returns self)
70
- dialler.dial!(:b, :c) # Longhand way of adding more keys (returns self)
71
- dialler.undial! # Removes the last-added key (returns self)
72
- dialler[:c][:d] # Adds two more keys (returns self)
73
- dialler += :e # Adds yet one more (returns self)
74
- dialler -= :a # Removes all such keys from the list (returns self)
75
- # So far we have dialled [:b][:c][:d][:e]
76
- dialler.call # Returns the value at struct[:b][:c][:d][:e] or nil
77
- dialler.hangup # Returns the original keyed object by reference
78
- ```
79
-
80
- ## Note
81
-
82
- KeyDial is a generic version of the gem HashDial, replacing and deprecating it.
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task :default => :spec
data/bin/console DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "key_dial"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
data/key_dial.gemspec DELETED
@@ -1,29 +0,0 @@
1
-
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "key_dial/version"
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "key_dial"
8
- spec.version = KeyDial::VERSION
9
- spec.authors = ["Convincible"]
10
- spec.email = ["development@convincible.media"]
11
-
12
- spec.summary = "Access (deeply nested) Hash, Array or Struct keys. Get the value, or nil/default instead of any error. (Even safer than Ruby 2.3's dig method)."
13
- spec.description = "Avoid all errors when accessing (deeply nested) Hash, Array or Struct keys. Safer than dig(), as will quietly return nil (or your default) if the keys requested are invalid for any reason at all. Bonus: you don't even need to fiddle with existing code. If you have already written something to access a deep key (e.g. hash[:a][:b][:c]), just surround this with '.dial' and '.call'."
14
- spec.homepage = "https://github.com/ConvincibleMedia/ruby-gem-key_dial"
15
-
16
- # Specify which files should be added to the gem when it is released.
17
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
- end
21
- spec.bindir = "exe"
22
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
- spec.require_paths = ["lib"]
24
- spec.required_ruby_version = '>= 2.3'
25
-
26
- spec.add_development_dependency "bundler", "~> 1.17"
27
- spec.add_development_dependency "rake", "~> 10.0"
28
- spec.add_development_dependency "rspec", "~> 3.0"
29
- end