collapsium 0.4.1 → 0.5.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.
@@ -16,6 +16,8 @@ module Collapsium
16
16
  ##
17
17
  # Functionality for extending the behaviour of Hash methods
18
18
  module Methods
19
+ WRAPPER_HASH = "@__collapsium_methods_wrappers".freeze
20
+
19
21
  ##
20
22
  # Given the base module, wraps the given method name in the given block.
21
23
  # The block must accept the wrapped_method as the first parameter, followed
@@ -54,8 +56,7 @@ module Collapsium
54
56
  return
55
57
  end
56
58
 
57
- # Hack for calling the private method "define_method"
58
- def_method.call(method_name) do |*args, &method_block|
59
+ wrap_method_block = proc do |*args, &method_block|
59
60
  # We're trying to prevent loops by maintaining a stack of wrapped
60
61
  # method invocations.
61
62
  @__collapsium_methods_callstack ||= []
@@ -63,7 +64,7 @@ module Collapsium
63
64
  # Our current binding is based on the wrapper block and our own class,
64
65
  # as well as the arguments (CRC32).
65
66
  require 'zlib'
66
- signature = Zlib::crc32(JSON::dump(args))
67
+ signature = Zlib.crc32(args.to_s)
67
68
  the_binding = [wrapper_block.object_id, self.class.object_id, signature]
68
69
 
69
70
  # We'll either pass the wrapped method to the wrapper block, or invoke
@@ -90,6 +91,16 @@ module Collapsium
90
91
 
91
92
  next result
92
93
  end
94
+
95
+ # Hack for calling the private method "define_method"
96
+ def_method.call(method_name, &wrap_method_block)
97
+
98
+ # Register this wrapper with the base
99
+ base_wrappers = base.instance_variable_get(WRAPPER_HASH)
100
+ base_wrappers ||= {}
101
+ base_wrappers[method_name] ||= []
102
+ base_wrappers[method_name] << wrapper_block
103
+ base.instance_variable_set(WRAPPER_HASH, base_wrappers)
93
104
  end
94
105
 
95
106
  def resolve_helpers(base, method_name, raise_on_missing)
@@ -107,13 +118,20 @@ module Collapsium
107
118
  if raise_on_missing
108
119
  raise
109
120
  end
110
- return base_method, def_method
121
+ return nil, nil, nil
111
122
  end
112
123
  def_method = base.method(:define_method)
113
124
  else
114
125
  # For Objects and Classes, the unbound method will later be bound to
115
126
  # the object or class to define the method on.
116
- base_method = base.method(method_name.to_s).unbind
127
+ begin
128
+ base_method = base.method(method_name.to_s).unbind
129
+ rescue NameError
130
+ if raise_on_missing
131
+ raise
132
+ end
133
+ return nil, nil, nil
134
+ end
117
135
  # With regards to method defintion, we only want to define methods
118
136
  # for the specific instance (i.e. use :define_singleton_method).
119
137
  def_method = base.method(:define_singleton_method)
@@ -123,6 +141,52 @@ module Collapsium
123
141
  end
124
142
 
125
143
  class << self
144
+ ##
145
+ # Given any base (value, class, module) and a method name, returns the
146
+ # wrappers defined for the base, in order of definition. If no wrappers
147
+ # are defined, an empty Array is returned.
148
+ def wrappers(base, method_name, visited = Set.new)
149
+ # First, check the instance, then its class for a wrapper. If either of
150
+ # them succeeds, exit with a result.
151
+ [base, base.class].each do |item|
152
+ item_wrappers = item.instance_variable_get(WRAPPER_HASH)
153
+ if not item_wrappers.nil? and item_wrappers.include?(method_name)
154
+ return item_wrappers[method_name]
155
+ end
156
+ end
157
+
158
+ # We add the base and its class to the set of visited items.
159
+ visited.add(base)
160
+ visited.add(base.class)
161
+
162
+ # If neither of the above contained a wrapper, look at ancestors
163
+ # recursively.
164
+ ancestors = nil
165
+ begin
166
+ ancestors = base.ancestors
167
+ rescue NoMethodError
168
+ ancestors = base.class.ancestors
169
+ end
170
+ ancestors = ancestors - Object.ancestors - [Object, Class, Module]
171
+
172
+ ancestors.each do |ancestor|
173
+ # Skip an visited item...
174
+ if visited.include?(ancestor)
175
+ next
176
+ end
177
+ visited.add(ancestor)
178
+
179
+ # ... and recurse into unvisited ones
180
+ anc_wrappers = wrappers(ancestor, method_name, visited)
181
+ if not anc_wrappers.empty?
182
+ return anc_wrappers
183
+ end
184
+ end
185
+
186
+ # Return an empty list if we couldn't find anything.
187
+ return []
188
+ end
189
+
126
190
  # Given an input array, return repeated sequences from the array. It's
127
191
  # used in loop detection.
128
192
  def repeated(array)
@@ -0,0 +1,95 @@
1
+ # coding: utf-8
2
+ #
3
+ # collapsium
4
+ # https://github.com/jfinkhaeuser/collapsium
5
+ #
6
+ # Copyright (c) 2016 Jens Finkhaeuser and other collapsium contributors.
7
+ # All rights reserved.
8
+ #
9
+
10
+ module Collapsium
11
+
12
+ ##
13
+ # Support functionality for Collapsium
14
+ module Support
15
+
16
+ ##
17
+ # @api private
18
+ # Defines functions for path prefixes and components. This is mainly used
19
+ # by PathedAccess, but it helps keeping everything separate so that
20
+ # ViralCapabilities can also apply it to Arrays.
21
+ module PathComponents
22
+
23
+ ##
24
+ # Assume any pathed access has this prefix.
25
+ def path_prefix=(value)
26
+ @path_prefix = normalize_path(value)
27
+ end
28
+
29
+ def path_prefix
30
+ @path_prefix ||= separator
31
+ return @path_prefix
32
+ end
33
+
34
+ # @api private
35
+ # Default path separator
36
+ DEFAULT_SEPARATOR = '.'.freeze
37
+
38
+ ##
39
+ # @return [RegExp] the pattern to split paths at; based on `separator`
40
+ def split_pattern
41
+ /(?<!\\)#{Regexp.escape(separator)}/
42
+ end
43
+
44
+ # @return [String] the separator is the character or pattern splitting paths.
45
+ def separator
46
+ @separator ||= DEFAULT_SEPARATOR
47
+ return @separator
48
+ end
49
+ attr_writer :separator
50
+
51
+ ##
52
+ # Break path into components. Expects a String path separated by the
53
+ # `#separator`, and returns the path split into components (an Array of
54
+ # String).
55
+ def path_components(path)
56
+ return filter_components(path.split(split_pattern))
57
+ end
58
+
59
+ ##
60
+ # Given path components, filters out unnecessary ones.
61
+ def filter_components(components)
62
+ return components.select { |c| not c.nil? and not c.empty? }
63
+ end
64
+
65
+ ##
66
+ # Join path components with the `#separator`.
67
+ def join_path(components)
68
+ return components.join(separator)
69
+ end
70
+
71
+ ##
72
+ # Get the parent path of the given path.
73
+ def parent_path(path)
74
+ components = path_components(normalize_path(path))
75
+ return normalize_path(components.slice(0, components.length - 1))
76
+ end
77
+
78
+ ##
79
+ # Normalizes a String path so that there are no empty components, and it
80
+ # starts with a separator.
81
+ def normalize_path(path)
82
+ components = []
83
+ if path.respond_to?(:split) # likely a String
84
+ components = path_components(path)
85
+ elsif path.respond_to?(:join) # likely an Array
86
+ components = filter_components(path)
87
+ end
88
+ return separator + join_path(components)
89
+ end
90
+
91
+ end # module PathComponents
92
+
93
+ end # module Support
94
+
95
+ end # module Collapsium
@@ -8,5 +8,5 @@
8
8
  #
9
9
  module Collapsium
10
10
  # The current release version
11
- VERSION = "0.4.1".freeze
11
+ VERSION = "0.5.0".freeze
12
12
  end
@@ -8,6 +8,7 @@
8
8
  #
9
9
 
10
10
  require 'collapsium/support/hash_methods'
11
+ require 'collapsium/support/array_methods'
11
12
  require 'collapsium/support/methods'
12
13
 
13
14
  module Collapsium
@@ -18,16 +19,37 @@ module Collapsium
18
19
  # Virality is ensured by changing the return value of various methods; if it
19
20
  # is derived from Hash, it is attempted to convert it to the including class.
20
21
  #
21
- # The module uses HashMethods to decide which methods to make viral in this
22
- # manner.
22
+ # The module uses HashMethods and ArrayMethods to decide which methods to make
23
+ # viral in this manner.
23
24
  #
24
25
  # There are two ways for using this module:
25
26
  # a) in a `Class`, either include, prepend or extend it.
26
27
  # b) in a `Module`, *extend* this module. The resulting module can be included,
27
28
  # prepended or extended in a `Class` again.
28
29
  module ViralCapabilities
30
+ # The default ancestor values for the accessors in ViralAncestorTypes
31
+ # are defined here. They're also used for generating functions that should
32
+ # provide the best ancestor class.
33
+ DEFAULT_ANCESTORS = {
34
+ hash_ancestor: Hash,
35
+ array_ancestor: Array,
36
+ }.freeze
37
+
38
+ ##
39
+ # Any Object (Class, Module) that's enhanced with ViralCapabilities will at
40
+ # least be extended with a module defining its Hash and Array ancestors.
41
+ module ViralAncestorTypes
42
+ def hash_ancestor
43
+ return @hash_ancestor || DEFAULT_ANCESTORS[:hash_ancestor]
44
+ end
45
+ attr_writer :hash_ancestor
46
+
47
+ def array_ancestor
48
+ return @array_ancestor || DEFAULT_ANCESTORS[:array_ancestor]
49
+ end
50
+ attr_writer :array_ancestor
51
+ end
29
52
 
30
- include ::Collapsium::Support::HashMethods
31
53
  include ::Collapsium::Support::Methods
32
54
 
33
55
  ##
@@ -45,7 +67,6 @@ module Collapsium
45
67
  end
46
68
 
47
69
  class << self
48
- include ::Collapsium::Support::HashMethods
49
70
  include ::Collapsium::Support::Methods
50
71
 
51
72
  ##
@@ -62,24 +83,40 @@ module Collapsium
62
83
  enhance(base)
63
84
  end
64
85
 
86
+ # We want to wrap methods for Arrays and Hashes alike
87
+ READ_METHODS = (
88
+ ::Collapsium::Support::HashMethods::READ_METHODS \
89
+ + ::Collapsium::Support::ArrayMethods::READ_METHODS
90
+ ).uniq.freeze
91
+ WRITE_METHODS = (
92
+ ::Collapsium::Support::HashMethods::WRITE_METHODS \
93
+ + ::Collapsium::Support::ArrayMethods::WRITE_METHODS
94
+ ).uniq.freeze
95
+
65
96
  ##
66
97
  # Enhance the base by wrapping all READ_METHODS and WRITE_METHODS in
67
- # a wrapper that uses enhance_hash_value to, well, enhance Hash results.
98
+ # a wrapper that uses enhance_value to, well, enhance Hash and Array
99
+ # results.
68
100
  def enhance(base)
69
101
  # rubocop:disable Style/ClassVars
70
102
  @@write_block ||= proc do |wrapped_method, *args, &block|
71
103
  arg_copy = args.map do |arg|
72
- enhance_hash_value(wrapped_method.receiver, arg)
104
+ enhance_value(wrapped_method.receiver, arg)
73
105
  end
74
106
  result = wrapped_method.call(*arg_copy, &block)
75
- next enhance_hash_value(wrapped_method.receiver, result)
107
+ next enhance_value(wrapped_method.receiver, result)
76
108
  end
77
109
  @@read_block ||= proc do |wrapped_method, *args, &block|
78
110
  result = wrapped_method.call(*args, &block)
79
- next enhance_hash_value(wrapped_method.receiver, result)
111
+ next enhance_value(wrapped_method.receiver, result)
80
112
  end
81
113
  # rubocop:enable Style/ClassVars
82
114
 
115
+ # Minimally: add the ancestor functions to classes
116
+ if base.is_a? Class
117
+ base.extend(ViralAncestorTypes)
118
+ end
119
+
83
120
  READ_METHODS.each do |method_name|
84
121
  wrap_method(base, method_name, raise_on_missing: false, &@@read_block)
85
122
  end
@@ -89,63 +126,192 @@ module Collapsium
89
126
  end
90
127
  end
91
128
 
129
+ ##
130
+ # Enhance Hash or Array value
131
+ def enhance_value(parent, value, *args)
132
+ if value.is_a? Hash
133
+ value = enhance_hash_value(parent, value, *args)
134
+ elsif value.is_a? Array
135
+ value = enhance_array_value(parent, value, *args)
136
+ end
137
+
138
+ # It's possible that the value is a Hash or an Array, but there's no
139
+ # ancestor from which capabilities can be copied. We can find out by
140
+ # checking whether any wrappers are defined for it.
141
+ needs_wrapping = true
142
+ READ_METHODS.each do |method_name|
143
+ wrappers = ::Collapsium::Support::Methods.wrappers(value, method_name)
144
+ # rubocop:disable Style/Next
145
+ if wrappers.include?(@@read_block)
146
+ # all done
147
+ needs_wrapping = false
148
+ break
149
+ end
150
+ # rubocop:enable Style/Next
151
+ end
152
+
153
+ # If we have a Hash or Array value that needs enhancing still, let's
154
+ # do that.
155
+ if needs_wrapping and (value.is_a? Array or value.is_a? Hash)
156
+ enhance(value)
157
+ end
158
+
159
+ return value
160
+ end
161
+
162
+ def enhance_array_value(parent, value, *args)
163
+ # If the value is not of the best ancestor type, make sure it becomes
164
+ # that type.
165
+ # XXX: DO NOT replace the loop with a simpler function - it could lead
166
+ # to infinite recursion!
167
+ enc_class = array_ancestor(parent, value)
168
+ if value.class != enc_class
169
+ new_value = enc_class.new
170
+ value.each do |item|
171
+ if not item.is_a? Hash and not item.is_a? Array
172
+ new_value << item
173
+ next
174
+ end
175
+
176
+ new_item = enhance_value(value, item)
177
+ new_value << new_item
178
+ end
179
+ value = new_value
180
+ end
181
+
182
+ # Copy all modules from the parent to the value
183
+ copy_mods(parent, value)
184
+
185
+ # Set appropriate ancestors on the value
186
+ set_ancestors(parent, value)
187
+
188
+ return call_virality(parent, value, *args)
189
+ end
190
+
92
191
  ##
93
192
  # Given an outer Hash and a value, enhance Hash values so that they have
94
193
  # the same capabilities as the outer Hash. Non-Hash values are returned
95
194
  # unchanged.
96
- def enhance_hash_value(outer_hash, value)
97
- # If the value is not a Hash, we don't do anything.
98
- if not value.is_a? Hash
99
- return value
100
- end
101
-
102
- # If the value is a different type of Hash from ourself, we want to
103
- # create an instance of our own type with the same values.
195
+ def enhance_hash_value(parent, value, *args)
196
+ # If the value is not of the best ancestor type, make sure it becomes
197
+ # that type.
104
198
  # XXX: DO NOT replace the loop with :merge! or :merge - those are
105
199
  # potentially wrapped write functions, leading to an infinite
106
200
  # recursion.
107
- if value.class != outer_hash.class
108
- new_value = outer_hash.class.new
201
+ enc_class = hash_ancestor(parent, value)
109
202
 
110
- value.each do |key, inner_val|
111
- if not inner_val.is_a? Hash
112
- new_value[key] = inner_val
113
- next
114
- end
203
+ if value.class != enc_class
204
+ new_value = enc_class.new
115
205
 
116
- if inner_val.class == outer_hash.class
117
- new_value[key] = inner_val
206
+ value.each do |key, item|
207
+ if not item.is_a? Hash and not item.is_a? Array
208
+ new_value[key] = item
118
209
  next
119
210
  end
120
211
 
121
- new_inner_value = outer_hash.class.new
122
- new_inner_value.merge!(inner_val)
123
- new_value[key] = new_inner_value
212
+ new_item = enhance_value(value, item)
213
+ new_value[key] = new_item
124
214
  end
125
215
  value = new_value
126
216
  end
127
217
 
128
- # Next, we want to extend all the modules in self. That might be a
129
- # no-op due to the above block, but not necessarily so.
130
- value_mods = (class << value; self end).included_modules
131
- own_mods = (class << outer_hash; self end).included_modules
132
- (own_mods - value_mods).each do |mod|
133
- value.extend(mod)
134
- end
218
+ # Copy all modules from the parent to the value
219
+ copy_mods(parent, value)
135
220
 
136
221
  # If we have a default_proc and the value doesn't, we want to use our
137
222
  # own. This *can* override a perfectly fine default_proc with our own,
138
223
  # which might suck.
139
- value.default_proc ||= outer_hash.default_proc
224
+ if parent.respond_to?(:default_proc)
225
+ # FIXME: need to inherit this for arrays, too?
226
+ value.default_proc ||= parent.default_proc
227
+ end
228
+
229
+ # Set appropriate ancestors on the value
230
+ set_ancestors(parent, value)
231
+
232
+ return call_virality(parent, value, *args)
233
+ end
234
+
235
+ def copy_mods(parent, value)
236
+ # We want to extend all the modules in self. That might be a
237
+ # no-op due to the above block, but not necessarily so.
238
+ value_mods = (class << value; self end).included_modules
239
+ parent_mods = (class << parent; self end).included_modules
240
+ parent_mods << ViralAncestorTypes
241
+ mods_to_copy = (parent_mods - value_mods).uniq
242
+
243
+ # Small fixup for JSON; this doesn't technically belong here, but let's
244
+ # play nice.
245
+ if value.is_a? Array
246
+ mods_to_copy.delete(::JSON::Ext::Generator::GeneratorMethods::Hash)
247
+ elsif value.is_a? Hash
248
+ mods_to_copy.delete(::JSON::Ext::Generator::GeneratorMethods::Array)
249
+ end
250
+
251
+ # Copy mods.
252
+ mods_to_copy.each do |mod|
253
+ value.extend(mod)
254
+ end
255
+ end
140
256
 
141
- # Finally, the class can define its own virality function.
142
- if outer_hash.respond_to?(:virality)
143
- value = outer_hash.virality(value)
257
+ def call_virality(parent, value, *args)
258
+ # The parent class can define its own virality function.
259
+ if parent.respond_to?(:virality)
260
+ value = parent.virality(value, *args)
144
261
  end
145
262
 
146
263
  return value
147
264
  end
148
- end
265
+
266
+ DEFAULT_ANCESTORS.each do |getter, default|
267
+ define_method(getter) do |parent, value|
268
+ # We value (haha) the value's stored ancestor over the parent's...
269
+ [value, parent].each do |receiver|
270
+ # rubocop:disable Lint/HandleExceptions
271
+ begin
272
+ klass = receiver.send(getter)
273
+ if klass != default
274
+ return klass
275
+ end
276
+ rescue NoMethodError
277
+ end
278
+ # rubocop:enable Lint/HandleExceptions
279
+ end
280
+
281
+ # ... but if neither yield satisfying results, we value the parent's
282
+ # class over the value's. This way parents can propagate their class.
283
+ [parent, value].each do |receiver|
284
+ if receiver.is_a? default
285
+ return receiver.class
286
+ end
287
+ end
288
+
289
+ # The default shouldn't really be reached, because it only applies
290
+ # if neither parent nor value are derived from default, and then
291
+ # this functions shouldn't even be called.
292
+ # :nocov:
293
+ return default
294
+ # :nocov:
295
+ end
296
+ end
297
+
298
+ def set_ancestors(parent, value)
299
+ DEFAULT_ANCESTORS.each do |getter, default|
300
+ setter = "#{getter}=".to_sym
301
+
302
+ ancestor = nil
303
+ if parent.is_a? default
304
+ ancestor = parent.class
305
+ elsif parent.respond_to?(getter)
306
+ ancestor = parent.send(getter)
307
+ else
308
+ ancestor = default
309
+ end
310
+
311
+ value.send(setter, ancestor)
312
+ end
313
+ end
314
+ end # class << self
149
315
 
150
316
  end # module ViralCapabilities
151
317
  end # module Collapsium