climate_control 1.0.1 → 1.2.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
  SHA256:
3
- metadata.gz: aac4a1d4fe272e5369855d9d80c8bfd764b752f27d1bdfad2b19a607d5f0a44e
4
- data.tar.gz: 8945986d5956f681810ef8a5f164323a3cd2a48e82368618c292f3c6fc7a6caa
3
+ metadata.gz: '00584062039c6b9161ff0e4a1b61ca51e4d8742225fc611180c4effcc43959c5'
4
+ data.tar.gz: 99135f45c57f58d87e17480896045c00eecebd632ed64eaf18bd1964ef7d454d
5
5
  SHA512:
6
- metadata.gz: c36a6b4ead29b23f93c95457e56f6bc3fd6c5cd596f66fe8243e736edcc5a11cfe9fd5fe62b1cf5fc8cd0c7f4fe72c051c0560338a6496e30c56b3cf095c92ec
7
- data.tar.gz: cbdf93ff0a149ef67129db0ac0f25838f02c7d2faf6dea87f8ce0acaf54848fb129f4d1b8cbf0ad40c50358cd2186e5a726b2fed5d82fbc9de2c765f4a9bf94d
6
+ metadata.gz: 01ba4ad3241cea796d408f0adb02f8c23b85771d1ce8338eedf973b95228a7cac9978218f08921915918c90a0f8f292dbb38f9401ca29de8d7a6ef3dc76d99b5
7
+ data.tar.gz: 9b20ce4affbcf22b6204016d31b7b54465ad5ba2a143fd2e6578d3dc5d4c80ac167c02c9ccc47d91c2a62441c5ce3037ebb13c445407876c163edaba73eb484d
@@ -6,7 +6,7 @@ jobs:
6
6
  runs-on: ubuntu-latest
7
7
  strategy:
8
8
  matrix:
9
- ruby-version: ['3.0', 2.7, 2.6, 2.5]
9
+ ruby-version: [3.1, '3.0', 2.7, 2.6, 2.5]
10
10
  steps:
11
11
  - uses: actions/checkout@v2
12
12
  - name: Set up Ruby ${{ matrix.ruby-version }}
data/CHANGELOG.md ADDED
@@ -0,0 +1,53 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## 1.2.0 / 2022-07-15
9
+
10
+ - Added: `ClimateControl.unsafe_modify` for a thread-unsafe version of
11
+ `ClimateControl.modify` (useful for minitest-around for instance)
12
+ - Deprecates `ClimateControl.env`, `ENV` should be used instead
13
+
14
+ ## 1.1.1 / 2022-05-28
15
+
16
+ - Fixed: ENV was not restored if an error was thrown when assigning ENV
17
+
18
+ ## 1.1.0 / 2022-05-26
19
+
20
+ - Refactor to use `Monitor` instead of `Mutex`
21
+ - Add documentation about thread-safety
22
+ - Allow ClimateControl.modify to be called without environment variables
23
+ - Add test for concurrent access needed to be inside block
24
+ - Relax development dependencies
25
+
26
+ ## 1.0.1 / 2021-05-26
27
+
28
+ - Require minimum Ruby version of 2.5.0
29
+
30
+ # 1.0.0 / 2021-03-06
31
+
32
+ - Commit to supporting latest patch versions of Ruby 2.5+
33
+ - Improve documentation
34
+ - Format code with StandardRB
35
+ - Bump gem dependencies
36
+
37
+ # 0.2.0 / 2017-05-12
38
+
39
+ - Allow nested environment changes in the same thread
40
+
41
+ # 0.1.0 / 2017-01-07
42
+
43
+ - Remove ActiveSupport dependency
44
+
45
+ # 0.0.4 / 2017-01-06
46
+
47
+ - Improved thread safety
48
+ - Handle TypeErrors during assignment
49
+ - Improve documentation
50
+
51
+ # 0.0.1 / 2012-11-28
52
+
53
+ - Initial release
data/CODEOWNERS ADDED
@@ -0,0 +1 @@
1
+ * @dorianmariefr
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Climate Control
2
2
 
3
+ ![GitHub Actions CI](https://github.com/thoughtbot/climate_control/actions/workflows/ci.yml/badge.svg)
4
+
3
5
  Easily manage your environment.
4
6
 
5
7
  ## Installation
@@ -48,7 +50,7 @@ end
48
50
  To use with RSpec, you could define this in your spec:
49
51
 
50
52
  ```ruby
51
- def with_modified_env(options, &block)
53
+ def with_modified_env(options = {}, &block)
52
54
  ClimateControl.modify(options, &block)
53
55
  end
54
56
  ```
@@ -105,6 +107,39 @@ manner becomes more difficult:
105
107
  Climate Control modifies environment variables only within the context of the
106
108
  block, ensuring values are managed properly and consistently.
107
109
 
110
+ ## Thread-safety
111
+
112
+ When using threads, for instance when running tests concurrently in the same
113
+ process, you may need to wrap your code inside `ClimateControl.modify` blocks,
114
+ e.g.:
115
+
116
+ ```ruby
117
+ first_thread = Thread.new do
118
+ ClimateControl.modify(SECRET: "1") do
119
+ p ENV["SECRET"] # => "1"
120
+ sleep 2
121
+ p ENV["SECRET"] # => "1"
122
+ end
123
+ end
124
+
125
+ second_thread = Thread.new do
126
+ ClimateControl.modify({}) do
127
+ sleep 1
128
+ p ENV["SECRET"] # => nil
129
+ sleep 1
130
+ p ENV["SECRET"] # => nil
131
+ end
132
+ end
133
+
134
+ first_thread.join
135
+ second_thread.join
136
+ ```
137
+
138
+ > The modification wraps ENV in a mutex. If there's contention (the env being used - including potentially mutating values), it blocks until the value is freed (we shift out of the Ruby block).
139
+ >
140
+ > <cite><a href="https://github.com/thoughtbot/climate_control/issues/32#issuecomment-800713686">Josh Clayton</a></cite>
141
+
142
+
108
143
  ## Contributing
109
144
 
110
145
  1. Fork it
@@ -118,3 +153,18 @@ This project uses [StandardRB](https://github.com/testdouble/standard) to ensure
118
153
  ## License
119
154
 
120
155
  climate_control is copyright 2012-2021 Joshua Clayton and thoughtbot, inc. It is free software and may be redistributed under the terms specified in the [LICENSE](https://github.com/thoughtbot/climate_control/blob/main/LICENSE) file.
156
+
157
+ About thoughtbot
158
+ ----------------
159
+
160
+ ![thoughtbot](https://thoughtbot.com/brand_assets/93:44.svg)
161
+
162
+ climate_control is maintained and funded by thoughtbot, inc.
163
+ The names and logos for thoughtbot are trademarks of thoughtbot, inc.
164
+
165
+ We love open source software!
166
+ See [our other projects][community] or
167
+ [hire us][hire] to design, develop, and grow your product.
168
+
169
+ [community]: https://thoughtbot.com/community?utm_source=github
170
+ [hire]: https://thoughtbot.com/hire-us?utm_source=github
@@ -5,8 +5,8 @@ require "climate_control/version"
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "climate_control"
7
7
  gem.version = ClimateControl::VERSION
8
- gem.authors = ["Joshua Clayton"]
9
- gem.email = ["joshua.clayton@gmail.com"]
8
+ gem.authors = ["Joshua Clayton", "Dorian Marié"]
9
+ gem.email = ["joshua.clayton@gmail.com", "dorian@dorianmarie.fr"]
10
10
  gem.description = "Modify your ENV"
11
11
  gem.summary = "Modify your ENV easily with ClimateControl"
12
12
  gem.homepage = "https://github.com/thoughtbot/climate_control"
@@ -18,8 +18,8 @@ Gem::Specification.new do |gem|
18
18
 
19
19
  gem.required_ruby_version = ">= 2.5.0"
20
20
 
21
- gem.add_development_dependency "rspec", "~> 3.10.0"
22
- gem.add_development_dependency "rake", "~> 12.3.3"
23
- gem.add_development_dependency "simplecov", "~> 0.9.1"
24
- gem.add_development_dependency "standard", "~> 1.0.0"
21
+ gem.add_development_dependency "rspec"
22
+ gem.add_development_dependency "rake"
23
+ gem.add_development_dependency "simplecov"
24
+ gem.add_development_dependency "standard"
25
25
  end
@@ -1,3 +1,3 @@
1
1
  module ClimateControl
2
- VERSION = "1.0.1".freeze
2
+ VERSION = "1.2.0".freeze
3
3
  end
@@ -1,16 +1,72 @@
1
- require "climate_control/environment"
2
1
  require "climate_control/errors"
3
- require "climate_control/modifier"
4
2
  require "climate_control/version"
3
+ require "monitor"
5
4
 
6
5
  module ClimateControl
7
- @@env = ClimateControl::Environment.new
6
+ extend self
7
+ extend Gem::Deprecate
8
8
 
9
- def self.modify(environment_overrides, &block)
10
- Modifier.new(env, environment_overrides, &block).process
9
+ SEMAPHORE = Monitor.new
10
+ private_constant :SEMAPHORE
11
+
12
+ def modify(environment_overrides = {}, &block)
13
+ environment_overrides = environment_overrides.transform_keys(&:to_s)
14
+
15
+ SEMAPHORE.synchronize do
16
+ previous = ENV.to_hash
17
+
18
+ begin
19
+ copy environment_overrides
20
+ ensure
21
+ middle = ENV.to_hash
22
+ end
23
+
24
+ block.call
25
+ ensure
26
+ after = ENV
27
+ (previous.keys | middle.keys | after.keys).each do |key|
28
+ if previous[key] != after[key] && middle[key] == after[key]
29
+ ENV[key] = previous[key]
30
+ end
31
+ end
32
+ end
11
33
  end
12
34
 
13
- def self.env
14
- @@env
35
+ def unsafe_modify(environment_overrides = {}, &block)
36
+ environment_overrides = environment_overrides.transform_keys(&:to_s)
37
+
38
+ previous = ENV.to_hash
39
+
40
+ begin
41
+ copy environment_overrides
42
+ ensure
43
+ middle = ENV.to_hash
44
+ end
45
+
46
+ block.call
47
+ ensure
48
+ after = ENV
49
+ (previous.keys | middle.keys | after.keys).each do |key|
50
+ if previous[key] != after[key] && middle[key] == after[key]
51
+ ENV[key] = previous[key]
52
+ end
53
+ end
54
+ end
55
+
56
+ def env
57
+ ENV
58
+ end
59
+
60
+ deprecate :env, "ENV", 2022, 10
61
+
62
+ private
63
+
64
+ def copy(overrides)
65
+ overrides.each do |key, value|
66
+ ENV[key] = value
67
+ rescue TypeError => e
68
+ raise UnassignableValueError,
69
+ "attempted to assign #{value} to #{key} but failed (#{e.message})"
70
+ end
15
71
  end
16
72
  end
@@ -5,7 +5,7 @@ Thing = Class.new
5
5
  describe "Climate control" do
6
6
  it "allows modification of the environment" do
7
7
  block_run = false
8
- ClimateControl.modify FOO: "bar" do
8
+ with_modified_env FOO: "bar" do
9
9
  expect(ENV["FOO"]).to eq "bar"
10
10
  block_run = true
11
11
  end
@@ -113,6 +113,39 @@ describe "Climate control" do
113
113
  expect(ENV["BAZ"]).to be_nil
114
114
  end
115
115
 
116
+ it "handles threads accessing the same key wrapped in a block" do
117
+ first_thread = Thread.new {
118
+ with_modified_env do
119
+ with_modified_env CONFLICTING_KEY: "1" do
120
+ sleep 0.5
121
+ expect(ENV["CONFLICTING_KEY"]).to eq("1")
122
+ end
123
+
124
+ expect(ENV["CONFLICTING_KEY"]).to be_nil
125
+ end
126
+ }
127
+
128
+ second_thread = Thread.new {
129
+ with_modified_env do
130
+ sleep 0.25
131
+ expect(ENV["CONFLICTING_KEY"]).to be_nil
132
+
133
+ with_modified_env CONFLICTING_KEY: "2" do
134
+ expect(ENV["CONFLICTING_KEY"]).to eq("2")
135
+ sleep 0.5
136
+ expect(ENV["CONFLICTING_KEY"]).to eq("2")
137
+ end
138
+
139
+ expect(ENV["CONFLICTING_KEY"]).to be_nil
140
+ end
141
+ }
142
+
143
+ first_thread.join
144
+ second_thread.join
145
+
146
+ expect(ENV["CONFLICTING_KEY"]).to be_nil
147
+ end
148
+
116
149
  it "is re-entrant" do
117
150
  ret = with_modified_env(FOO: "foo") {
118
151
  with_modified_env(BAR: "bar") do
@@ -134,7 +167,32 @@ describe "Climate control" do
134
167
  }.to raise_error ClimateControl::UnassignableValueError, /attempted to assign .*Thing.* to FOO but failed \(#{message}\)$/
135
168
  end
136
169
 
137
- def with_modified_env(options, &block)
170
+ it "restores the ENV even when an error was raised when assigning values" do
171
+ ENV["KEY_TO_OVERRIDE"] = "initial_value_1"
172
+ ENV["KEY_THAT_WILL_ERROR_OUT"] = "initial_value_2"
173
+
174
+ expect {
175
+ with_modified_env(
176
+ KEY_TO_OVERRIDE: "overwriten_value_1",
177
+ KEY_THAT_WILL_ERROR_OUT: :value_that_will_error_out
178
+ ) {}
179
+ }.to raise_error ClimateControl::UnassignableValueError
180
+
181
+ expect(ENV["KEY_TO_OVERRIDE"]).to eq("initial_value_1")
182
+ expect(ENV["KEY_THAT_WILL_ERROR_OUT"]).to eq("initial_value_2")
183
+ end
184
+
185
+ it "doesn't block on nested modify calls" do
186
+ with_modified_env(SMS_DEFAULT_COUNTRY_CODE: nil) do
187
+ with_modified_env(SMS_DEFAULT_COUNTRY_CODE: "++56") do
188
+ expect(ENV.fetch("SMS_DEFAULT_COUNTRY_CODE", "++41")).to eq("++56")
189
+ end
190
+
191
+ expect(ENV.fetch("SMS_DEFAULT_COUNTRY_CODE", "++41")).to eq("++41")
192
+ end
193
+ end
194
+
195
+ def with_modified_env(options = {}, &block)
138
196
  ClimateControl.modify(options, &block)
139
197
  end
140
198
 
@@ -0,0 +1,186 @@
1
+ require "spec_helper"
2
+
3
+ describe "ClimateControl#unsafe_modify" do
4
+ it "allows modification of the environment" do
5
+ block_run = false
6
+ with_modified_env FOO: "bar" do
7
+ expect(ENV["FOO"]).to eq "bar"
8
+ block_run = true
9
+ end
10
+
11
+ expect(ENV["FOO"]).to be_nil
12
+ expect(block_run).to be true
13
+ end
14
+
15
+ it "modifies the environment" do
16
+ with_modified_env VARIABLE_1: "bar", VARIABLE_2: "qux" do
17
+ expect(ENV["VARIABLE_1"]).to eq "bar"
18
+ expect(ENV["VARIABLE_2"]).to eq "qux"
19
+ end
20
+
21
+ expect(ENV["VARIABLE_1"]).to be_nil
22
+ expect(ENV["VARIABLE_2"]).to be_nil
23
+ end
24
+
25
+ it "allows for environment variables to be assigned within the block" do
26
+ with_modified_env VARIABLE_1: "modified" do
27
+ ENV["ASSIGNED_IN_BLOCK"] = "assigned"
28
+ end
29
+
30
+ expect(ENV["ASSIGNED_IN_BLOCK"]).to eq "assigned"
31
+ end
32
+
33
+ it "reassigns previously set environment variables" do
34
+ ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"] = "original"
35
+ expect(ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"]).to eq "original"
36
+
37
+ with_modified_env VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV: "overridden" do
38
+ expect(ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"]).to eq "overridden"
39
+ end
40
+
41
+ expect(ENV["VARIABLE_ASSIGNED_BEFORE_MODIFYING_ENV"]).to eq "original"
42
+ end
43
+
44
+ it "persists the change when overriding the variable in the block" do
45
+ with_modified_env VARIABLE_MODIFIED_AND_THEN_ASSIGNED: "modified" do
46
+ ENV["VARIABLE_MODIFIED_AND_THEN_ASSIGNED"] = "assigned value"
47
+ end
48
+
49
+ expect(ENV["VARIABLE_MODIFIED_AND_THEN_ASSIGNED"]).to eq "assigned value"
50
+ end
51
+
52
+ it "resets environment variables even if the block raises" do
53
+ expect {
54
+ with_modified_env FOO: "bar" do
55
+ raise "broken"
56
+ end
57
+ }.to raise_error("broken")
58
+
59
+ expect(ENV["FOO"]).to be_nil
60
+ end
61
+
62
+ it "preserves environment variables set within the block" do
63
+ ENV["CHANGED"] = "old value"
64
+
65
+ with_modified_env IRRELEVANT: "ignored value" do
66
+ ENV["CHANGED"] = "new value"
67
+ end
68
+
69
+ expect(ENV["CHANGED"]).to eq "new value"
70
+ end
71
+
72
+ it "returns the value of the block" do
73
+ value = with_modified_env VARIABLE_1: "bar" do
74
+ "value inside block"
75
+ end
76
+
77
+ expect(value).to eq "value inside block"
78
+ end
79
+
80
+ it "handles threads correctly" do
81
+ # failure path without mutex
82
+ # [thread_removing_env] BAZ is assigned
83
+ # 0.25s passes
84
+ # [other_thread] FOO is assigned and ENV is copied (which includes BAZ)
85
+ # 0.25s passes
86
+ # [thread_removing_env] thread resolves and BAZ is removed from env; other_thread still retains knowledge of BAZ
87
+ # 0.25s passes
88
+ # [other_thread] thread resolves, FOO is removed, BAZ is copied back to ENV
89
+
90
+ thread_removing_env = Thread.new {
91
+ with_modified_env BAZ: "buzz" do
92
+ sleep 0.5
93
+ end
94
+
95
+ expect(ENV["BAZ"]).to be_nil
96
+ }
97
+
98
+ other_thread = Thread.new {
99
+ sleep 0.25
100
+ with_modified_env FOO: "bar" do
101
+ sleep 0.5
102
+ end
103
+
104
+ expect(ENV["FOO"]).to be_nil
105
+ }
106
+
107
+ thread_removing_env.join
108
+ other_thread.join
109
+
110
+ expect(ENV["FOO"]).to be_nil
111
+ expect(ENV["BAZ"]).to be_nil
112
+ end
113
+
114
+ it "is re-entrant" do
115
+ ret = with_modified_env(FOO: "foo") {
116
+ with_modified_env(BAR: "bar") do
117
+ "bar"
118
+ end
119
+ }
120
+
121
+ expect(ret).to eq("bar")
122
+
123
+ expect(ENV["FOO"]).to be_nil
124
+ expect(ENV["BAR"]).to be_nil
125
+ end
126
+
127
+ it "raises when the value cannot be assigned properly" do
128
+ message = generate_type_error_for_object(Thing.new)
129
+
130
+ expect {
131
+ with_modified_env(FOO: Thing.new)
132
+ }.to raise_error ClimateControl::UnassignableValueError, /attempted to assign .*Thing.* to FOO but failed \(#{message}\)$/
133
+ end
134
+
135
+ it "restores the ENV even when an error was raised when assigning values" do
136
+ ENV["KEY_TO_OVERRIDE"] = "initial_value_1"
137
+ ENV["KEY_THAT_WILL_ERROR_OUT"] = "initial_value_2"
138
+
139
+ expect {
140
+ with_modified_env(
141
+ KEY_TO_OVERRIDE: "overwriten_value_1",
142
+ KEY_THAT_WILL_ERROR_OUT: :value_that_will_error_out
143
+ ) {}
144
+ }.to raise_error ClimateControl::UnassignableValueError
145
+
146
+ expect(ENV["KEY_TO_OVERRIDE"]).to eq("initial_value_1")
147
+ expect(ENV["KEY_THAT_WILL_ERROR_OUT"]).to eq("initial_value_2")
148
+ end
149
+
150
+ it "doesn't block on nested modify calls" do
151
+ with_modified_env(SMS_DEFAULT_COUNTRY_CODE: nil) do
152
+ with_modified_env(SMS_DEFAULT_COUNTRY_CODE: "++56") do
153
+ expect(ENV.fetch("SMS_DEFAULT_COUNTRY_CODE", "++41")).to eq("++56")
154
+ end
155
+
156
+ expect(ENV.fetch("SMS_DEFAULT_COUNTRY_CODE", "++41")).to eq("++41")
157
+ end
158
+ end
159
+
160
+ def with_modified_env(options = {}, &block)
161
+ ClimateControl.unsafe_modify(options, &block)
162
+ end
163
+
164
+ def generate_type_error_for_object(object)
165
+ message = nil
166
+
167
+ begin
168
+ "1" + object
169
+ rescue TypeError => e
170
+ message = e.message
171
+ end
172
+
173
+ message
174
+ end
175
+
176
+ around do |example|
177
+ old_env = ENV.to_hash
178
+
179
+ example.run
180
+
181
+ ENV.clear
182
+ old_env.each do |key, value|
183
+ ENV[key] = value
184
+ end
185
+ end
186
+ end
metadata CHANGED
@@ -1,93 +1,95 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: climate_control
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Clayton
8
+ - Dorian Marié
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2021-05-27 00:00:00.000000000 Z
12
+ date: 2022-07-15 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: rspec
15
16
  requirement: !ruby/object:Gem::Requirement
16
17
  requirements:
17
- - - "~>"
18
+ - - ">="
18
19
  - !ruby/object:Gem::Version
19
- version: 3.10.0
20
+ version: '0'
20
21
  type: :development
21
22
  prerelease: false
22
23
  version_requirements: !ruby/object:Gem::Requirement
23
24
  requirements:
24
- - - "~>"
25
+ - - ">="
25
26
  - !ruby/object:Gem::Version
26
- version: 3.10.0
27
+ version: '0'
27
28
  - !ruby/object:Gem::Dependency
28
29
  name: rake
29
30
  requirement: !ruby/object:Gem::Requirement
30
31
  requirements:
31
- - - "~>"
32
+ - - ">="
32
33
  - !ruby/object:Gem::Version
33
- version: 12.3.3
34
+ version: '0'
34
35
  type: :development
35
36
  prerelease: false
36
37
  version_requirements: !ruby/object:Gem::Requirement
37
38
  requirements:
38
- - - "~>"
39
+ - - ">="
39
40
  - !ruby/object:Gem::Version
40
- version: 12.3.3
41
+ version: '0'
41
42
  - !ruby/object:Gem::Dependency
42
43
  name: simplecov
43
44
  requirement: !ruby/object:Gem::Requirement
44
45
  requirements:
45
- - - "~>"
46
+ - - ">="
46
47
  - !ruby/object:Gem::Version
47
- version: 0.9.1
48
+ version: '0'
48
49
  type: :development
49
50
  prerelease: false
50
51
  version_requirements: !ruby/object:Gem::Requirement
51
52
  requirements:
52
- - - "~>"
53
+ - - ">="
53
54
  - !ruby/object:Gem::Version
54
- version: 0.9.1
55
+ version: '0'
55
56
  - !ruby/object:Gem::Dependency
56
57
  name: standard
57
58
  requirement: !ruby/object:Gem::Requirement
58
59
  requirements:
59
- - - "~>"
60
+ - - ">="
60
61
  - !ruby/object:Gem::Version
61
- version: 1.0.0
62
+ version: '0'
62
63
  type: :development
63
64
  prerelease: false
64
65
  version_requirements: !ruby/object:Gem::Requirement
65
66
  requirements:
66
- - - "~>"
67
+ - - ">="
67
68
  - !ruby/object:Gem::Version
68
- version: 1.0.0
69
+ version: '0'
69
70
  description: Modify your ENV
70
71
  email:
71
72
  - joshua.clayton@gmail.com
73
+ - dorian@dorianmarie.fr
72
74
  executables: []
73
75
  extensions: []
74
76
  extra_rdoc_files: []
75
77
  files:
76
78
  - ".github/workflows/ci.yml"
77
79
  - ".gitignore"
80
+ - CHANGELOG.md
81
+ - CODEOWNERS
78
82
  - CODE_OF_CONDUCT.md
79
83
  - Gemfile
80
84
  - LICENSE
81
- - NEWS
82
85
  - README.md
83
86
  - Rakefile
84
87
  - climate_control.gemspec
85
88
  - lib/climate_control.rb
86
- - lib/climate_control/environment.rb
87
89
  - lib/climate_control/errors.rb
88
- - lib/climate_control/modifier.rb
89
90
  - lib/climate_control/version.rb
90
91
  - spec/acceptance/climate_control_spec.rb
92
+ - spec/acceptance/unsafe_modify_spec.rb
91
93
  - spec/spec_helper.rb
92
94
  homepage: https://github.com/thoughtbot/climate_control
93
95
  licenses:
@@ -108,10 +110,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
108
110
  - !ruby/object:Gem::Version
109
111
  version: '0'
110
112
  requirements: []
111
- rubygems_version: 3.2.3
113
+ rubygems_version: 3.3.13
112
114
  signing_key:
113
115
  specification_version: 4
114
116
  summary: Modify your ENV easily with ClimateControl
115
117
  test_files:
116
118
  - spec/acceptance/climate_control_spec.rb
119
+ - spec/acceptance/unsafe_modify_spec.rb
117
120
  - spec/spec_helper.rb
data/NEWS DELETED
@@ -1,22 +0,0 @@
1
- 1.0.1 (May 26, 2021)
2
- Require minimum Ruby version of 2.5.0
3
-
4
- 1.0.0 (March 6, 2021)
5
- Commit to supporting latest patch versions of Ruby 2.5+
6
- Improve documentation
7
- Format code with StandardRB
8
- Bump gem dependencies
9
-
10
- 0.2.0 (May 12, 2017)
11
- Allow nested environment changes in the same thread
12
-
13
- 0.1.0 (January 7, 2017)
14
- Remove ActiveSupport dependency
15
-
16
- 0.0.4 (January 6, 2017)
17
- Improved thread safety
18
- Handle TypeErrors during assignment
19
- Improve documentation
20
-
21
- 0.0.1 (November 28, 2012)
22
- Initial release
@@ -1,33 +0,0 @@
1
- require "forwardable"
2
-
3
- module ClimateControl
4
- class Environment
5
- extend Forwardable
6
-
7
- def initialize
8
- @semaphore = Mutex.new
9
- @owner = nil
10
- end
11
-
12
- def_delegators :env, :[]=, :to_hash, :[], :delete
13
-
14
- def synchronize
15
- if @owner == Thread.current
16
- return yield if block_given?
17
- end
18
-
19
- @semaphore.synchronize do
20
- @owner = Thread.current
21
- yield if block_given?
22
- ensure
23
- @owner = nil
24
- end
25
- end
26
-
27
- private
28
-
29
- def env
30
- ENV
31
- end
32
- end
33
- end
@@ -1,92 +0,0 @@
1
- module ClimateControl
2
- class Modifier
3
- def initialize(env, environment_overrides = {}, &block)
4
- @environment_overrides = stringify_keys(environment_overrides)
5
- @block = block
6
- @env = env
7
- end
8
-
9
- def process
10
- @env.synchronize do
11
- prepare_environment_for_block
12
- run_block
13
- ensure
14
- cache_environment_after_block
15
- delete_keys_that_do_not_belong
16
- revert_changed_keys
17
- end
18
- end
19
-
20
- private
21
-
22
- def prepare_environment_for_block
23
- @original_env = clone_environment
24
- copy_overrides_to_environment
25
- @env_with_overrides_before_block = clone_environment
26
- end
27
-
28
- def run_block
29
- @block.call
30
- end
31
-
32
- def copy_overrides_to_environment
33
- @environment_overrides.each do |key, value|
34
- @env[key] = value
35
- rescue TypeError => e
36
- raise UnassignableValueError,
37
- "attempted to assign #{value} to #{key} but failed (#{e.message})"
38
- end
39
- end
40
-
41
- def keys_to_remove
42
- @environment_overrides.keys
43
- end
44
-
45
- def keys_changed_by_block
46
- @keys_changed_by_block ||= OverlappingKeysWithChangedValues.new(@env_with_overrides_before_block, @env_after_block).keys
47
- end
48
-
49
- def cache_environment_after_block
50
- @env_after_block = clone_environment
51
- end
52
-
53
- def delete_keys_that_do_not_belong
54
- (keys_to_remove - keys_changed_by_block).each { |key| @env.delete(key) }
55
- end
56
-
57
- def revert_changed_keys
58
- (@original_env.keys - keys_changed_by_block).each do |key|
59
- @env[key] = @original_env[key]
60
- end
61
- end
62
-
63
- def clone_environment
64
- @env.to_hash
65
- end
66
-
67
- def stringify_keys(env)
68
- env.each_with_object({}) do |(key, value), hash|
69
- hash[key.to_s] = value
70
- end
71
- end
72
-
73
- class OverlappingKeysWithChangedValues
74
- def initialize(hash_1, hash_2)
75
- @hash_1 = hash_1 || {}
76
- @hash_2 = hash_2
77
- end
78
-
79
- def keys
80
- overlapping_keys.select do |overlapping_key|
81
- @hash_1[overlapping_key] != @hash_2[overlapping_key]
82
- end
83
- end
84
-
85
- private
86
-
87
- def overlapping_keys
88
- @hash_2.keys & @hash_1.keys
89
- end
90
- end
91
- end
92
- end