collapsium 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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