collapsium 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 15f88a96badb4dff1a23e1eac842de71b86e4b27
4
- data.tar.gz: 1ad64e9ec74b0d125292d1424280d149389a4609
3
+ metadata.gz: 0565523a24719d83a3a81ec99005231cb86155df
4
+ data.tar.gz: eb817e05d8dec93788ecc51337579315a6719c70
5
5
  SHA512:
6
- metadata.gz: a97d7feaf6c43dcd442c5342970563c7665d5d45c812284ebd24620db7a25ea5d6adf97fe73a8e16331b8587d214a4627be1c88f86021221d3fdfaa210192668
7
- data.tar.gz: 3c8a5e286cd04e4d79957d94e3e22aef249c0be5352848f2f22b2061449637ad90f41ec1cff3d517b81d594fc83f1700332d0d7dd14801b08bf5878de87d3884
6
+ metadata.gz: c3e20a3f37be0496fd1ac6c6f653e845aaf57d7dd8b149347bddef882ac2ddf7b414b656370e65c49070fb8a638c4beb9ba139fc18bd485765f7fe695ea738da
7
+ data.tar.gz: 45ed48ecbc31111ac87689f7ffc84cdb5769d6a19bde8368e6cf5e0f60902af1c1753894d011f059b286e989db398f50e65984b8cd336e132a9a09b4b0197ca3
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- collapsium (0.3.0)
4
+ collapsium (0.4.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -11,7 +11,7 @@ GEM
11
11
  simplecov (>= 0.7.1, < 1.0.0)
12
12
  diff-lcs (1.2.5)
13
13
  docile (1.1.5)
14
- json (2.0.1)
14
+ json (2.0.2)
15
15
  parser (2.3.1.2)
16
16
  ast (~> 2.2)
17
17
  powerpack (0.1.1)
@@ -21,7 +21,7 @@ GEM
21
21
  rspec-core (~> 3.5.0)
22
22
  rspec-expectations (~> 3.5.0)
23
23
  rspec-mocks (~> 3.5.0)
24
- rspec-core (3.5.1)
24
+ rspec-core (3.5.2)
25
25
  rspec-support (~> 3.5.0)
26
26
  rspec-expectations (3.5.0)
27
27
  diff-lcs (>= 1.2.0, < 2.0)
@@ -30,7 +30,7 @@ GEM
30
30
  diff-lcs (>= 1.2.0, < 2.0)
31
31
  rspec-support (~> 3.5.0)
32
32
  rspec-support (3.5.0)
33
- rubocop (0.41.2)
33
+ rubocop (0.42.0)
34
34
  parser (>= 2.3.1.1, < 3.0)
35
35
  powerpack (~> 0.1)
36
36
  rainbow (>= 1.99.1, < 3.0)
@@ -43,7 +43,7 @@ GEM
43
43
  simplecov-html (~> 0.10.0)
44
44
  simplecov-html (0.10.0)
45
45
  unicode-display_width (1.1.0)
46
- yard (0.9.0)
46
+ yard (0.9.5)
47
47
 
48
48
  PLATFORMS
49
49
  ruby
@@ -54,7 +54,7 @@ DEPENDENCIES
54
54
  collapsium!
55
55
  rake (~> 11.2)
56
56
  rspec (~> 3.5)
57
- rubocop (~> 0.41)
57
+ rubocop (~> 0.42)
58
58
  simplecov (~> 0.12)
59
59
  yard (~> 0.9)
60
60
 
@@ -35,7 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.required_ruby_version = '>= 2.0'
36
36
 
37
37
  spec.add_development_dependency "bundler", "~> 1.12"
38
- spec.add_development_dependency "rubocop", "~> 0.41"
38
+ spec.add_development_dependency "rubocop", "~> 0.42"
39
39
  spec.add_development_dependency "rake", "~> 11.2"
40
40
  spec.add_development_dependency "rspec", "~> 3.5"
41
41
  spec.add_development_dependency "simplecov", "~> 0.12"
@@ -9,8 +9,13 @@
9
9
 
10
10
  require 'collapsium/version'
11
11
 
12
- require 'collapsium/recursive_merge'
12
+ require 'collapsium/environment_override'
13
13
  require 'collapsium/indifferent_access'
14
14
  require 'collapsium/pathed_access'
15
+ require 'collapsium/prototype_match'
16
+ require 'collapsium/recursive_dup'
17
+ require 'collapsium/recursive_merge'
18
+ require 'collapsium/recursive_sort'
19
+ require 'collapsium/viral_capabilities'
15
20
 
16
21
  require 'collapsium/uber_hash'
@@ -0,0 +1,167 @@
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
+ require 'collapsium/support/hash_methods'
11
+ require 'collapsium/support/methods'
12
+
13
+ require 'collapsium/viral_capabilities'
14
+
15
+ module Collapsium
16
+
17
+ ##
18
+ # The EnvironmentOverride module wraps read access methods to return
19
+ # the contents of environment variables derived from the key instead of
20
+ # the value contained in the Hash.
21
+ #
22
+ # The environment variable to use is derived from the key by replacing
23
+ # all consecutive occurrences of non-alphanumeric characters with an
24
+ # underscore (_), and converting the alphabetic characters to upper case.
25
+ # Leading and trailing underscores will be stripped.
26
+ #
27
+ # For example, the key "some!@email.org" will become "SOME_EMAIL_ORG".
28
+ #
29
+ # If PathedAccess is also used, the :path_prefix of nested hashes will
30
+ # be consulted after converting it in the same manner.
31
+ module EnvironmentOverride
32
+ ##
33
+ # If the module is included, extended or prepended in a class, it'll
34
+ # wrap accessor methods.
35
+ class << self
36
+ include ::Collapsium::Support::HashMethods
37
+ include ::Collapsium::Support::Methods
38
+
39
+ ##
40
+ # Returns a proc read access.
41
+ ENV_ACCESS_READER = proc do |super_method, *args, &block|
42
+ # If there are no arguments, there's nothing to do with paths. Just
43
+ # delegate to the hash.
44
+ if args.empty?
45
+ next super_method.call(*args, &block)
46
+ end
47
+
48
+ # The method's receiver is encapsulated in the super_method; we'll
49
+ # use it a few times so let's reduce typing. This is essentially the
50
+ # equivalent of `self`.
51
+ receiver = super_method.receiver
52
+
53
+ # All KEYED_READ_METHODS have a key as the first argument.
54
+ key = args[0]
55
+
56
+ # Grab matching environment variable names. We consider first a pathed
57
+ # name, if pathed access is supported, followed by the unpathed name.
58
+ env_keys = [key.to_s]
59
+ if receiver.respond_to?(:path_prefix) and not
60
+ (receiver.path_prefix.nil? or receiver.path_prefix.empty?)
61
+ prefix_components = receiver.path_components(receiver.path_prefix)
62
+
63
+ if prefix_components.last == key
64
+ env_keys.unshift(receiver.path_prefix)
65
+ else
66
+ env_keys.unshift(receiver.path_prefix + receiver.separator + key.to_s)
67
+ end
68
+ end
69
+ env_keys.map! { |k| receiver.key_to_env(k) }
70
+ env_keys.select! { |k| not k.empty? }
71
+ env_keys.uniq!
72
+
73
+ # When we have the keys (in priority order), try to see if the
74
+ # environment yields something useful.
75
+ value = nil
76
+ env_keys.each do |env_key|
77
+ # Grab the environment value; skip if there's nothing there.
78
+ env_value = ENV[env_key]
79
+ if env_value.nil?
80
+ next
81
+ end
82
+
83
+ # If the environment variable parses as JSON, that's great, we'll use
84
+ # the parsed result. Otherwise use it as a string.
85
+ require 'json'
86
+ # rubocop:disable Lint/HandleExceptions
87
+ begin
88
+ env_value = JSON.parse(env_value)
89
+ rescue JSON::ParserError
90
+ # Do nothing. We just use the env_value verbatim.
91
+ end
92
+ # rubocop:enable Lint/HandleExceptions
93
+
94
+ # For the given key, retrieve the current value. We'll need it later.
95
+ # Note that we deliberately do not use any of KEYED_READ_METHODS,
96
+ # because that would recurse infinitely.
97
+ old_value = nil
98
+ receiver.each do |k, v|
99
+ if k == key
100
+ old_value = v
101
+ break
102
+ end
103
+ end
104
+
105
+ # Note that we re-assign the value only if it's changed, but we
106
+ # break either way. If env_value exists, it must be used, but
107
+ # changing it always will lead to an infinite recursion.
108
+ # The double's super_method will never be called, but rather
109
+ # always this wrapper.
110
+ if env_value != old_value
111
+ value = env_value
112
+ end
113
+ break
114
+ end
115
+ if not value.nil?
116
+ # We can't just return the value, because that doesn't respect the
117
+ # method being called. We can deal with this by duplicating the
118
+ # receiver, writing the value we found into the appropriate key,
119
+ # and then sending the super_method to the duplicate.
120
+ double = receiver.dup
121
+ double[key] = value
122
+ meth = double.method(super_method.name)
123
+ next meth.call(*args, &block)
124
+ end
125
+
126
+ # Otherwise, fall back on the super method.
127
+ next super_method.call(*args, &block)
128
+ end.freeze # proc
129
+
130
+ def included(base)
131
+ enhance(base)
132
+ end
133
+
134
+ def extended(base)
135
+ enhance(base)
136
+ end
137
+
138
+ def prepended(base)
139
+ enhance(base)
140
+ end
141
+
142
+ def enhance(base)
143
+ # Make the capabilities of classes using PathedAccess viral.
144
+ base.extend(ViralCapabilities)
145
+
146
+ # Wrap read accessor functions to deal with paths
147
+ KEYED_READ_METHODS.each do |method|
148
+ wrap_method(base, method, &ENV_ACCESS_READER)
149
+ end
150
+ end
151
+ end # class << self
152
+
153
+ def key_to_env(key)
154
+ # First, convert to upper case
155
+ env_key = key.upcase
156
+
157
+ # Next, replace non-alphanumeric characters to underscore. This also
158
+ # collapses them into a single undescore.
159
+ env_key.gsub!(/[^[:alnum:]]+/, '_')
160
+
161
+ # Strip leading and trailing underscores.
162
+ env_key.gsub!(/^_*(.*?)_*$/, '\1')
163
+
164
+ return env_key
165
+ end
166
+ end # module EnvironmentOverride
167
+ end # module Collapsium
@@ -7,6 +7,11 @@
7
7
  # All rights reserved.
8
8
  #
9
9
 
10
+ require 'collapsium/support/hash_methods'
11
+ require 'collapsium/support/methods'
12
+
13
+ require 'collapsium/viral_capabilities'
14
+
10
15
  module Collapsium
11
16
 
12
17
  ##
@@ -28,17 +33,16 @@ module Collapsium
28
33
  return @separator
29
34
  end
30
35
 
31
- # @api private
32
- # Methods redefined to support pathed read access.
33
- READ_METHODS = [
34
- :[], :default, :delete, :fetch, :has_key?, :include?, :key?, :member?,
35
- ].freeze
36
+ ##
37
+ # Assume any pathed access has this prefix.
38
+ def path_prefix=(value)
39
+ @path_prefix = normalize_path(value)
40
+ end
36
41
 
37
- # @api private
38
- # Methods redefined to support pathed write access.
39
- WRITE_METHODS = [
40
- :[]=, :store,
41
- ].freeze
42
+ def path_prefix
43
+ @path_prefix ||= ''
44
+ return @path_prefix
45
+ end
42
46
 
43
47
  # @api private
44
48
  # Default path separator
@@ -50,95 +54,183 @@ module Collapsium
50
54
  /(?<!\\)#{Regexp.escape(separator)}/
51
55
  end
52
56
 
53
- (READ_METHODS + WRITE_METHODS).each do |method|
54
- # Wrap all accessor functions to deal with paths
55
- define_method(method) do |*args, &block|
56
- # If there are no arguments, there's nothing to do with paths. Just
57
- # delegate to the hash.
58
- if args.empty?
59
- return super(*args, &block)
60
- end
57
+ ##
58
+ # If the module is included, extended or prepended in a class, it'll
59
+ # wrap accessor methods.
60
+ class << self
61
+ include ::Collapsium::Support::HashMethods
62
+ include ::Collapsium::Support::Methods
63
+
64
+ ##
65
+ # Returns a proc for either read or write access. Procs for write
66
+ # access will create intermediary hashes when e.g. setting a value for
67
+ # `foo.bar.baz`, and the `bar` Hash doesn't exist yet.
68
+ def create_proc(write_access)
69
+ return proc do |super_method, *args, &block|
70
+ # If there are no arguments, there's nothing to do with paths. Just
71
+ # delegate to the hash.
72
+ if args.empty?
73
+ next super_method.call(*args, &block)
74
+ end
75
+
76
+ # The method's receiver is encapsulated in the super_method; we'll
77
+ # use it a few times so let's reduce typing. This is essentially the
78
+ # equivalent of `self`.
79
+ receiver = super_method.receiver
61
80
 
62
- # With any of the dispatch methods, we know that the first argument has
63
- # to be a key. We'll try to split it by the path separator.
64
- components = args[0].to_s.split(split_pattern)
65
- loop do
66
- if components.empty? or not components[0].empty?
67
- break
81
+ # With any of the dispatch methods, we know that the first argument has
82
+ # to be a key. We'll try to split it by the path separator.
83
+ components = receiver.path_components(args[0].to_s)
84
+
85
+ # If there are no components, return the receiver itself/the root
86
+ if components.empty?
87
+ next receiver
88
+ end
89
+
90
+ # Try to find the leaf, based on the given components.
91
+ leaf = recursive_fetch(components, receiver, [], create: write_access)
92
+
93
+ # The tricky part is what to do with the leaf.
94
+ meth = nil
95
+ if receiver.object_id == leaf.object_id
96
+ # a) if the leaf and the receiver are identical, then the receiver
97
+ # itself was requested, and we really just need to delegate to its
98
+ # super_method.
99
+ meth = super_method
100
+ else
101
+ # b) if the leaf is different from the receiver, we want to delegate
102
+ # to the leaf.
103
+ meth = leaf.method(super_method.name)
68
104
  end
69
- components.shift
105
+
106
+ # If the first argument was a symbol key, we want to use it verbatim.
107
+ # Otherwise we had pathed access, and only want to pass the last
108
+ # component to whatever method we're calling.
109
+ the_args = args
110
+ if not args[0].is_a?(Symbol)
111
+ the_args = args.dup
112
+ the_args[0] = components.last
113
+ end
114
+
115
+ # Then we can continue with that method.
116
+ next meth.call(*the_args, &block)
117
+ end # proc
118
+ end # create_proc
119
+
120
+ # Create a reader and write proc, because we only know
121
+ PATHED_ACCESS_READER = PathedAccess.create_proc(false).freeze
122
+ PATHED_ACCESS_WRITER = PathedAccess.create_proc(true).freeze
123
+
124
+ def included(base)
125
+ enhance(base)
126
+ end
127
+
128
+ def extended(base)
129
+ enhance(base)
130
+ end
131
+
132
+ def prepended(base)
133
+ enhance(base)
134
+ end
135
+
136
+ def enhance(base)
137
+ # Make the capabilities of classes using PathedAccess viral.
138
+ base.extend(ViralCapabilities)
139
+
140
+ # Wrap all accessor functions to deal with paths
141
+ KEYED_READ_METHODS.each do |method|
142
+ wrap_method(base, method, &PATHED_ACCESS_READER)
143
+ end
144
+ KEYED_WRITE_METHODS.each do |method|
145
+ wrap_method(base, method, &PATHED_ACCESS_WRITER)
70
146
  end
147
+ end
71
148
 
72
- # If there are no components, return self/the root
73
- if components.empty?
74
- return self
149
+ ##
150
+ # Given the path components, recursively fetch any but the last key.
151
+ def recursive_fetch(path, data, current_path = [], options = {})
152
+ # Split path into head and tail; for the next iteration, we'll look use
153
+ # only head, and pass tail on recursively.
154
+ head = path[0]
155
+ current_path << head
156
+ tail = path.slice(1, path.length)
157
+
158
+ # We know that the data has the current path. We also know that thanks to
159
+ # virality, data will respond to :path_prefix. So we might as well set the
160
+ # path, as long as it is more specific than what was previously there.
161
+ current_normalized = data.normalize_path(current_path)
162
+ if current_normalized.length > data.path_prefix.length
163
+ data.path_prefix = current_normalized
75
164
  end
76
165
 
77
- # This is already the leaf-most Hash
78
- if components.length == 1
79
- # Weird edge case: if we didn't have to shift anything, then it's
80
- # possible we inadvertently changed a symbol key into a string key,
81
- # which could mean looking fails.
82
- # We can detect that by comparing copy[0] to a symbolized version of
83
- # components[0].
84
- copy = args.dup
85
- if copy[0] != components[0].to_sym
86
- copy[0] = components[0]
87
- end
88
- return super(*copy, &block)
166
+ # For the leaf element, we do nothing because that's where we want to
167
+ # dispatch to.
168
+ if path.length == 1
169
+ return data
89
170
  end
90
171
 
91
- # Deal with other paths. The frustrating part here is that for nested
92
- # hashes, only this outermost one is guaranteed to know anything about
93
- # path splitting, so we'll have to recurse down to the leaf here.
94
- #
95
- # For write methods, we need to create intermediary hashes.
96
- leaf = recursive_fetch(components, self,
97
- create: WRITE_METHODS.include?(method))
98
- if leaf.is_a? Hash
99
- leaf.default_proc = default_proc
172
+ # If we're a write function, then we need to create intermediary objects,
173
+ # i.e. what's at head if nothing is there.
174
+ if data[head].nil?
175
+ # If the head is nil, we can't recurse. In create mode that means we
176
+ # want to create hash children, but in read mode we're done recursing.
177
+ # By returning a hash here, we allow the caller to send methods on to
178
+ # this temporary, making a PathedAccess Hash act like any other Hash.
179
+ if not options[:create]
180
+ return {}
181
+ end
182
+
183
+ data[head] = {}
100
184
  end
101
185
 
102
- # If we have a leaf, we want to send the requested method to that
103
- # leaf.
104
- copy = args.dup
105
- copy[0] = components.last
106
- return leaf.send(method, *copy, &block)
186
+ # Ok, recurse.
187
+ return recursive_fetch(tail, data[head], current_path, options)
107
188
  end
108
- end
189
+ end # class << self
109
190
 
110
- private
191
+ ##
192
+ # Break path into components. Expects a String path separated by the
193
+ # `#separator`, and returns the path split into components (an Array of
194
+ # String).
195
+ def path_components(path)
196
+ return filter_components(path.split(split_pattern))
197
+ end
111
198
 
112
199
  ##
113
- # Given the path components, recursively fetch any but the last key.
114
- def recursive_fetch(path, data, options = {})
115
- # For the leaf element, we do nothing because that's where we want to
116
- # dispatch to.
117
- if path.length == 1
118
- return data
119
- end
200
+ # Given path components, filters out unnecessary ones.
201
+ def filter_components(components)
202
+ return components.select { |c| not c.nil? and not c.empty? }
203
+ end
120
204
 
121
- # Split path into head and tail; for the next iteration, we'll look use only
122
- # head, and pass tail on recursively.
123
- head = path[0]
124
- tail = path.slice(1, path.length)
125
-
126
- # If we're a write function, then we need to create intermediary objects,
127
- # i.e. what's at head if nothing is there.
128
- if data[head].nil?
129
- # If the head is nil, we can't recurse. In create mode that means we
130
- # want to create hash children, but in read mode we're done recursing.
131
- # By returning a hash here, we allow the caller to send methods on to
132
- # this temporary, making a PathedAccess Hash act like any other Hash.
133
- if not options[:create]
134
- return {}
135
- end
205
+ ##
206
+ # Join path components with the `#separator`.
207
+ def join_path(components)
208
+ return components.join(separator)
209
+ end
136
210
 
137
- data[head] = {}
211
+ ##
212
+ # Normalizes a String path so that there are no empty components, and it
213
+ # starts with a separator.
214
+ def normalize_path(path)
215
+ components = []
216
+ if path.respond_to?(:split) # likely a String
217
+ components = path_components(path)
218
+ elsif path.respond_to?(:join) # likely an Array
219
+ components = filter_components(path)
138
220
  end
221
+ return separator + join_path(components)
222
+ end
139
223
 
140
- # Ok, recurse.
141
- return recursive_fetch(tail, data[head], options)
224
+ ##
225
+ # Ensure that all values have their path_prefix set.
226
+ def virality(value)
227
+ # If a value was set via a nested Hash, it may not have got its
228
+ # path_prefix set during storing (i.e. x[key] = { nested: some_hash }
229
+ # In that case, we do always know that the value's path prefix is the same
230
+ # as the receiver.
231
+ value.path_prefix = path_prefix
232
+ return value
142
233
  end
234
+
143
235
  end # module PathedAccess
144
236
  end # module Collapsium