Class: KeyDial::KeyDialler

Inherits:
Object
  • Object
show all
Defined in:
lib/key_dial/key_dialler.rb

Constant Summary collapse

DEFAULT_OBJECT =
{}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

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

#defaultObject

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.

Parameters:

  • 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.



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.

Parameters:

  • value_obj

    The value to add to the array at the dialled location.



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).

Parameters:

  • key

    The key to dial, determined via [key] syntax



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.

Parameters:

  • key_obj

    The last key to dial, determined via [key] syntax

  • value_obj

    What to set it to.



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.

Parameters:

  • keys_array

    The key(s) to add. Multiple arguments would add multiple keys.



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.

Parameters:

  • type_class (defaults to: (type_class_skipped = true; nil))

    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.



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

#keysObject

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

#objectObject 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.

Parameters:

  • obj_with_keys

    The object that should be dialled, e.g. a Hash, Array or Struct.



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.

Parameters:

  • key_obj

    The key to alter, determined via [key_obj] syntax

  • value_obj

    What to set this key to, determined via [key_obj] = value_obj syntax



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.

Parameters:

  • default

    What to return if no key is found.

Returns:

  • (Boolean)


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.

Parameters:

  • keys_array

    If specified, these keys would be removed from wherever they appear in the dialling list. Otherwise, the last added key is removed.



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