cleanroom 1.0.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 +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +12 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +2 -0
- data/LICENSE +201 -0
- data/README.md +229 -0
- data/Rakefile +19 -0
- data/cleanroom.gemspec +37 -0
- data/lib/cleanroom.rb +188 -0
- data/lib/cleanroom/errors.rb +33 -0
- data/lib/cleanroom/rspec.rb +60 -0
- data/lib/cleanroom/version.rb +24 -0
- data/spec/functional/cleanroom_spec.rb +96 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/unit/cleanroom_spec.rb +265 -0
- data/spec/unit/rspec_spec.rb +77 -0
- metadata +113 -0
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'cleanroom'
|
3
|
+
|
4
|
+
RSpec.configure do |config|
|
5
|
+
config.filter_run(focus: true)
|
6
|
+
config.run_all_when_everything_filtered = true
|
7
|
+
|
8
|
+
# Force the expect syntax
|
9
|
+
config.expect_with :rspec do |c|
|
10
|
+
c.syntax = :expect
|
11
|
+
end
|
12
|
+
|
13
|
+
# Create and clear tmp_path on each run
|
14
|
+
config.before(:each) do
|
15
|
+
FileUtils.rm_rf(tmp_path)
|
16
|
+
FileUtils.mkdir_p(tmp_path)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Run specs in a random order
|
20
|
+
config.order = 'random'
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# The path on disk to the temporary directory.
|
25
|
+
#
|
26
|
+
# @param [String, Array<String>] paths
|
27
|
+
# the extra path parameters to join
|
28
|
+
#
|
29
|
+
# @return [String]
|
30
|
+
#
|
31
|
+
def tmp_path(*paths)
|
32
|
+
root = File.expand_path('../..', __FILE__)
|
33
|
+
File.join('tmp', *paths)
|
34
|
+
end
|
@@ -0,0 +1,265 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Cleanroom do
|
4
|
+
let(:klass) do
|
5
|
+
Class.new do
|
6
|
+
include Cleanroom
|
7
|
+
|
8
|
+
def exposed_method
|
9
|
+
@called = true
|
10
|
+
end
|
11
|
+
expose :exposed_method
|
12
|
+
|
13
|
+
def unexposed_method; end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:instance) { klass.new }
|
18
|
+
|
19
|
+
describe '.included' do
|
20
|
+
let(:klass) { Class.new { include Cleanroom } }
|
21
|
+
|
22
|
+
it 'extends the ClassMethods' do
|
23
|
+
expect(klass).to be_a(Cleanroom::ClassMethods)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'includes the InstanceMethods' do
|
27
|
+
expect(instance).to be_a(Cleanroom::InstanceMethods)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '.extended' do
|
32
|
+
let(:klass) { Class.new { extend Cleanroom } }
|
33
|
+
|
34
|
+
it 'extends the ClassMethods' do
|
35
|
+
expect(klass).to be_a(Cleanroom::ClassMethods)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'includes the InstanceMethods' do
|
39
|
+
expect(instance).to be_a(Cleanroom::InstanceMethods)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '.evaluate_file' do
|
44
|
+
let(:path) { '/path/to/file' }
|
45
|
+
let(:contents) { 'contents' }
|
46
|
+
|
47
|
+
before do
|
48
|
+
allow(File).to receive(:expand_path)
|
49
|
+
.with(path)
|
50
|
+
.and_return(path)
|
51
|
+
|
52
|
+
allow(IO).to receive(:read)
|
53
|
+
.with(path)
|
54
|
+
.and_return(contents)
|
55
|
+
|
56
|
+
allow(klass).to receive(:evaluate)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'gets the absolute path to the file' do
|
60
|
+
expect(File).to receive(:expand_path).with(path).once
|
61
|
+
klass.evaluate_file(instance, path)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'reads the contents to a string' do
|
65
|
+
expect(IO).to receive(:read).with(path).once
|
66
|
+
klass.evaluate_file(instance, path)
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'evaluates the contents' do
|
70
|
+
expect(klass).to receive(:evaluate).with(instance, contents, path, 1).once
|
71
|
+
klass.evaluate_file(instance, path)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe '.evaluate' do
|
76
|
+
let(:cleanroom) { double('Cleanroom.cleanroom') }
|
77
|
+
let(:cleanroom_instance) { double('Cleanroom.cleanroom_instance') }
|
78
|
+
|
79
|
+
let(:string) { '"hello"' }
|
80
|
+
|
81
|
+
before do
|
82
|
+
allow(cleanroom).to receive(:new)
|
83
|
+
.with(instance)
|
84
|
+
.and_return(cleanroom_instance)
|
85
|
+
|
86
|
+
allow(cleanroom_instance).to receive(:instance_eval)
|
87
|
+
|
88
|
+
allow(klass).to receive(:cleanroom)
|
89
|
+
.and_return(cleanroom)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'creates a new cleanroom object' do
|
93
|
+
expect(cleanroom).to receive(:new).with(instance).once
|
94
|
+
klass.evaluate(instance, string)
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'evaluates against the new cleanroom object' do
|
98
|
+
expect(cleanroom_instance).to receive(:instance_eval).with(string).once
|
99
|
+
klass.evaluate(instance, string)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe '.expose' do
|
104
|
+
let(:klass) do
|
105
|
+
Class.new do
|
106
|
+
include Cleanroom
|
107
|
+
|
108
|
+
def public_method; end
|
109
|
+
|
110
|
+
protected
|
111
|
+
def protected_method; end
|
112
|
+
|
113
|
+
private
|
114
|
+
def private_method; end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'exposes the method when it is public' do
|
119
|
+
expect { klass.expose(:public_method) }.to_not raise_error
|
120
|
+
expect(klass.exposed_methods).to include(:public_method)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'raises an exception if the method is not defined' do
|
124
|
+
expect { klass.expose(:no_method) }.to raise_error(NameError)
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'raises an exception if the method is protected' do
|
128
|
+
expect { klass.expose(:protected_method) }.to raise_error(NameError)
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'raises an exception if the method is private' do
|
132
|
+
expect { klass.expose(:private_method) }.to raise_error(NameError)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe '.exposed_methods' do
|
137
|
+
it 'returns a hash' do
|
138
|
+
expect(klass.exposed_methods).to be_a(Hash)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe '.cleanroom' do
|
143
|
+
let(:klass) do
|
144
|
+
Class.new do
|
145
|
+
include Cleanroom
|
146
|
+
|
147
|
+
def method_1
|
148
|
+
@method_1 = true
|
149
|
+
end
|
150
|
+
expose :method_1
|
151
|
+
|
152
|
+
def method_2
|
153
|
+
@method_2 = true
|
154
|
+
end
|
155
|
+
expose :method_2
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'creates a new anonymous class each time' do
|
160
|
+
a, b = klass.send(:cleanroom), klass.send(:cleanroom)
|
161
|
+
expect(a).to_not be(b)
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'creates a method for each exposed one on the proxy object' do
|
165
|
+
cleanroom = klass.send(:cleanroom)
|
166
|
+
|
167
|
+
expect(cleanroom).to be_public_method_defined(:method_1)
|
168
|
+
expect(cleanroom).to be_public_method_defined(:method_2)
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'calls the proxied method' do
|
172
|
+
cleanroom = klass.send(:cleanroom).new(instance)
|
173
|
+
cleanroom.method_1
|
174
|
+
cleanroom.method_2
|
175
|
+
|
176
|
+
expect(instance.instance_variable_get(:@method_1)).to be(true)
|
177
|
+
expect(instance.instance_variable_get(:@method_2)).to be(true)
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'prevents calls to the instance directly' do
|
181
|
+
cleanroom = klass.send(:cleanroom).new(instance)
|
182
|
+
expect {
|
183
|
+
cleanroom.__instance__
|
184
|
+
}.to raise_error(Cleanroom::InaccessibleError)
|
185
|
+
|
186
|
+
expect {
|
187
|
+
cleanroom.send(:__instance__)
|
188
|
+
}.to raise_error(Cleanroom::InaccessibleError)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe '#evaluate_file' do
|
193
|
+
let(:path) { '/path/to/file' }
|
194
|
+
|
195
|
+
before do
|
196
|
+
allow(klass).to receive(:evaluate_file)
|
197
|
+
.with(instance, path)
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'delegates to the class method' do
|
201
|
+
expect(klass).to receive(:evaluate_file).with(instance, path)
|
202
|
+
instance.evaluate_file(path)
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'returns self' do
|
206
|
+
expect(instance.evaluate_file(path)).to be(instance)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
describe '#evaluate' do
|
211
|
+
let(:string) { '"hello"' }
|
212
|
+
|
213
|
+
before do
|
214
|
+
allow(klass).to receive(:evaluate)
|
215
|
+
.with(instance, string)
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'delegates to the class method' do
|
219
|
+
expect(klass).to receive(:evaluate).with(instance, string)
|
220
|
+
instance.evaluate(string)
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'returns self' do
|
224
|
+
expect(instance.evaluate(string)).to be(instance)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
context 'when evaluating a DSL subclass' do
|
229
|
+
let(:parent) do
|
230
|
+
Class.new do
|
231
|
+
include Cleanroom
|
232
|
+
|
233
|
+
def parent_method; end
|
234
|
+
expose :parent_method
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
let(:child) do
|
239
|
+
Class.new(parent) do
|
240
|
+
def child_method; end
|
241
|
+
expose :child_method
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
let(:instance) { child.new }
|
246
|
+
|
247
|
+
it 'inherits the parent DSL methods' do
|
248
|
+
expect {
|
249
|
+
instance.evaluate("parent_method")
|
250
|
+
}.to_not raise_error
|
251
|
+
end
|
252
|
+
|
253
|
+
it 'allows for custom DSL methods' do
|
254
|
+
expect {
|
255
|
+
instance.evaluate("child_method")
|
256
|
+
}.to_not raise_error
|
257
|
+
end
|
258
|
+
|
259
|
+
it 'does not change the parent DSL' do
|
260
|
+
expect {
|
261
|
+
parent.new.evaluate("child_method")
|
262
|
+
}.to raise_error(NameError)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'cleanroom/rspec'
|
3
|
+
|
4
|
+
describe 'RSpec matchers' do
|
5
|
+
let(:klass) do
|
6
|
+
Class.new do
|
7
|
+
include Cleanroom
|
8
|
+
|
9
|
+
def method_1; end
|
10
|
+
expose :method_1
|
11
|
+
|
12
|
+
def method_2; end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:instance) { klass.new }
|
17
|
+
|
18
|
+
describe '#be_an_exposed_method_on' do
|
19
|
+
context 'when given a class' do
|
20
|
+
it 'is true when the method is exposed' do
|
21
|
+
expect(:method_1).to be_an_exposed_method_on(klass)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'is false when the method exists, but is not exposed' do
|
25
|
+
expect(:method_2).to_not be_an_exposed_method_on(klass)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'is false when the method is not exposed' do
|
29
|
+
expect(:method_3).to_not be_an_exposed_method_on(klass)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'when given an instance' do
|
34
|
+
it 'is true when the method is exposed' do
|
35
|
+
expect(:method_1).to be_an_exposed_method_on(instance)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'is false when the method exists, but is not exposed' do
|
39
|
+
expect(:method_2).to_not be_an_exposed_method_on(instance)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'is false when the method is not exposed' do
|
43
|
+
expect(:method_3).to_not be_an_exposed_method_on(instance)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#have_exposed_method' do
|
49
|
+
context 'when given a class' do
|
50
|
+
it 'is true when the method is exposed' do
|
51
|
+
expect(klass).to have_exposed_method(:method_1)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'is false when the method exists, but is not exposed' do
|
55
|
+
expect(klass).to_not have_exposed_method(:method_2)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'is false when the method is not exposed' do
|
59
|
+
expect(klass).to_not have_exposed_method(:method_3)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'when given an instance' do
|
64
|
+
it 'is true when the method is exposed' do
|
65
|
+
expect(instance).to have_exposed_method(:method_1)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'is false when the method exists, but is not exposed' do
|
69
|
+
expect(instance).to_not have_exposed_method(:method_2)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'is false when the method is not exposed' do
|
73
|
+
expect(instance).to_not have_exposed_method(:method_3)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cleanroom
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Seth Vargo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-08-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Ruby is an excellent programming language for creating and managing custom
|
56
|
+
DSLs, but how can you securely evaluate a DSL while explicitly controlling the methods
|
57
|
+
exposed to the user? Our good friends instance_eval and instance_exec are great,
|
58
|
+
but they expose all methods - public, protected, and private - to the user. Even
|
59
|
+
worse, they expose the ability to accidentally or intentionally alter the behavior
|
60
|
+
of the system! The cleanroom pattern is a safer, more convenient, Ruby-like approach
|
61
|
+
for limiting the information exposed by a DSL while giving users the ability to
|
62
|
+
write awesome code!
|
63
|
+
email: sethvargo@gmail.com
|
64
|
+
executables: []
|
65
|
+
extensions: []
|
66
|
+
extra_rdoc_files: []
|
67
|
+
files:
|
68
|
+
- ".gitignore"
|
69
|
+
- ".travis.yml"
|
70
|
+
- CHANGELOG.md
|
71
|
+
- Gemfile
|
72
|
+
- LICENSE
|
73
|
+
- README.md
|
74
|
+
- Rakefile
|
75
|
+
- cleanroom.gemspec
|
76
|
+
- lib/cleanroom.rb
|
77
|
+
- lib/cleanroom/errors.rb
|
78
|
+
- lib/cleanroom/rspec.rb
|
79
|
+
- lib/cleanroom/version.rb
|
80
|
+
- spec/functional/cleanroom_spec.rb
|
81
|
+
- spec/spec_helper.rb
|
82
|
+
- spec/unit/cleanroom_spec.rb
|
83
|
+
- spec/unit/rspec_spec.rb
|
84
|
+
homepage: https://github.com/sethvargo/cleanroom
|
85
|
+
licenses:
|
86
|
+
- Apache 2.0
|
87
|
+
metadata: {}
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.9.3
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project:
|
104
|
+
rubygems_version: 2.3.0
|
105
|
+
signing_key:
|
106
|
+
specification_version: 4
|
107
|
+
summary: "(More) safely evaluate Ruby DSLs with cleanroom"
|
108
|
+
test_files:
|
109
|
+
- spec/functional/cleanroom_spec.rb
|
110
|
+
- spec/spec_helper.rb
|
111
|
+
- spec/unit/cleanroom_spec.rb
|
112
|
+
- spec/unit/rspec_spec.rb
|
113
|
+
has_rdoc:
|