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 +4 -4
- data/lib/key_dial/coercion.rb +189 -0
- data/lib/key_dial/key_dialler.rb +462 -76
- data/lib/key_dial/version.rb +3 -3
- data/lib/key_dial.rb +145 -37
- metadata +52 -19
- data/.gitignore +0 -17
- data/.rspec +0 -3
- data/.travis.yml +0 -7
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -35
- data/README.md +0 -82
- data/Rakefile +0 -6
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/key_dial.gemspec +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: adcb8d79a70615b55ef7096cb21d1df639a35fa8e83e42eff35e59dbfa2b466a
|
4
|
+
data.tar.gz: 5eed7fe134f7bf05227012c4a0242a5c89cc76d8c62afacc978ff17a1e09e76a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/key_dial/key_dialler.rb
CHANGED
@@ -1,76 +1,462 @@
|
|
1
|
-
module KeyDial
|
2
|
-
|
3
|
-
class KeyDialler
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
@
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
data/lib/key_dial/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
module KeyDial
|
2
|
-
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Convincible
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
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: '
|
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: '
|
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: '
|
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: '
|
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:
|
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.
|
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
data/.rspec
DELETED
data/.travis.yml
DELETED
data/Gemfile
DELETED
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
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
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
|