flexor 0.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.
@@ -0,0 +1,453 @@
1
+ Flexor.new:
2
+ with no arguments:
3
+ creates an empty store
4
+ is a root Flexor
5
+ with an empty hash:
6
+ creates an empty store
7
+ with a flat hash:
8
+ stores all key-value pairs
9
+ allows method access on each key
10
+ allows bracket access on each key
11
+ with a nested hash:
12
+ recursively converts nested hashes into Flexors
13
+ allows method access at every level
14
+ allows bracket access at every level
15
+ with a deeply nested hash (3+ levels):
16
+ allows method access at every level
17
+ allows bracket access at every level
18
+ with a hash containing arrays:
19
+ preserves arrays of scalars
20
+ converts hashes inside arrays into Flexors
21
+ preserves non-hash elements in mixed arrays
22
+ with a hash containing nil values:
23
+ stores the nil value directly
24
+ with a non-hash argument:
25
+ raises ArgumentError
26
+ when comparing root vs non-root (via .new):
27
+ defaults to root: true
28
+ can be set to root: false
29
+
30
+ Flexor.[]:
31
+ with a Hash:
32
+ creates a Flexor with the given data
33
+ with a JSON string:
34
+ parses JSON and creates a Flexor
35
+ with no arguments:
36
+ creates an empty Flexor
37
+ with a non-Hash, non-String argument:
38
+ raises ArgumentError
39
+
40
+ Flexor.from_json:
41
+ with valid flat JSON:
42
+ creates a Flexor with symbolized keys
43
+ allows method access on parsed values
44
+ with nested JSON:
45
+ recursively converts nested objects
46
+ allows method chaining on nested values
47
+ with JSON containing arrays:
48
+ preserves arrays
49
+ converts objects inside arrays into Flexors
50
+ with invalid JSON:
51
+ raises a parse error
52
+
53
+ reading a level 1 property via method:
54
+ when the property exists:
55
+ reads the correct property
56
+ when the property does not exist:
57
+ returns a value where nil? is true
58
+ returns a value that == nil
59
+ does not return the nil singleton
60
+ returns a Flexor
61
+
62
+ reading a level 1 property via hash accessor:
63
+ when the property exists:
64
+ reads the correct property
65
+ when the property does not exist:
66
+ returns a value where nil? is true
67
+ returns a value that == nil
68
+ does not return the nil singleton
69
+ returns a Flexor
70
+
71
+ reading a level 2 property via method:
72
+ when the level 1 property exists:
73
+ when the level 2 property exists:
74
+ reads the correct property
75
+ when the level 2 property does not exist:
76
+ returns a value where nil? is true
77
+ when the level 1 property does not exist:
78
+ returns a value where nil? is true
79
+ the returned value is itself a Flexor that supports further chaining
80
+
81
+ reading a level 2 property via hash accessor:
82
+ when the level 1 property exists:
83
+ when the level 2 property exists:
84
+ reads the correct property
85
+ when the level 2 property does not exist:
86
+ returns a value where nil? is true
87
+ when the level 1 property does not exist:
88
+ returns a value where nil? is true
89
+ the returned value is itself a Flexor that supports further chaining
90
+
91
+ reading at arbitrary depth (3+ levels):
92
+ when all levels are set:
93
+ reads the correct property
94
+ when no levels are set:
95
+ returns a value where nil? is true at every intermediate level
96
+ supports chaining to any depth without error
97
+
98
+ method vs bracket access equivalence:
99
+ writing via method and reading via bracket returns the written value
100
+ writing via bracket and reading via method returns the written value
101
+ when reading the same key via method and bracket:
102
+ for a set property
103
+ for an unset property
104
+
105
+ writing a level 1 property via method:
106
+ writes and reads the written property
107
+
108
+ writing a level 1 property via hash accessor:
109
+ writes and reads the written property
110
+
111
+ writing a level 2 property via method:
112
+ when the level 1 property has been set:
113
+ writes and reads the written property
114
+ when the level 1 property has not been set:
115
+ vivifies the level 1 property
116
+ writes and reads the written property
117
+
118
+ writing a level 2 property via hash accessor:
119
+ when the level 1 property has been set:
120
+ writes and reads the written property
121
+ when the level 1 property has not been set:
122
+ vivifies the level 1 property
123
+ writes and reads the written property
124
+
125
+ writing at arbitrary depth (3+ levels):
126
+ when no intermediate levels are set:
127
+ vivifies every intermediate level
128
+ writes and reads the written property
129
+
130
+ assigning a plain hash via setter:
131
+ when assigning a hash:
132
+ bracket access returns a Flexor, not a Hash
133
+ method chaining works on the assigned value
134
+
135
+ assigning an array via setter:
136
+ with an array of scalars:
137
+ stores and retrieves the array
138
+ with an array of hashes:
139
+ auto-converts inner hashes to Flexors
140
+
141
+ overwriting:
142
+ replacing a nested subtree with a scalar stores the scalar
143
+ replacing a nested subtree with a scalar removes the old subtree
144
+ replacing a scalar with a nested write vivifies the new path
145
+ replacing a scalar with nil makes the property read as nil
146
+
147
+ Flexor#set_raw:
148
+ stores a hash without vivification
149
+ stores an array of hashes without vivification
150
+ stores scalars normally
151
+
152
+ Flexor#merge:
153
+ returns a new Flexor with the merged contents
154
+ does not modify the original
155
+ overwrites scalar values
156
+ deep merges nested hashes
157
+ deep merges multiple levels
158
+ replaces a nested subtree with a scalar when incoming is scalar
159
+ replaces a scalar with a nested hash
160
+ accepts a Flexor as argument
161
+ vivifies hashes in the merged result
162
+
163
+ Flexor#merge!:
164
+ modifies the receiver in place
165
+ returns self
166
+ deep merges nested hashes in place
167
+ accepts a Flexor as argument
168
+
169
+ Flexor#delete:
170
+ removes the key and returns the value
171
+ returns nil for a missing key
172
+ removes nested Flexors
173
+ does not affect other keys
174
+
175
+ Flexor#clear:
176
+ removes all keys
177
+ returns self
178
+ makes the store nil-like
179
+ allows new data after clearing
180
+
181
+ Flexor#to_json:
182
+ serializes scalar values
183
+ serializes nested Flexors
184
+ serializes arrays
185
+ round-trips with from_json
186
+ serializes an empty Flexor as an empty object
187
+ raises when a value is not JSON-serializable (pending)
188
+
189
+ Flexor#nil?:
190
+ is truthy on an unset property (empty Flexor)
191
+ is falsey on a property with a value
192
+ is truthy on a property set explicitly to nil (via NilClass#nil?)
193
+ is truthy on a root Flexor with no data
194
+ is falsey on a root Flexor with data
195
+
196
+ Flexor#==:
197
+ when comparing an unset property (empty Flexor):
198
+ against nil is truthy
199
+ against a non-nil value is falsey
200
+ when comparing two Flexors:
201
+ is truthy when both have identical contents
202
+ is falsey when contents differ
203
+ is truthy when both are empty
204
+ when comparing a Flexor against a Hash:
205
+ is truthy when keys and values are identical
206
+ is falsey when keys and values are NOT identical
207
+ when comparing a property set explicitly to nil:
208
+ against nil is truthy (via NilClass#==)
209
+ against a non-nil value is falsey (via NilClass#==)
210
+ when comparing a scalar value:
211
+ against an identical scalar is truthy (via String#==)
212
+ against a different scalar is falsey (via String#==)
213
+ when checking symmetry:
214
+ documents whether nil == empty Flexor is symmetric
215
+
216
+ Flexor#===:
217
+ when used in a case/when on a Flexor value:
218
+ matches against a scalar when values are equal
219
+ matches against nil when property is unset
220
+
221
+ Flexor.===:
222
+ when used as a class in case/when:
223
+ matches a Flexor instance
224
+ does not match a plain Hash
225
+ does not match nil
226
+
227
+ Flexor#to_s:
228
+ when the property has been set:
229
+ returns the string representation for a scalar value
230
+ returns the string representation for a nested value
231
+ when the property has NOT been set:
232
+ returns an empty string to match nil.to_s behavior
233
+ returns an empty string, not actual nil
234
+ is indistinguishable from nil in string interpolation
235
+ is indistinguishable from nil when passed to puts
236
+ with multiple levels of depth:
237
+ returns empty string at every unset level
238
+ is indistinguishable from nil in puts at any depth
239
+ when comparing root vs non-root:
240
+ root with data returns the store's string representation
241
+ root without data returns empty string
242
+ non-root with data returns the store's string representation
243
+ non-root without data returns empty string
244
+
245
+ Flexor#inspect:
246
+ on a root Flexor with values returns the hash inspection
247
+ on a root Flexor without values returns the empty hash inspection
248
+ on an unset property (empty non-root Flexor) is indistinguishable from inspecting nil
249
+ on a non-root Flexor with values returns the hash inspection
250
+
251
+ Flexor#to_ary:
252
+ returns nil to prevent puts from treating Flexor as an array
253
+ prevents splat expansion from treating Flexor as an array
254
+
255
+ Flexor#to_h:
256
+ returns a plain hash with scalar values
257
+ recursively converts nested Flexors back to hashes
258
+ preserves arrays of scalars
259
+ recursively converts Flexors inside arrays
260
+ converts empty nested Flexors to nil
261
+ with round-trip conversion:
262
+ flat hash survives new -> to_h unchanged
263
+ nested hash survives new -> to_h unchanged
264
+ deeply nested hash survives new -> to_h unchanged
265
+ hash with arrays survives new -> to_h unchanged
266
+ with autovivified but never written paths:
267
+ does not include phantom keys
268
+
269
+ Flexor#deconstruct_keys:
270
+ with a Flexor containing values:
271
+ supports pattern matching via case/in
272
+ extracts matching keys
273
+ returns only requested keys
274
+ with nested Flexors:
275
+ supports nested pattern matching
276
+ with no keys requested:
277
+ returns the entire store
278
+ with an empty Flexor:
279
+ returns an empty hash
280
+
281
+ Flexor#deconstruct:
282
+ returns the values of the store for array-style pattern matching
283
+ supports variable binding in array patterns
284
+ returns an empty array for an empty Flexor
285
+ preserves nested Flexors for recursive pattern matching
286
+
287
+ hash-like query methods:
288
+ "#empty?":
289
+ is truthy when the store has no data
290
+ is falsey when the store has data
291
+ does not autovivify a key named :empty?
292
+ "#keys":
293
+ returns the keys of the store
294
+ does not autovivify a key named :keys
295
+ "#values":
296
+ returns the values of the store
297
+ does not autovivify a key named :values
298
+ "#size / #length":
299
+ returns the number of keys in the store
300
+ does not autovivify a key named :size or :length
301
+ "#key? / #has_key?":
302
+ when the key exists:
303
+ is truthy
304
+ when the key does not exist:
305
+ is falsey
306
+ does not autovivify the queried key
307
+ "#has_key?":
308
+ works identically to key? for existing keys
309
+ works identically to key? for missing keys
310
+ does not autovivify the queried key
311
+ non-standard key types:
312
+ with numeric keys:
313
+ stores and retrieves values with integer keys
314
+ with boolean keys:
315
+ stores and retrieves values with boolean keys
316
+ with empty string keys:
317
+ stores and retrieves values with empty string keys
318
+
319
+ symbol vs string keys:
320
+ method access uses symbol keys (store.foo writes to and reads from :foo)
321
+ bracket access with a symbol reads and writes using the symbol key
322
+ bracket access with a string reads and writes using the string key
323
+ with cross-access:
324
+ store.foo writes a value readable via store[:foo]
325
+ store[:baz] writes a value readable via store.baz
326
+ store.foo and store["foo"] do NOT access the same value
327
+
328
+ method name collisions:
329
+ with methods defined on Object (class, freeze, hash, object_id, send, display):
330
+ are not intercepted by method_missing
331
+ values stored under those keys are accessible via bracket
332
+ with methods defined on Flexor (to_h, to_s, nil?, ==):
333
+ are not intercepted by method_missing
334
+ values stored under those keys are accessible via bracket
335
+
336
+ method_missing edge cases:
337
+ calling a method with arguments (store.foo(1)) raises NoMethodError
338
+ calling a method with a block raises NoMethodError
339
+ setter with too many arguments (store.foo = 1, 2) raises NoMethodError
340
+
341
+ error message clarity:
342
+ with constructor ArgumentError:
343
+ includes the actual class in the error message
344
+ with NoMethodError from cached getter:
345
+ includes the method name after caching
346
+ with NoMethodError from method_missing:
347
+ includes the method name before caching
348
+
349
+ "#respond_to?":
350
+ is truthy for any arbitrary method name
351
+ is truthy for method names that also exist on Object
352
+ is truthy with include_private: true
353
+
354
+ "#respond_to_missing?":
355
+ returns true for any method name
356
+
357
+ array handling end-to-end:
358
+ when constructed with arrays:
359
+ hashes inside arrays are converted to Flexors
360
+ scalars inside arrays are preserved
361
+ nested arrays of hashes are converted recursively
362
+ when assigning directly:
363
+ assigning an array of hashes auto-converts inner hashes
364
+ when reading from arrays stored in Flexor:
365
+ array elements are accessible via standard array methods
366
+
367
+ autovivification side effects:
368
+ reading an unset property creates the key in the store (default_proc behavior)
369
+ the created key holds an empty Flexor
370
+ chaining reads on unset properties creates keys at every intermediate level
371
+ documents whether reads leave traces in to_h
372
+
373
+ thread safety:
374
+ concurrent reads on the same Flexor do not raise
375
+ concurrent writes to different keys documents expected behavior
376
+ concurrent autovivification documents expected behavior
377
+
378
+ dup and clone:
379
+ when duping a Flexor:
380
+ returns a new Flexor with the same contents
381
+ modifications to the dup do not affect the original
382
+ when cloning a Flexor:
383
+ returns a new Flexor with the same contents
384
+ modifications to the clone do not affect the original
385
+ with deep nesting:
386
+ dup is shallow (nested Flexors are shared)
387
+
388
+ freeze:
389
+ when freezing a Flexor:
390
+ prevents further writes
391
+ reads still work
392
+ autovivification raises on frozen store
393
+ merge! on a frozen Flexor raises FrozenError
394
+ delete on a frozen Flexor raises FrozenError
395
+ clear on a frozen Flexor raises FrozenError
396
+ set_raw on a frozen Flexor raises FrozenError
397
+ dup of a frozen Flexor returns an unfrozen copy
398
+ clone of a frozen Flexor returns a frozen copy
399
+
400
+ method caching:
401
+ defines a singleton method after first read
402
+ defines a singleton method after first write
403
+ cached getter returns the correct value
404
+ cached getter reflects updated values
405
+ cached setter writes correctly
406
+ cached getter on missing key returns nil-like Flexor
407
+ cached getter still raises NoMethodError with a block
408
+ cached getter still raises NoMethodError with arguments
409
+ does not cache methods that already exist on Flexor
410
+ caching does not leak between instances
411
+ works correctly with nested chaining
412
+ does not cache getter on first read of unset key (autovivification)
413
+ caches getter on second read of same key
414
+ caches getter immediately for constructor-populated keys
415
+
416
+ Flexor.from_json (edge cases):
417
+ with a JSON array root:
418
+ raises ArgumentError for an Array
419
+ with a JSON null root:
420
+ raises ArgumentError for nil
421
+ with a JSON scalar root:
422
+ raises ArgumentError for a String
423
+
424
+ Flexor#merge! (nil values):
425
+ with nil values:
426
+ nil overwrites an existing scalar
427
+ nil overwrites an existing nested Flexor
428
+ nil inside a nested hash is preserved during deep merge
429
+
430
+ Flexor#merge (nil values):
431
+ with nil values:
432
+ nil overwrites an existing scalar in the new Flexor
433
+ nil inside a nested hash is preserved during deep merge
434
+
435
+ Flexor#set_raw (edge cases):
436
+ with method access on a raw hash:
437
+ method access on a raw Hash value returns the Hash (not a Flexor)
438
+ with interaction with method cache:
439
+ returns the raw Hash on subsequent reads
440
+
441
+ serialization beyond JSON:
442
+ with Marshal:
443
+ round-trips via Marshal.dump and Marshal.load
444
+ preserves autovivification after Marshal round-trip
445
+ preserves nested Flexor structure after round-trip
446
+ preserves the root flag after round-trip
447
+ with YAML:
448
+ round-trips via YAML.dump and YAML.safe_load
449
+ preserves autovivification after YAML round-trip
450
+ preserves nested Flexor structure after round-trip
451
+
452
+ version:
453
+ has a version number
@@ -0,0 +1,30 @@
1
+ class Flexor
2
+ ##
3
+ # Methods that delegate directly to the underlying Hash store.
4
+ module HashDelegation
5
+ def empty?
6
+ @store.empty?
7
+ end
8
+
9
+ def keys
10
+ @store.keys
11
+ end
12
+
13
+ def values
14
+ @store.values
15
+ end
16
+
17
+ def size
18
+ @store.size
19
+ end
20
+
21
+ def length
22
+ @store.length
23
+ end
24
+
25
+ def key?(key)
26
+ @store.key?(key)
27
+ end
28
+ alias has_key? key?
29
+ end
30
+ end
@@ -0,0 +1,32 @@
1
+ class Flexor
2
+ ##
3
+ # Methods for Marshal and YAML round-trip serialization.
4
+ module Serialization
5
+ def marshal_dump
6
+ { store: to_h, root: @root }
7
+ end
8
+
9
+ def marshal_load(data)
10
+ @root = data[:root]
11
+ @store = vivify(data[:store])
12
+ end
13
+
14
+ def encode_with(coder)
15
+ coder["store"] = to_h
16
+ coder["root"] = @root
17
+ end
18
+
19
+ def init_with(coder)
20
+ @root = coder["root"]
21
+ @store = vivify(symbolize_keys(coder["store"] || {}))
22
+ end
23
+
24
+ private
25
+
26
+ def symbolize_keys(hash)
27
+ hash.transform_keys(&:to_sym).transform_values do |v|
28
+ v.is_a?(Hash) ? symbolize_keys(v) : v
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ class Flexor
2
+ VERSION = "0.1.0".freeze
3
+ end
@@ -0,0 +1,47 @@
1
+ class Flexor
2
+ ##
3
+ # Methods for recursively converting raw Hashes and Arrays into Flexor objects.
4
+ module Vivification
5
+ private
6
+
7
+ def vivify(hash)
8
+ new_hash = Hash.new do |h, key|
9
+ h[key] = self.class.new({}, root: false)
10
+ end
11
+
12
+ hash.each do |key, value|
13
+ new_hash[key] = vivify_value(value)
14
+ end
15
+
16
+ new_hash
17
+ end
18
+
19
+ def vivify_value(value)
20
+ case value
21
+ when Hash then self.class.new(value, root: false)
22
+ when Array then vivify_array(value)
23
+ else value
24
+ end
25
+ end
26
+
27
+ def vivify_array(array)
28
+ array.map do |item|
29
+ case item
30
+ when Hash then self.class.new(item, root: false)
31
+ when Array then vivify_array(item)
32
+ else item
33
+ end
34
+ end
35
+ end
36
+
37
+ def recurse_to_h(object)
38
+ case object
39
+ in Array then object.map { recurse_to_h(it) }
40
+ in ^(self.class)
41
+ converted = object.to_h
42
+ converted.empty? ? nil : converted
43
+ else object
44
+ end
45
+ end
46
+ end
47
+ end