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.
- checksums.yaml +4 -4
- data/Gemfile.lock +8 -8
- data/collapsium.gemspec +2 -2
- data/lib/collapsium/environment_override.rb +65 -52
- data/lib/collapsium/pathed_access.rb +54 -89
- data/lib/collapsium/support/array_methods.rb +48 -0
- data/lib/collapsium/support/methods.rb +69 -5
- data/lib/collapsium/support/path_components.rb +95 -0
- data/lib/collapsium/version.rb +1 -1
- data/lib/collapsium/viral_capabilities.rb +206 -40
- data/spec/environment_override_spec.rb +43 -18
- data/spec/pathed_access_spec.rb +96 -31
- data/spec/support_methods_spec.rb +247 -5
- data/spec/support_path_components.rb +85 -0
- data/spec/viral_capabilities_spec.rb +42 -15
- metadata +10 -6
@@ -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
|
-
|
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
|
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
|
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
|
-
|
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
|
data/lib/collapsium/version.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
104
|
+
enhance_value(wrapped_method.receiver, arg)
|
73
105
|
end
|
74
106
|
result = wrapped_method.call(*arg_copy, &block)
|
75
|
-
next
|
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
|
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(
|
97
|
-
# If the value is not
|
98
|
-
|
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
|
-
|
108
|
-
new_value = outer_hash.class.new
|
201
|
+
enc_class = hash_ancestor(parent, value)
|
109
202
|
|
110
|
-
|
111
|
-
|
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
|
-
|
117
|
-
|
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
|
-
|
122
|
-
|
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
|
-
#
|
129
|
-
|
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
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
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
|
-
|
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
|