collapsium 0.4.0 → 0.4.1

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: 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