Class: KeyDial::KeyDialler
- Inherits:
-
Object
- Object
- KeyDial::KeyDialler
- Defined in:
- lib/key_dial/key_dialler.rb
Constant Summary collapse
- DEFAULT_OBJECT =
{}.freeze
Instance Attribute Summary collapse
-
#default ⇒ Object
Returns the value of attribute default.
Instance Method Summary collapse
-
#+(key) ⇒ Object
Add a key to the dialling chain.
-
#-(key) ⇒ Object
Remove keys that have been dialled.
-
#<<(value_obj) ⇒ Object
The preferred way to add to an array at the end of a set of keys.
-
#[](key) ⇒ Object
The preferred way to build up your dialling list.
-
#[]=(key_obj, value_obj) ⇒ Object
The preferred way to set a value at the end of a set of keys.
- #call(default = (default_skipped = true; @default)) ⇒ Object
-
#dial!(*keys_array) ⇒ Object
Adds a key to the list of nested keys to try, one level deeper.
- #fetch(default = (default_skipped = true; @default)) ⇒ Object
-
#initialize(obj_with_keys = DEFAULT_OBJECT, *lookup) ⇒ KeyDialler
constructor
A new instance of KeyDialler.
-
#insist!(type_class = (type_class_skipped = true; nil)) ⇒ Object
Forces the current list of dialled keys to be instantiated on the object.
-
#keys ⇒ Object
Return the array of keys dialled so far.
-
#keys=(keys_array) ⇒ Object
Set the key list directly.
-
#object ⇒ Object
(also: #hangup)
Return the original keyed object.
-
#object=(obj_with_keys) ⇒ Object
Set/change the keyed object.
-
#set!(value_obj) ⇒ Object
Set any deep key.
-
#set? ⇒ Boolean
Digs into the object to the list of keys specified by dialling.
-
#undial!(*keys_array) ⇒ Object
Remove keys from the dialling list.
Constructor Details
#initialize(obj_with_keys = DEFAULT_OBJECT, *lookup) ⇒ KeyDialler
Returns a new instance of KeyDialler.
11 12 13 14 15 16 17 18 |
# File 'lib/key_dial/key_dialler.rb', line 11 def initialize(obj_with_keys = DEFAULT_OBJECT, *lookup) self.object = obj_with_keys @lookup = [] @default = nil if lookup.length > 0 dial!(*lookup) end end |
Instance Attribute Details
#default ⇒ Object
Returns the value of attribute default.
20 21 22 |
# File 'lib/key_dial/key_dialler.rb', line 20 def default @default end |
Instance Method Details
#+(key) ⇒ Object
Add a key to the dialling chain. If an array is passed, each item in the array will be added in order.
427 428 429 |
# File 'lib/key_dial/key_dialler.rb', line 427 def +(key) return dial!(*key) end |
#-(key) ⇒ Object
Remove keys that have been dialled.
435 436 437 438 439 440 441 |
# File 'lib/key_dial/key_dialler.rb', line 435 def -(key) if key.is_a?(Integer) && key > 0 return key.times { undial! } else return undial!(*key) end end |
#<<(value_obj) ⇒ Object
The preferred way to add to an array at the end of a set of keys. Will create or coerce the array if required.
183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/key_dial/key_dialler.rb', line 183 def <<(value_obj) array = call(Keys::MISSING) # Dial the next array key index - @lookup can never be empty before set!() if array.is_a?(Array) || array.is_a?(Hash) || array.is_a?(Struct) dial!(array.size) elsif array == Keys::MISSING dial!(0) else dial!(1) end return set!(value_obj) end |
#[](key) ⇒ Object
The preferred way to build up your dialling list. Access KeyDialler as if it were a keyed object, e.g. keydialler[b]. This does not actually return any value, rather it dials those keys (awaiting a call).
163 164 165 |
# File 'lib/key_dial/key_dialler.rb', line 163 def [](key) return dial!(key) end |
#[]=(key_obj, value_obj) ⇒ Object
The preferred way to set a value at the end of a set of keys. Will create or coerce intermediate keys if required.
172 173 174 175 176 177 |
# File 'lib/key_dial/key_dialler.rb', line 172 def []=(key_obj, value_obj) # Dial the key to be set - @lookup can never be empty dial!(key_obj) # Set the value return set!(value_obj) end |
#call(default = (default_skipped = true; @default)) ⇒ Object
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/key_dial/key_dialler.rb', line 102 def call(default = (default_skipped = true; @default)) value = nil if set? { |exists| value = exists } # Key exists at key list, and we've captured it to value if block_given? # If block given, yield value to the block return yield(value) else # Otherwise, just return the value return value end else # Key does not exist if default.is_a?(Proc) # If default provided is a Proc, don't just return this as a value - run it return default.call else # Return the default return default end end end |
#dial!(*keys_array) ⇒ Object
Adds a key to the list of nested keys to try, one level deeper.
26 27 28 29 30 |
# File 'lib/key_dial/key_dialler.rb', line 26 def dial!(*keys_array) keys_array = use_keys(keys_array) @lookup += keys_array return self end |
#fetch(default = (default_skipped = true; @default)) ⇒ Object
88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/key_dial/key_dialler.rb', line 88 def fetch(default = (default_skipped = true; @default)) value = nil if set? { |exists| value = exists } return value else if block_given? warn 'warning: block supersedes default value argument' if !default_skipped return yield else return default end end end |
#insist!(type_class = (type_class_skipped = true; nil)) ⇒ Object
Forces the current list of dialled keys to be instantiated on the object.
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 |
# File 'lib/key_dial/key_dialler.rb', line 212 def insist!(type_class = (type_class_skipped = true; nil)) return @obj_with_keys if @lookup.empty? # Hashes can be accessed at [Object] of any kind # Structs can be accessed at [String] and [Symbol], and [Integer] for the nth member (or [Float] which rounds down) # Arrays can be accessed at [Integer] or [Float] which rounds down index = 0 # Will run at least twice, as: # Always runs once for @obj_with_keys itself # Then at least one more time because @lookup is not empty return @lookup.inject(@obj_with_keys) { |deep_obj, this_key| last_index = index >= @lookup.size - 1 # this = object to be accessed # key = key to access on this # access = what kind of key is key key = { this: { type: nil, value: this_key }, next: { type: nil, value: last_index ? Keys::MISSING : @lookup[index + 1] }, last: { type: nil, value: index == 0 ? Keys::MISSING : @lookup[index - 1] } } key.each { |pos, _| if Keys.index?(key[pos][:value]) key[pos][:type] = :index key[pos][:max] = key[pos][:value].magnitude.floor + (key[pos][:value] <= -1 ? 0 : 1) else key[pos][:type] = :object key[pos][:type] = :string if key[pos][:value].is_a?(String) key[pos][:type] = :symbol if key[pos][:value].is_a?(Symbol) end } reconstruct = false # Ensure this object is a supported type - always true for index == 0 i.e. @obj_with_keys itself if !(deep_obj.respond_to?(:fetch) && deep_obj.respond_to?(:[])) # Not a supported type! e.g. a string if key[:this][:type] == :index # If we'll access an array here, re-embed the unsupported object in an array as [0 => original] deep_obj = Array.new(key[:this][:max] - 1).unshift(deep_obj) else # Otherwise, embed the unsupported object in a hash with the key 0 deep_obj = {0 => deep_obj} end # Will never run on @obj_with_keys itself reconstruct = true else # Supported type, but what if this doesn't accept that kind of key? Then... # "You asked for it!"(TM) # 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. if deep_obj.is_a?(Struct) if key[:this][:type] == :string || key[:this][:type] == :symbol if !deep_obj.members.include?(key[:this][:value].to_sym) # You asked for it! # Add the member you requested new_members = deep_obj.members.push(key[:this][:value].to_sym) deep_obj = Struct.new(*new_members).new(*deep_obj.values) reconstruct = true end elsif key[:this][:type] == :index if key[:this][:max] > deep_obj.size # You asked for it! # Create new numeric members up to key requested if key[:this][:value] <= -1 range = 0..((key[:this][:max] - deep_obj.size) - 1) else range = deep_obj.size..(key[:this][:max] - 1) end new_keys = (range).to_a.map { |num| num.to_s.to_sym } # Shove them in if key[:this][:value] <= -1 # Prepend new_members = new_keys.concat(deep_obj.members) new_values = Array.new(new_keys.size - deep_obj.values.size, nil).concat(deep_obj.values) else # Append new_members = deep_obj.members.concat(new_keys) new_values = deep_obj.values end deep_obj = Struct.new(*new_members).new(*new_values) reconstruct = true end end end # "You asked for it!"(TM) # 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. if deep_obj.is_a?(Array) if key[:this][:type] == :index if key[:this][:value] <= -1 && key[:this][:max] > deep_obj.size # You asked for it! # 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. deep_obj = Array.new(key[:this][:max] - deep_obj.size, nil).concat(deep_obj) reconstruct = true end else # You asked for it! # Trying to access non-numeric key on an array, so will convert the array into a hash with integer keys. deep_obj = deep_obj.each_with_index.map { |v, index| [index, v] }.to_h reconstruct = true end end end if reconstruct # Go back and reinject this altered value into the array @lookup[0...(index-1)].inject(@obj_with_keys) { |deep_obj2, this_key2| deep_obj2[this_key2] }[key[:last][:value]] = deep_obj end # Does this object already have this key? if !deep_obj.dial[key[:this][:value]].set? # If not, create empty array/hash dependant on upcoming key if type_class_skipped if key[:next][:type] == :index if key[:next][:value] <= -1 # Ensure new array big enough to address a negative key deep_obj[key[:this][:value]] = Array.new(key[:next][:max]) else # Otherwise, can just create an empty array deep_obj[key[:this][:value]] = [] end else # Create an empty hash awaiting keys/values deep_obj[key[:this][:value]] = {} end else if type_class == Array deep_obj[key[:this][:value]] = [] elsif type_class == Hash deep_obj[key[:this][:value]] = {} elsif type_class == Struct # Why would you do this? deep_obj[key[:this][:value]] = Coercion::Structs::EMPTY.dup elsif type_class.is_a?(Class) && type_class < Struct deep_obj[key[:this][:value]] = type_class.new elsif type_class.respond_to?(:new) begin deep_obj[key[:this][:value]] = type_class.new rescue deep_obj[key[:this][:value]] = nil end else deep_obj[key[:this][:value]] = nil end end elsif !type_class_skipped && last_index && !deep_obj[key[:this][:value]].is_a?(type_class) #Key already exists, but we must ensure it's of the right type if type_class == Array deep_obj[key[:this][:value]] = Array.from(deep_obj[key[:this][:value]]) elsif type_class == Hash deep_obj[key[:this][:value]] = Hash.from(deep_obj[key[:this][:value]]) elsif type_class == Struct # Why would you do this? deep_obj[key[:this][:value]] = Struct.from(deep_obj[key[:this][:value]]) elsif type_class.is_a?(Class) && type_class < Struct deep_obj[key[:this][:value]] = type_class.from(deep_obj[key[:this][:value]]) elsif type_class == String && deep_obj[key[:this][:value]].respond_to?(:to_s) deep_obj[key[:this][:value]] = deep_obj[key[:this][:value]].to_s elsif type_class == Symbol if deep_obj[key[:this][:value]].respond_to?(:to_sym) deep_obj[key[:this][:value]] = deep_obj[key[:this][:value]].to_s elsif deep_obj[key[:this][:value]].respond_to?(:to_s) deep_obj[key[:this][:value]] = deep_obj[key[:this][:value]].to_s.to_sym else warn "Could not coerce value to #{type_class}" end elsif type_class.respond_to?(:new) begin deep_obj[key[:this][:value]] = type_class.new rescue warn "Could not coerce value to #{type_class}" end else warn "Could not coerce value to #{type_class}" end end # Quit if this is the penultimate or last iteration #next deep_obj if last_index # Increment index manually index += 1 # Before here, we must make sure we can access key on deep_obj # Return the value at this key for the next part of inject loop deep_obj[key[:this][:value]] } # Final access (and set) of last key in the @lookup - by this point should be guaranteed to work! #if value_obj_skipped # return obj_to_set[@lookup[-1]] #else # return obj_to_set[@lookup[-1]] = value_obj #end end |
#keys ⇒ Object
Return the array of keys dialled so far.
126 127 128 |
# File 'lib/key_dial/key_dialler.rb', line 126 def keys return @lookup end |
#keys=(keys_array) ⇒ Object
Set the key list directly.
131 132 133 134 135 136 137 138 |
# File 'lib/key_dial/key_dialler.rb', line 131 def keys=(keys_array) if keys_array.is_a?(Array) @lookup = [] dial!(*keys_array) else raise ArgumentError, 'Key list must be set to an array.' end end |
#object ⇒ Object Also known as: hangup
Return the original keyed object.
141 142 143 |
# File 'lib/key_dial/key_dialler.rb', line 141 def object return @obj_with_keys end |
#object=(obj_with_keys) ⇒ Object
Set/change the keyed object.
150 151 152 153 154 155 156 157 |
# File 'lib/key_dial/key_dialler.rb', line 150 def object=(obj_with_keys) obj_with_keys = DEFAULT_OBJECT if obj_with_keys.nil? if obj_with_keys.respond_to?(:fetch) @obj_with_keys = obj_with_keys else raise ArgumentError, 'KeyDialler must be used on a Hash, Array or Struct, or object that responds to the fetch method.' end end |
#set!(value_obj) ⇒ Object
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.
201 202 203 204 205 206 |
# File 'lib/key_dial/key_dialler.rb', line 201 def set!(value_obj) insist!() @lookup[0...-1].inject(@obj_with_keys) { |deep_obj, this_key| deep_obj[this_key] }[@lookup[-1]] = value_obj end |
#set? ⇒ Boolean
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.
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 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/key_dial/key_dialler.rb', line 50 def set? begin value = @lookup.inject(@obj_with_keys) { |deep_obj, this_key| # Has to be an object that can have keys return false unless deep_obj.respond_to?(:[]) if deep_obj.respond_to?(:fetch) # Hash, Array and Struct all respond to fetch # We've monkeypatched fetch to Struct if deep_obj.is_a?(Array) # Check array separately as must fetch numeric key return false unless Keys.index?(this_key) end next_obj = deep_obj.fetch(this_key, Keys::MISSING) else return false end # No need to go any further return false if Keys::MISSING == next_obj # Reinject value to next loop next_obj } rescue # If fetch throws a wobbly at any point, fail gracefully return false end # No errors - yield the value if desired if block_given? yield(value) end # Return true return true end |
#undial!(*keys_array) ⇒ Object
Remove keys from the dialling list.
36 37 38 39 40 41 42 43 44 |
# File 'lib/key_dial/key_dialler.rb', line 36 def undial!(*keys_array) keys_array = use_keys(keys_array) if keys_array.length > 0 @lookup -= keys_array elsif @lookup.length > 0 @lookup.pop end return self end |