collapsium 0.3.0 → 0.4.0

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