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