collapsium 0.4.0 → 0.4.1

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: 0565523a24719d83a3a81ec99005231cb86155df
4
- data.tar.gz: eb817e05d8dec93788ecc51337579315a6719c70
3
+ metadata.gz: 1b85cef5b1becbc08cec24005e6e7c31b4e8156a
4
+ data.tar.gz: 1a25c135dcb2d7784dedcb04cdee7e6633e841eb
5
5
  SHA512:
6
- metadata.gz: c3e20a3f37be0496fd1ac6c6f653e845aaf57d7dd8b149347bddef882ac2ddf7b414b656370e65c49070fb8a638c4beb9ba139fc18bd485765f7fe695ea738da
7
- data.tar.gz: 45ed48ecbc31111ac87689f7ffc84cdb5769d6a19bde8368e6cf5e0f60902af1c1753894d011f059b286e989db398f50e65984b8cd336e132a9a09b4b0197ca3
6
+ metadata.gz: 2d54cd48ceef5cd706e662b40451f349595e88ecc7987ee54544110adff1a8493a11345e83d2ef659f825741029630d8d911b4be8325111ea28d7558fb2a7ad3
7
+ data.tar.gz: 24dc1841b56c2b5304d13a8977e1123aabfe196ec87f912bc4251c575ffdb9d7cfd17703a9b10d933b03a335111350ff4aa6d70099c93eeebc893d7cbb4631f2
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- collapsium (0.4.0)
4
+ collapsium (0.4.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -10,6 +10,7 @@
10
10
  require 'collapsium/support/hash_methods'
11
11
  require 'collapsium/support/methods'
12
12
 
13
+ require 'collapsium/recursive_dup'
13
14
  require 'collapsium/viral_capabilities'
14
15
 
15
16
  module Collapsium
@@ -27,8 +28,12 @@ module Collapsium
27
28
  # For example, the key "some!@email.org" will become "SOME_EMAIL_ORG".
28
29
  #
29
30
  # If PathedAccess is also used, the :path_prefix of nested hashes will
30
- # be consulted after converting it in the same manner.
31
+ # be consulted after converting it in the same manner. NOTE:
32
+ # include EnvironmentOverride *after* PathedAccess for both to work well
33
+ # together.
31
34
  module EnvironmentOverride
35
+ include ::Collapsium::RecursiveDup
36
+
32
37
  ##
33
38
  # If the module is included, extended or prepended in a class, it'll
34
39
  # wrap accessor methods.
@@ -38,33 +43,54 @@ module Collapsium
38
43
 
39
44
  ##
40
45
  # Returns a proc read access.
41
- ENV_ACCESS_READER = proc do |super_method, *args, &block|
46
+ ENV_ACCESS_READER = proc do |wrapped_method, *args, &block|
42
47
  # If there are no arguments, there's nothing to do with paths. Just
43
48
  # delegate to the hash.
44
49
  if args.empty?
45
- next super_method.call(*args, &block)
50
+ next wrapped_method.call(*args, &block)
46
51
  end
47
52
 
48
- # The method's receiver is encapsulated in the super_method; we'll
53
+ # The method's receiver is encapsulated in the wrapped_method; we'll
49
54
  # use it a few times so let's reduce typing. This is essentially the
50
55
  # equivalent of `self`.
51
- receiver = super_method.receiver
56
+ receiver = wrapped_method.receiver
52
57
 
53
58
  # All KEYED_READ_METHODS have a key as the first argument.
54
59
  key = args[0]
55
60
 
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)
61
+ # Grab matching environment variable names. If PathedAccess is
62
+ # supported, we'll try environment variables of the path, starting
63
+ # with the full qualified path and ending with just the last key
64
+ # component.
65
+ env_keys = []
66
+ if receiver.respond_to?(:path_prefix)
67
+
68
+ # If we have a prefix, use it.
69
+ components = []
70
+ if not receiver.path_prefix.nil?
71
+ components += receiver.path_components(receiver.path_prefix)
72
+ end
73
+
74
+ # The key has its own components. If the key components and prefix
75
+ # components overlap (it happens), ignore the duplication.
76
+ key_comps = receiver.path_components(key.to_s)
77
+ if key_comps.first == components.last
78
+ components.pop
79
+ end
80
+ components += key_comps
81
+
82
+ # Start with most qualified, shifting off the first component
83
+ # until we reach just the last component.
84
+ loop do
85
+ path = receiver.normalize_path(components)
86
+ env_keys << path
87
+ components.shift
88
+ if components.empty?
89
+ break
90
+ end
67
91
  end
92
+ else
93
+ env_keys = [key.to_s]
68
94
  end
69
95
  env_keys.map! { |k| receiver.key_to_env(k) }
70
96
  env_keys.select! { |k| not k.empty? }
@@ -84,47 +110,44 @@ module Collapsium
84
110
  # the parsed result. Otherwise use it as a string.
85
111
  require 'json'
86
112
  # rubocop:disable Lint/HandleExceptions
113
+ parsed = env_value
87
114
  begin
88
- env_value = JSON.parse(env_value)
115
+ parsed = JSON.parse(env_value)
89
116
  rescue JSON::ParserError
90
117
  # Do nothing. We just use the env_value verbatim.
91
118
  end
92
119
  # rubocop:enable Lint/HandleExceptions
93
120
 
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
121
+ # Excellent, we've got an environment variable. Only use it if it
122
+ # changes something, though. We'll temporarily unset the environment
123
+ # variable while fetching the old value.
124
+ ENV.delete(env_key)
125
+ old_value = receiver[key]
126
+ ENV[env_key] = env_value # not the parsed value!
104
127
 
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
128
+ if parsed != old_value
129
+ value = parsed
112
130
  end
113
131
  break
114
132
  end
133
+
115
134
  if not value.nil?
116
135
  # 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
136
+ # method being called. We also can't store the value, because that
137
+ # would not reset the Hash after the environment variable was
138
+ # cleared.
139
+ #
140
+ # We can deal with this by duplicating the receiver, writing the value
141
+ # we found into the appropriate key, and then sending the
142
+ # wrapped_method to the duplicate.
143
+ double = receiver.recursive_dup
121
144
  double[key] = value
122
- meth = double.method(super_method.name)
145
+ meth = double.method(wrapped_method.name)
123
146
  next meth.call(*args, &block)
124
147
  end
125
148
 
126
149
  # Otherwise, fall back on the super method.
127
- next super_method.call(*args, &block)
150
+ next wrapped_method.call(*args, &block)
128
151
  end.freeze # proc
129
152
 
130
153
  def included(base)
@@ -140,7 +163,7 @@ module Collapsium
140
163
  end
141
164
 
142
165
  def enhance(base)
143
- # Make the capabilities of classes using PathedAccess viral.
166
+ # Make the capabilities of classes using EnvironmentOverride viral.
144
167
  base.extend(ViralCapabilities)
145
168
 
146
169
  # Wrap read accessor functions to deal with paths
@@ -66,17 +66,17 @@ module Collapsium
66
66
  # access will create intermediary hashes when e.g. setting a value for
67
67
  # `foo.bar.baz`, and the `bar` Hash doesn't exist yet.
68
68
  def create_proc(write_access)
69
- return proc do |super_method, *args, &block|
69
+ return proc do |wrapped_method, *args, &block|
70
70
  # If there are no arguments, there's nothing to do with paths. Just
71
71
  # delegate to the hash.
72
72
  if args.empty?
73
- next super_method.call(*args, &block)
73
+ next wrapped_method.call(*args, &block)
74
74
  end
75
75
 
76
- # The method's receiver is encapsulated in the super_method; we'll
76
+ # The method's receiver is encapsulated in the wrapped_method; we'll
77
77
  # use it a few times so let's reduce typing. This is essentially the
78
78
  # equivalent of `self`.
79
- receiver = super_method.receiver
79
+ receiver = wrapped_method.receiver
80
80
 
81
81
  # With any of the dispatch methods, we know that the first argument has
82
82
  # to be a key. We'll try to split it by the path separator.
@@ -95,12 +95,12 @@ module Collapsium
95
95
  if receiver.object_id == leaf.object_id
96
96
  # a) if the leaf and the receiver are identical, then the receiver
97
97
  # itself was requested, and we really just need to delegate to its
98
- # super_method.
99
- meth = super_method
98
+ # wrapped_method.
99
+ meth = wrapped_method
100
100
  else
101
101
  # b) if the leaf is different from the receiver, we want to delegate
102
102
  # to the leaf.
103
- meth = leaf.method(super_method.name)
103
+ meth = leaf.method(wrapped_method.name)
104
104
  end
105
105
 
106
106
  # If the first argument was a symbol key, we want to use it verbatim.
@@ -16,10 +16,9 @@ module Collapsium
16
16
  ##
17
17
  # Functionality for extending the behaviour of Hash methods
18
18
  module Methods
19
-
20
19
  ##
21
20
  # Given the base module, wraps the given method name in the given block.
22
- # The block must accept the super_method as the first parameter, followed
21
+ # The block must accept the wrapped_method as the first parameter, followed
23
22
  # by any arguments and blocks the super method might accept.
24
23
  #
25
24
  # The canonical usage example is of a module that when prepended wraps
@@ -31,9 +30,9 @@ module Collapsium
31
30
  # include ::Collapsium::Support::Methods
32
31
  #
33
32
  # def prepended(base)
34
- # wrap_method(base, :method_to_wrap) do |super_method, *args, &block|
33
+ # wrap_method(base, :method_name) do |wrapped_method, *args, &block|
35
34
  # # modify args, if desired
36
- # result = super_method.call(*args, &block)
35
+ # result = wrapped_method.call(*args, &block)
37
36
  # # do something with the result, if desired
38
37
  # next result
39
38
  # end
@@ -41,7 +40,59 @@ module Collapsium
41
40
  # end
42
41
  # end
43
42
  # ```
44
- def wrap_method(base, method_name, raise_on_missing = true, &wrapper_block)
43
+ def wrap_method(base, method_name, options = {}, &wrapper_block)
44
+ # Option defaults (need to check for nil if we default to true)
45
+ if options[:raise_on_missing].nil?
46
+ options[:raise_on_missing] = true
47
+ end
48
+
49
+ # Grab helper methods
50
+ base_method, def_method = resolve_helpers(base, method_name,
51
+ options[:raise_on_missing])
52
+ if base_method.nil?
53
+ # Indicates that we're not done building a Module yet
54
+ return
55
+ end
56
+
57
+ # Hack for calling the private method "define_method"
58
+ def_method.call(method_name) do |*args, &method_block|
59
+ # We're trying to prevent loops by maintaining a stack of wrapped
60
+ # method invocations.
61
+ @__collapsium_methods_callstack ||= []
62
+
63
+ # Our current binding is based on the wrapper block and our own class,
64
+ # as well as the arguments (CRC32).
65
+ require 'zlib'
66
+ signature = Zlib::crc32(JSON::dump(args))
67
+ the_binding = [wrapper_block.object_id, self.class.object_id, signature]
68
+
69
+ # We'll either pass the wrapped method to the wrapper block, or invoke
70
+ # it ourselves.
71
+ wrapped_method = base_method.bind(self)
72
+
73
+ # If we do find a loop with the current binding involved, we'll just
74
+ # call the wrapped method.
75
+ if Methods.loop_detected?(the_binding, @__collapsium_methods_callstack)
76
+ next wrapped_method.call(*args, &method_block)
77
+ end
78
+
79
+ # If there is no loop, call the wrapper block and pass along the
80
+ # wrapped method as the first argument.
81
+ args.unshift(wrapped_method)
82
+
83
+ # Then yield to the given wrapper block. The wrapper should decide
84
+ # whether to call the old method or not. But by modifying our stack
85
+ # before/after the invocation, we allow the loop detection above to
86
+ # work.
87
+ @__collapsium_methods_callstack << the_binding
88
+ result = wrapper_block.call(*args, &method_block)
89
+ @__collapsium_methods_callstack.pop
90
+
91
+ next result
92
+ end
93
+ end
94
+
95
+ def resolve_helpers(base, method_name, raise_on_missing)
45
96
  # The base class must define an instance method of method_name, otherwise
46
97
  # this will NameError. That's also a good check that sensible things are
47
98
  # being done.
@@ -56,7 +107,7 @@ module Collapsium
56
107
  if raise_on_missing
57
108
  raise
58
109
  end
59
- return
110
+ return base_method, def_method
60
111
  end
61
112
  def_method = base.method(:define_method)
62
113
  else
@@ -68,18 +119,31 @@ module Collapsium
68
119
  def_method = base.method(:define_singleton_method)
69
120
  end
70
121
 
71
- # Hack for calling the private method "define_method"
72
- def_method.call(method_name) do |*args, &method_block|
73
- # Prepend the old method to the argument list; but bind it to the current
74
- # instance.
75
- super_method = base_method.bind(self)
76
- args.unshift(super_method)
122
+ return base_method, def_method
123
+ end
77
124
 
78
- # Then yield to the given wrapper block. The wrapper should decide
79
- # whether to call the old method or not.
80
- next wrapper_block.call(*args, &method_block)
125
+ class << self
126
+ # Given an input array, return repeated sequences from the array. It's
127
+ # used in loop detection.
128
+ def repeated(array)
129
+ counts = Hash.new(0)
130
+ array.each { |val| counts[val] += 1 }
131
+ return counts.reject { |_, count| count == 1 }.keys
81
132
  end
82
- end
133
+
134
+ # Given a call stack and a binding, returns true if there seems to be a
135
+ # loop in the call stack with the binding causing it, false otherwise.
136
+ def loop_detected?(the_binding, stack)
137
+ # Make a temporary stack with the binding pushed
138
+ tmp_stack = stack.dup
139
+ tmp_stack << the_binding
140
+ loops = Methods.repeated(tmp_stack)
141
+
142
+ # If we do find a loop with the current binding involved, we'll just
143
+ # call the wrapped method.
144
+ return loops.include?(the_binding)
145
+ end
146
+ end # class << self
83
147
 
84
148
  end # module Methods
85
149
 
@@ -8,5 +8,5 @@
8
8
  #
9
9
  module Collapsium
10
10
  # The current release version
11
- VERSION = "0.4.0".freeze
11
+ VERSION = "0.4.1".freeze
12
12
  end
@@ -67,25 +67,25 @@ module Collapsium
67
67
  # a wrapper that uses enhance_hash_value to, well, enhance Hash results.
68
68
  def enhance(base)
69
69
  # rubocop:disable Style/ClassVars
70
- @@write_block ||= proc do |super_method, *args, &block|
70
+ @@write_block ||= proc do |wrapped_method, *args, &block|
71
71
  arg_copy = args.map do |arg|
72
- enhance_hash_value(super_method.receiver, arg)
72
+ enhance_hash_value(wrapped_method.receiver, arg)
73
73
  end
74
- result = super_method.call(*arg_copy, &block)
75
- next enhance_hash_value(super_method.receiver, result)
74
+ result = wrapped_method.call(*arg_copy, &block)
75
+ next enhance_hash_value(wrapped_method.receiver, result)
76
76
  end
77
- @@read_block ||= proc do |super_method, *args, &block|
78
- result = super_method.call(*args, &block)
79
- next enhance_hash_value(super_method.receiver, result)
77
+ @@read_block ||= proc do |wrapped_method, *args, &block|
78
+ result = wrapped_method.call(*args, &block)
79
+ next enhance_hash_value(wrapped_method.receiver, result)
80
80
  end
81
81
  # rubocop:enable Style/ClassVars
82
82
 
83
83
  READ_METHODS.each do |method_name|
84
- wrap_method(base, method_name, false, &@@read_block)
84
+ wrap_method(base, method_name, raise_on_missing: false, &@@read_block)
85
85
  end
86
86
 
87
87
  WRITE_METHODS.each do |method_name|
88
- wrap_method(base, method_name, false, &@@write_block)
88
+ wrap_method(base, method_name, raise_on_missing: false, &@@write_block)
89
89
  end
90
90
  end
91
91
 
@@ -37,7 +37,7 @@ describe ::Collapsium::EnvironmentOverride do
37
37
  end
38
38
  end
39
39
 
40
- context "without pathed access" do
40
+ context "without PathedAccess" do
41
41
  it "overrides first-order keys" do
42
42
  expect(@tester["foo"].is_a?(Hash)).to be_truthy
43
43
  ENV["FOO"] = "test"
@@ -56,16 +56,30 @@ describe ::Collapsium::EnvironmentOverride do
56
56
  @tester.store("foo", 42)
57
57
  expect(@tester["foo"]).to eql 42
58
58
  end
59
+
60
+ it "changes with every env change" do
61
+ ENV["BAR"] = "test1"
62
+ expect(@tester["foo"]["bar"]).to eql "test1"
63
+ ENV["BAR"] = "test2"
64
+ expect(@tester["foo"]["bar"]).to eql "test2"
65
+ end
66
+
67
+ it "resets when the env resets" do
68
+ ENV["BAR"] = "test"
69
+ expect(@tester["foo"]["bar"]).to eql "test"
70
+ ENV.delete("BAR")
71
+ expect(@tester["foo"]["bar"]).to eql 42
72
+ end
59
73
  end
60
74
 
61
- context "with pathed access" do
75
+ context "with PathedAccess" do
62
76
  before :each do
63
77
  @tester = { "foo" => { "bar" => 42 } }
64
- @tester.extend(::Collapsium::EnvironmentOverride)
65
78
  @tester.extend(::Collapsium::PathedAccess)
66
- ENV["FOO"] = nil
67
- ENV["BAR"] = nil
68
- ENV["FOO_BAR"] = nil
79
+ @tester.extend(::Collapsium::EnvironmentOverride)
80
+ ENV.delete("FOO")
81
+ ENV.delete("BAR")
82
+ ENV.delete("FOO_BAR")
69
83
  end
70
84
 
71
85
  it "overrides first-order keys" do
@@ -76,10 +90,10 @@ describe ::Collapsium::EnvironmentOverride do
76
90
  end
77
91
 
78
92
  it "inherits environment override" do
79
- expect(@tester["foo"]["bar"].is_a?(Fixnum)).to be_truthy
93
+ expect(@tester["foo.bar"].is_a?(Fixnum)).to be_truthy
80
94
  ENV["BAR"] = "test"
81
- expect(@tester["foo"]["bar"].is_a?(Fixnum)).to be_falsy
82
- expect(@tester["foo"]["bar"]).to eql "test"
95
+ expect(@tester["foo.bar"].is_a?(Fixnum)).to be_falsy
96
+ expect(@tester["foo.bar"]).to eql "test"
83
97
  end
84
98
 
85
99
  it "write still works" do
@@ -87,6 +101,20 @@ describe ::Collapsium::EnvironmentOverride do
87
101
  expect(@tester["foo"]).to eql 42
88
102
  end
89
103
 
104
+ it "changes with every env change" do
105
+ ENV["BAR"] = "test1"
106
+ expect(@tester["foo.bar"]).to eql "test1"
107
+ ENV["BAR"] = "test2"
108
+ expect(@tester["foo.bar"]).to eql "test2"
109
+ end
110
+
111
+ it "resets when the env resets" do
112
+ ENV["BAR"] = "test"
113
+ expect(@tester["foo.bar"]).to eql "test"
114
+ ENV.delete("BAR")
115
+ expect(@tester["foo.bar"]).to eql 42
116
+ end
117
+
90
118
  it "overrides from pathed key" do
91
119
  expect(@tester["foo.bar"].is_a?(Fixnum)).to be_truthy
92
120
  ENV["FOO_BAR"] = "test"
@@ -144,11 +172,22 @@ describe ::Collapsium::EnvironmentOverride do
144
172
  expect(@tester["foo"].is_a?(Hash)).to be_truthy
145
173
  expect(@tester["foo"]["json_key"]).to eql "json_value"
146
174
  end
175
+
176
+ it "respects PathedAccess" do
177
+ @tester = { "foo" => { "bar" => 42 } }
178
+ @tester.extend(::Collapsium::PathedAccess)
179
+ @tester.extend(::Collapsium::EnvironmentOverride)
180
+
181
+ ENV["FOO_BAR"] = '{ "json_key": "json_value" }'
182
+ expect(@tester["foo.bar"].is_a?(Hash)).to be_truthy
183
+ expect(@tester["foo.bar"]["json_key"]).to eql "json_value"
184
+ expect(@tester["foo"]["bar.json_key"]).to eql "json_value"
185
+ end
147
186
  end
148
187
 
149
188
  context "coverage" do
150
189
  it "raises when not passing arguments" do
151
- expect { @tester.fetch }.to raise_error
190
+ expect { @tester.fetch }.to raise_error(ArgumentError)
152
191
  end
153
192
  end
154
193
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: collapsium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jens Finkhaeuser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-08-17 00:00:00.000000000 Z
11
+ date: 2016-08-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler