cleanroom-next 1.0.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 +7 -0
- data/lib/cleanroom/errors.rb +38 -0
- data/lib/cleanroom/rspec.rb +57 -0
- data/lib/cleanroom/version.rb +24 -0
- data/lib/cleanroom.rb +236 -0
- metadata +54 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 05b7bbf84ca0dda05ebb3e5f4bff790af5e69b7f6f7081dd24549c759a84cf71
|
|
4
|
+
data.tar.gz: e4347462a89a0a8f1c11ce338da4f8dfab9ff65fbea6900c09c3678b8b332cf1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5fa0e7784212b46b7367f725e7113e91f68160721a961ddf131e08d3b5cd8db3944f6749b81d6ef1933a4a282b0be5c1bd840ac8f9c310d67ed4d4c871aae5c1
|
|
7
|
+
data.tar.gz: 1853e7e1fb508cba0a5d3270e7264ad0d192639f0e3c180edee211c786a24d42367fc596ebd96a7525ab6b25af5bc34926347d3d9916349a560e8bc447d8c133
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2014 Seth Vargo <sethvargo@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
module Cleanroom
|
|
18
|
+
# Base class for errors raised by Cleanroom
|
|
19
|
+
class Error < StandardError; end
|
|
20
|
+
|
|
21
|
+
# Error raised when trying to access a non accessible variable or method
|
|
22
|
+
class InaccessibleError < Error
|
|
23
|
+
def initialize(name, instance)
|
|
24
|
+
super()
|
|
25
|
+
@name = name
|
|
26
|
+
@instance = instance
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [String] The error message
|
|
30
|
+
def to_s
|
|
31
|
+
<<~ERROR_MESSAGE.gsub(/\r?\n/, ' ')
|
|
32
|
+
Undefined local variable or method `#{@name}' for #{@instance}. It may have
|
|
33
|
+
been removed for the purposes of evaluating the DSL or for added security. If
|
|
34
|
+
you feel you have reached this message in error, please open an issue.
|
|
35
|
+
ERROR_MESSAGE
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2014 Seth Vargo <sethvargo@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
require_relative '../cleanroom'
|
|
18
|
+
|
|
19
|
+
require 'rspec' unless defined?(RSpec)
|
|
20
|
+
|
|
21
|
+
#
|
|
22
|
+
# Assert a given method is exposed on a class.
|
|
23
|
+
#
|
|
24
|
+
# @example Checking against an instance
|
|
25
|
+
# expect(:method1).to be_an_exposed_method_on(instance)
|
|
26
|
+
#
|
|
27
|
+
# @example Checking against a class
|
|
28
|
+
# expect(:method1).to be_an_exposed_method_on(klass)
|
|
29
|
+
#
|
|
30
|
+
RSpec::Matchers.define :be_an_exposed_method_on do |object|
|
|
31
|
+
match do |name|
|
|
32
|
+
if object.is_a?(Class)
|
|
33
|
+
object.exposed_methods.key?(name.to_sym)
|
|
34
|
+
else
|
|
35
|
+
object.class.exposed_methods.key?(name.to_sym)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
#
|
|
41
|
+
# Assert a given class or instance has an exposed method.
|
|
42
|
+
#
|
|
43
|
+
# @example Checking against an instance
|
|
44
|
+
# expect(instance).to have_exposed_method(:method1)
|
|
45
|
+
#
|
|
46
|
+
# @example Checking against a class
|
|
47
|
+
# expect(klass).to have_exposed_method(:method1)
|
|
48
|
+
#
|
|
49
|
+
RSpec::Matchers.define :have_exposed_method do |name|
|
|
50
|
+
match do |object|
|
|
51
|
+
if object.is_a?(Class)
|
|
52
|
+
object.exposed_methods.key?(name.to_sym)
|
|
53
|
+
else
|
|
54
|
+
object.class.exposed_methods.key?(name.to_sym)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2014 Seth Vargo <sethvargo@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
module Cleanroom
|
|
18
|
+
#
|
|
19
|
+
# The version of the Cleanroom gem.
|
|
20
|
+
#
|
|
21
|
+
# @return [String]
|
|
22
|
+
#
|
|
23
|
+
VERSION = '1.0.1'
|
|
24
|
+
end
|
data/lib/cleanroom.rb
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2014 Seth Vargo <sethvargo@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
require_relative 'cleanroom/errors'
|
|
18
|
+
require_relative 'cleanroom/version'
|
|
19
|
+
|
|
20
|
+
# The cleanroom pattern is a safer, more convenient, Ruby-like approach for
|
|
21
|
+
# limiting the information exposed by a DSL while giving users the ability to
|
|
22
|
+
# write awesome code!
|
|
23
|
+
module Cleanroom
|
|
24
|
+
#
|
|
25
|
+
# Callback for when this module is included.
|
|
26
|
+
#
|
|
27
|
+
# @param [Class] base
|
|
28
|
+
#
|
|
29
|
+
def self.included(base)
|
|
30
|
+
base.send(:extend, ClassMethods)
|
|
31
|
+
base.send(:include, InstanceMethods)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
#
|
|
35
|
+
# Callback for when this module is included.
|
|
36
|
+
#
|
|
37
|
+
# @param [Class] base
|
|
38
|
+
#
|
|
39
|
+
def self.extended(base)
|
|
40
|
+
base.send(:extend, ClassMethods)
|
|
41
|
+
base.send(:include, InstanceMethods)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#
|
|
45
|
+
# Class methods
|
|
46
|
+
#
|
|
47
|
+
module ClassMethods
|
|
48
|
+
#
|
|
49
|
+
# Evaluate the file in the context of the cleanroom.
|
|
50
|
+
#
|
|
51
|
+
# @param [Class] instance
|
|
52
|
+
# the instance of the class to evaluate against
|
|
53
|
+
# @param [String] filepath
|
|
54
|
+
# the path of the file to evaluate
|
|
55
|
+
#
|
|
56
|
+
def evaluate_file(instance, filepath)
|
|
57
|
+
absolute_path = File.expand_path(filepath)
|
|
58
|
+
file_contents = File.read(absolute_path)
|
|
59
|
+
evaluate(instance, file_contents, absolute_path, 1)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#
|
|
63
|
+
# Evaluate the string or block in the context of the cleanroom.
|
|
64
|
+
#
|
|
65
|
+
# @param [Class] instance
|
|
66
|
+
# the instance of the class to evaluate against
|
|
67
|
+
def evaluate(instance, ...)
|
|
68
|
+
cleanroom.new(instance).instance_eval(...)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#
|
|
72
|
+
# Expose the given method to the DSL.
|
|
73
|
+
#
|
|
74
|
+
# @param [Symbol] name
|
|
75
|
+
#
|
|
76
|
+
def expose(name)
|
|
77
|
+
raise NameError, "undefined method `#{name}' for class `#{self.name}'" unless public_method_defined?(name)
|
|
78
|
+
|
|
79
|
+
exposed_methods_with_kwargs[name] = true if instance_method(name).parameters.any? { |(arg_type, _arg_name)| KWARGS_TYPES.include?(arg_type) }
|
|
80
|
+
exposed_methods[name] = true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
#
|
|
84
|
+
# The list of exposed methods.
|
|
85
|
+
#
|
|
86
|
+
# @return [Hash]
|
|
87
|
+
#
|
|
88
|
+
def exposed_methods
|
|
89
|
+
@exposed_methods ||= from_superclass(:exposed_methods, {}).dup
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# Define the types of argument types that point kwargs arguments.
|
|
95
|
+
# Useful to treat them differently as when defining a method with kwargs, Ruby will pass parameters having a to_hash method differently to such methods:
|
|
96
|
+
#
|
|
97
|
+
# See this example illustrating the difference in treatment with and without kwargs in the method definition:
|
|
98
|
+
# def without_kwargs(*args)
|
|
99
|
+
# p args
|
|
100
|
+
# end
|
|
101
|
+
# def with_kwargs(*args, **kwargs)
|
|
102
|
+
# p args
|
|
103
|
+
# p kwargs
|
|
104
|
+
# end
|
|
105
|
+
# s_without_to_hash = 'Without to_hash'
|
|
106
|
+
# s_with_to_hash = 'With to_hash'
|
|
107
|
+
# s_with_to_hash.define_singleton_method(:to_hash) { { string: self.to_s } }
|
|
108
|
+
# without_kwargs(s_without_to_hash)
|
|
109
|
+
# ["Without to_hash"]
|
|
110
|
+
# without_kwargs(s_with_to_hash)
|
|
111
|
+
# ["With to_hash"]
|
|
112
|
+
# with_kwargs(s_without_to_hash)
|
|
113
|
+
# ["Without to_hash"]
|
|
114
|
+
# {}
|
|
115
|
+
# with_kwargs(s_with_to_hash)
|
|
116
|
+
# []
|
|
117
|
+
# {:string=>"With to_hash"}
|
|
118
|
+
KWARGS_TYPES = %i[key keyreq]
|
|
119
|
+
|
|
120
|
+
#
|
|
121
|
+
# The list of exposed methods with kwargs.
|
|
122
|
+
#
|
|
123
|
+
# @return [Hash]
|
|
124
|
+
#
|
|
125
|
+
def exposed_methods_with_kwargs
|
|
126
|
+
@exposed_methods_with_kwargs ||= from_superclass(:exposed_methods_with_kwargs, {}).dup
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
#
|
|
130
|
+
# The cleanroom instance for this class. This method is intentionally
|
|
131
|
+
# NOT cached!
|
|
132
|
+
#
|
|
133
|
+
# @return [Class]
|
|
134
|
+
#
|
|
135
|
+
def cleanroom
|
|
136
|
+
exposed = exposed_methods.keys
|
|
137
|
+
exposed_with_kwargs = exposed_methods_with_kwargs.keys
|
|
138
|
+
parent = name || 'Anonymous'
|
|
139
|
+
|
|
140
|
+
Class.new(Object) do
|
|
141
|
+
class << self
|
|
142
|
+
def class_eval
|
|
143
|
+
raise Cleanroom::InaccessibleError.new(:class_eval, self)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def instance_eval
|
|
147
|
+
raise Cleanroom::InaccessibleError.new(:instance_eval, self)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
define_method(:initialize) do |instance|
|
|
152
|
+
define_singleton_method(:__instance__) do
|
|
153
|
+
# Since Ruby 2.6, the first frame of `caller` for code evaluated via
|
|
154
|
+
# `instance_eval`/`eval` is labeled "(eval at <file>:<line>)", where
|
|
155
|
+
# the location is the very call site of `instance_eval` - here
|
|
156
|
+
# cleanroom.rb itself. That made the previous
|
|
157
|
+
# `caller[0].include?(__FILE__)` check wrongly grant DSL code access
|
|
158
|
+
# to the wrapped instance. Eval frames must be excluded: only real
|
|
159
|
+
# frames inside this file may call __instance__.
|
|
160
|
+
calling_frame = caller_locations(1, 1).first
|
|
161
|
+
unless calling_frame && !calling_frame.path.start_with?('(') && calling_frame.path.include?(__FILE__)
|
|
162
|
+
raise Cleanroom::InaccessibleError.new(:__instance__, self)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
instance
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
(exposed - exposed_with_kwargs).each do |exposed_method|
|
|
170
|
+
define_method(exposed_method) do |*args, &block|
|
|
171
|
+
__instance__.public_send(exposed_method, *args, &block)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
exposed_with_kwargs.each do |exposed_method|
|
|
176
|
+
define_method(exposed_method) do |*args, **kwargs, &block|
|
|
177
|
+
__instance__.public_send(exposed_method, *args, **kwargs, &block)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
define_method(:class_eval) do
|
|
182
|
+
raise Cleanroom::InaccessibleError.new(:class_eval, self)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
define_method(:inspect) do
|
|
186
|
+
"#<#{parent} (Cleanroom)>"
|
|
187
|
+
end
|
|
188
|
+
alias_method :to_s, :inspect
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
#
|
|
193
|
+
# Get the value from the superclass, if it responds, otherwise return
|
|
194
|
+
# +default+. Since class instance variables are **not** inherited upon
|
|
195
|
+
# subclassing, this is a required check to ensure subclasses inherit
|
|
196
|
+
# exposed DSL methods.
|
|
197
|
+
#
|
|
198
|
+
# @param [Symbol] method_name
|
|
199
|
+
# the name of the method to find
|
|
200
|
+
# @param [Object] default
|
|
201
|
+
# the default value to return if not found
|
|
202
|
+
#
|
|
203
|
+
def from_superclass(method_name, default = nil)
|
|
204
|
+
return default if superclass == Cleanroom
|
|
205
|
+
|
|
206
|
+
superclass.respond_to?(method_name) ? superclass.send(method_name) : default
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
#
|
|
211
|
+
# Instance Methods
|
|
212
|
+
#
|
|
213
|
+
module InstanceMethods
|
|
214
|
+
#
|
|
215
|
+
# Evaluate the file against the current instance.
|
|
216
|
+
#
|
|
217
|
+
# @param (see Cleanroom.evaluate_file)
|
|
218
|
+
# @return [self]
|
|
219
|
+
#
|
|
220
|
+
def evaluate_file(filepath)
|
|
221
|
+
self.class.evaluate_file(self, filepath)
|
|
222
|
+
self
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
#
|
|
226
|
+
# Evaluate the contents against the current instance.
|
|
227
|
+
#
|
|
228
|
+
# @param (see Cleanroom.evaluate_file)
|
|
229
|
+
# @return [self]
|
|
230
|
+
#
|
|
231
|
+
def evaluate(...)
|
|
232
|
+
self.class.evaluate(self, ...)
|
|
233
|
+
self
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cleanroom-next
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Seth Vargo
|
|
8
|
+
- Muriel Salvan
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Ruby is an excellent programming language for creating and managing custom
|
|
14
|
+
DSLs, but how can you securely evaluate a DSL while explicitly controlling the methods
|
|
15
|
+
exposed to the user? Our good friends instance_eval and instance_exec are great,
|
|
16
|
+
but they expose all methods - public, protected, and private - to the user. Even
|
|
17
|
+
worse, they expose the ability to accidentally or intentionally alter the behavior
|
|
18
|
+
of the system! The cleanroom pattern is a safer, more convenient, Ruby-like approach
|
|
19
|
+
for limiting the information exposed by a DSL while giving users the ability to
|
|
20
|
+
write awesome code!
|
|
21
|
+
email:
|
|
22
|
+
- sethvargo@gmail.com
|
|
23
|
+
- muriel@x-aeon.com
|
|
24
|
+
executables: []
|
|
25
|
+
extensions: []
|
|
26
|
+
extra_rdoc_files: []
|
|
27
|
+
files:
|
|
28
|
+
- lib/cleanroom.rb
|
|
29
|
+
- lib/cleanroom/errors.rb
|
|
30
|
+
- lib/cleanroom/rspec.rb
|
|
31
|
+
- lib/cleanroom/version.rb
|
|
32
|
+
homepage: https://github.com/Muriel-Salvan/cleanroom
|
|
33
|
+
licenses:
|
|
34
|
+
- Apache-2.0
|
|
35
|
+
metadata:
|
|
36
|
+
rubygems_mfa_required: 'true'
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
require_paths:
|
|
39
|
+
- lib
|
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: '3.0'
|
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0'
|
|
50
|
+
requirements: []
|
|
51
|
+
rubygems_version: 3.6.9
|
|
52
|
+
specification_version: 4
|
|
53
|
+
summary: "(More) safely evaluate Ruby DSLs with cleanroom"
|
|
54
|
+
test_files: []
|