must_be 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.
- data/.gitignore +2 -0
- data/README.md +348 -0
- data/Rakefile +27 -0
- data/VERSION +1 -0
- data/doc/readme/examples.rb +262 -0
- data/doc/readme/run_examples.rb +47 -0
- data/lib/must_be/attr_typed.rb +78 -0
- data/lib/must_be/basic.rb +120 -0
- data/lib/must_be/containers.rb +291 -0
- data/lib/must_be/containers_registered_classes.rb +83 -0
- data/lib/must_be/core.rb +247 -0
- data/lib/must_be/nonstandard_control_flow.rb +159 -0
- data/lib/must_be/proxy.rb +62 -0
- data/lib/must_be.rb +9 -0
- data/must_be.gemspec +71 -0
- data/spec/must_be/attr_typed_spec.rb +225 -0
- data/spec/must_be/basic_spec.rb +578 -0
- data/spec/must_be/containers_spec.rb +952 -0
- data/spec/must_be/core_spec.rb +675 -0
- data/spec/must_be/nonstandard_control_flow_spec.rb +845 -0
- data/spec/must_be/proxy_spec.rb +194 -0
- data/spec/notify_matcher_spec.rb +59 -0
- data/spec/spec_helper.rb +180 -0
- data/spec/typical_usage_spec.rb +176 -0
- metadata +98 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
class Module
|
2
|
+
def attr_typed(symbol, *types, &test)
|
3
|
+
raise TypeError, "#{symbol} is not a symbol" if symbol.is_a? Fixnum
|
4
|
+
|
5
|
+
types.each do |type|
|
6
|
+
raise TypeError, "class or module required" unless type.is_a? Module
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader symbol
|
10
|
+
name = symbol.to_sym.id2name
|
11
|
+
check_method_name = "attr_typed__check_#{name}"
|
12
|
+
|
13
|
+
unless types.empty?
|
14
|
+
types_message = types.size == 1 ? types[0] :
|
15
|
+
types.size == 2 ? "#{types[0]} or #{types[1]}" :
|
16
|
+
"one of #{types.inspect}"
|
17
|
+
|
18
|
+
type_check = lambda do |value|
|
19
|
+
if types.none?{|type| value.is_a? type }
|
20
|
+
must_notify("attribute `#{name}' must be a #{types_message},"\
|
21
|
+
" but value #{value.inspect} is a #{value.class}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if test
|
27
|
+
test_check = lambda do |value|
|
28
|
+
unless test[value]
|
29
|
+
must_notify("attribute `#{name}' cannot be #{value.inspect}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
define_method(check_method_name, &(
|
35
|
+
if types.empty?
|
36
|
+
if test
|
37
|
+
test_check
|
38
|
+
else
|
39
|
+
lambda do |value|
|
40
|
+
if value.nil?
|
41
|
+
must_notify("attribute `#{name}' cannot be nil")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
else
|
46
|
+
if test
|
47
|
+
lambda do |value|
|
48
|
+
type_check[value]
|
49
|
+
test_check[value]
|
50
|
+
end
|
51
|
+
else
|
52
|
+
type_check
|
53
|
+
end
|
54
|
+
end
|
55
|
+
))
|
56
|
+
|
57
|
+
module_eval %Q{
|
58
|
+
def #{name}=(value)
|
59
|
+
#{check_method_name}(value)
|
60
|
+
@#{name} = value
|
61
|
+
end
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
MustBe.register_disabled_handler do |enabled|
|
66
|
+
if enabled
|
67
|
+
if method(:attr_typed__original)
|
68
|
+
alias attr_typed attr_typed__original
|
69
|
+
remove_method(:attr_typed__original)
|
70
|
+
end
|
71
|
+
else
|
72
|
+
alias attr_typed__original attr_typed
|
73
|
+
define_method(:attr_typed) do |symbol, *types|
|
74
|
+
attr_accessor symbol
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module MustBe
|
2
|
+
def self.match_any_case?(v, cases)
|
3
|
+
cases = [cases] unless cases.is_a? Array
|
4
|
+
cases.any? {|c| c === v }
|
5
|
+
end
|
6
|
+
|
7
|
+
def must_be(*cases)
|
8
|
+
unless cases.empty? ? self : MustBe.match_any_case?(self, cases)
|
9
|
+
must_notify(self, __method__, cases, nil, ", but matches #{self.class}")
|
10
|
+
end
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def must_not_be(*cases)
|
15
|
+
if cases.empty? ? self : MustBe.match_any_case?(self, cases)
|
16
|
+
must_notify(self, __method__, cases, nil, ", but matches #{self.class}")
|
17
|
+
end
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def must_be_a__body(modules, test_method, method)
|
24
|
+
if modules.size.zero?
|
25
|
+
raise ArgumentError, "wrong number of arguments (0 for 1)"
|
26
|
+
end
|
27
|
+
ms = modules.last.is_a?(Module) ? modules : modules[0..-2]
|
28
|
+
if ms.size.zero?
|
29
|
+
raise TypeError, "class or module required"
|
30
|
+
end
|
31
|
+
ms.each do |mod|
|
32
|
+
raise TypeError, "class or module required" unless mod.is_a? Module
|
33
|
+
end
|
34
|
+
|
35
|
+
if ms.send(test_method) {|mod| is_a? mod }
|
36
|
+
must_notify(self, method, modules, nil, ", but is a #{self.class}")
|
37
|
+
end
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
public
|
42
|
+
|
43
|
+
def must_be_a(*modules)
|
44
|
+
must_be_a__body(modules, :none?, __method__)
|
45
|
+
end
|
46
|
+
|
47
|
+
def must_not_be_a(*modules)
|
48
|
+
must_be_a__body(modules, :any?, __method__)
|
49
|
+
end
|
50
|
+
|
51
|
+
def must_be_in(*collection)
|
52
|
+
cs = collection.size == 1 ? collection[0] : collection
|
53
|
+
unless cs.include? self
|
54
|
+
must_notify(self, __method__, collection)
|
55
|
+
end
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
def must_not_be_in(*collection)
|
60
|
+
cs = collection.size == 1 ? collection[0] : collection
|
61
|
+
if cs.include? self
|
62
|
+
must_notify(self, __method__, collection)
|
63
|
+
end
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def must_be_nil
|
68
|
+
must_notify(self, __method__) unless nil?
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
def must_not_be_nil
|
73
|
+
must_notify(self, __method__) if nil?
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
def must_be_true
|
78
|
+
must_notify(self, __method__) unless self == true
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
def must_be_false
|
83
|
+
must_notify(self, __method__) unless self == false
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
def must_be_boolean
|
88
|
+
unless self == true or self == false
|
89
|
+
must_notify(self, __method__)
|
90
|
+
end
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def must_be_close(expected, delta = 0.1)
|
95
|
+
difference = (self - expected).abs
|
96
|
+
unless difference < delta
|
97
|
+
must_notify(self, __method__, [expected, delta], nil,
|
98
|
+
", difference is #{difference}")
|
99
|
+
end
|
100
|
+
self
|
101
|
+
end
|
102
|
+
|
103
|
+
def must_not_be_close(expected, delta = 0.1)
|
104
|
+
if (self - expected).abs < delta
|
105
|
+
must_notify(self, __method__, [expected, delta])
|
106
|
+
end
|
107
|
+
self
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
### Proc Case Equality Patch ###
|
112
|
+
#
|
113
|
+
# Semantics of case equality `===' for Proc changed between Ruby 1.8 (useless)
|
114
|
+
# and Ruby 1.9 (awesome). So let's fix 'er up.
|
115
|
+
#
|
116
|
+
if RUBY_VERSION < "1.9"
|
117
|
+
class Proc
|
118
|
+
alias === call
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,291 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module MustBe
|
4
|
+
class ContainerNote < Note
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_accessor :original_note, :container
|
8
|
+
|
9
|
+
def_delegators :@original_note,
|
10
|
+
:receiver, :receiver=,
|
11
|
+
:assertion, :assertion=,
|
12
|
+
:args, :args=,
|
13
|
+
:block, :block=,
|
14
|
+
:additional_message, :additional_message=,
|
15
|
+
:prefix, :prefix=
|
16
|
+
|
17
|
+
def initialize(original_note, container = nil)
|
18
|
+
@original_note = original_note
|
19
|
+
@container = container
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
if assertion
|
24
|
+
@original_note.to_s+" in container #{MustBe.short_inspect(container)}"
|
25
|
+
else
|
26
|
+
@original_note.to_s
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
alias regular_backtrace backtrace
|
31
|
+
|
32
|
+
def backtrace
|
33
|
+
return unless regular_backtrace
|
34
|
+
|
35
|
+
if container.respond_to?(:must_only_ever_contain_backtrace) and
|
36
|
+
container.must_only_ever_contain_backtrace
|
37
|
+
regular_backtrace+["=== caused by container ==="]+
|
38
|
+
container.must_only_ever_contain_backtrace
|
39
|
+
else
|
40
|
+
regular_backtrace
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class PairNote < ContainerNote
|
46
|
+
attr_accessor :key, :value, :cases, :negate
|
47
|
+
|
48
|
+
def initialize(key, value, cases, container, negate)
|
49
|
+
super(Note.new(""), container)
|
50
|
+
@key = key
|
51
|
+
@value = value
|
52
|
+
@cases = cases
|
53
|
+
@negate = negate
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
match = negate ? "matches" : "does not match"
|
58
|
+
"#{prefix}pair {#{MustBe.short_inspect(key)}=>"\
|
59
|
+
"#{MustBe.short_inspect(value)}} #{match}"\
|
60
|
+
" #{MustBe.short_inspect(cases)} in"\
|
61
|
+
" container #{MustBe.short_inspect(container)}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.check_pair_against_hash_cases(key, value, cases, negate = false)
|
66
|
+
if negate
|
67
|
+
if cases.empty?
|
68
|
+
!key and !value
|
69
|
+
else
|
70
|
+
cases.all? do |c|
|
71
|
+
c.all? do |k, v|
|
72
|
+
not (match_any_case?(key, k) and match_any_case?(value, v))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
else
|
77
|
+
if cases.empty?
|
78
|
+
key and value
|
79
|
+
else
|
80
|
+
cases.any? do |c|
|
81
|
+
c.any? do |k, v|
|
82
|
+
match_any_case?(key, k) and match_any_case?(value, v)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.must_check_member_against_cases(container, member, cases,
|
90
|
+
negate = false)
|
91
|
+
member.must_check(lambda do
|
92
|
+
if negate
|
93
|
+
member.must_not_be(*cases)
|
94
|
+
else
|
95
|
+
member.must_be(*cases)
|
96
|
+
end
|
97
|
+
end) do |note|
|
98
|
+
note = ContainerNote.new(note, container)
|
99
|
+
block_given? ? yield(note) : note
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.must_check_pair_against_hash_cases(container, key, value, cases,
|
104
|
+
negate = false)
|
105
|
+
unless MustBe.check_pair_against_hash_cases(key, value, cases, negate)
|
106
|
+
note = PairNote.new(key, value, cases, container, negate)
|
107
|
+
must_notify(block_given? ? yield(note) : note)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.must_only_contain(container, cases, negate = false)
|
112
|
+
prefix = negate ? "must_not_contain: " : "must_only_contain: "
|
113
|
+
|
114
|
+
advice = MustOnlyEverContain.registered_class(container)
|
115
|
+
if advice and advice.respond_to? :must_only_contain_check
|
116
|
+
advice.must_only_contain_check(container, cases, negate)
|
117
|
+
elsif container.respond_to? :each_pair
|
118
|
+
container.each_pair do |key, value|
|
119
|
+
MustBe.must_check_pair_against_hash_cases(container, key, value,
|
120
|
+
cases, negate) do |note|
|
121
|
+
note.prefix = prefix
|
122
|
+
note
|
123
|
+
end
|
124
|
+
end
|
125
|
+
else
|
126
|
+
container.each do |member|
|
127
|
+
MustBe.must_check_member_against_cases(container, member, cases,
|
128
|
+
negate) do |note|
|
129
|
+
note.prefix = prefix
|
130
|
+
note
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
container
|
135
|
+
end
|
136
|
+
|
137
|
+
def must_only_contain(*cases)
|
138
|
+
MustBe.must_only_contain(self, cases)
|
139
|
+
end
|
140
|
+
|
141
|
+
def must_not_contain(*cases)
|
142
|
+
MustBe.must_only_contain(self, cases, true)
|
143
|
+
end
|
144
|
+
|
145
|
+
module MustOnlyEverContain
|
146
|
+
REGISTERED_CLASSES = {}
|
147
|
+
|
148
|
+
module Base
|
149
|
+
attr_accessor :must_only_ever_contain_cases,
|
150
|
+
:must_only_ever_contain_backtrace, :must_only_ever_contain_negate
|
151
|
+
|
152
|
+
module ClassMethods
|
153
|
+
def must_check_contents_after(*methods)
|
154
|
+
methods.each do |method|
|
155
|
+
define_method(method) do |*args, &block|
|
156
|
+
begin
|
157
|
+
super(*args, &block)
|
158
|
+
ensure
|
159
|
+
must_check_contents
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.included(base)
|
167
|
+
base.extend(ClassMethods)
|
168
|
+
end
|
169
|
+
|
170
|
+
def must_only_ever_contain_prefix
|
171
|
+
must_only_ever_contain_negate ? "must_never_ever_contain: " :
|
172
|
+
"must_only_ever_contain: "
|
173
|
+
end
|
174
|
+
|
175
|
+
def must_only_ever_contain_cases=(cases)
|
176
|
+
cases = [cases] unless cases.is_a? Array
|
177
|
+
@must_only_ever_contain_cases = cases
|
178
|
+
|
179
|
+
must_check(lambda { must_check_contents }) do |note|
|
180
|
+
note.prefix = must_only_ever_contain_prefix
|
181
|
+
note
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
protected
|
186
|
+
|
187
|
+
def must_check_member(member)
|
188
|
+
MustBe.must_check_member_against_cases(self, member,
|
189
|
+
must_only_ever_contain_cases, must_only_ever_contain_negate)
|
190
|
+
end
|
191
|
+
|
192
|
+
def must_check_pair(key, value)
|
193
|
+
MustBe.must_check_pair_against_hash_cases(self, key, value,
|
194
|
+
must_only_ever_contain_cases, must_only_ever_contain_negate)
|
195
|
+
end
|
196
|
+
|
197
|
+
def must_check_contents(members = self)
|
198
|
+
MustBe.must_only_contain(members, must_only_ever_contain_cases,
|
199
|
+
must_only_ever_contain_negate)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
public
|
204
|
+
|
205
|
+
##
|
206
|
+
# Creates a module from `body' which includes MustOnlyEverContain::Base.
|
207
|
+
# The module will be mixed into an objects of type `klass' when
|
208
|
+
# `must_only_ever_contain' is called. The module should override methods
|
209
|
+
# of`klass' which modify the contents of the object.
|
210
|
+
#
|
211
|
+
# If the module has a class method
|
212
|
+
# `must_only_contain_check(object, cases, negate = false)',
|
213
|
+
# then this method is used by `MustBe.must_only_contain'
|
214
|
+
# to check the contents of `object' against `cases'.
|
215
|
+
# `must_only_contain_check' should call `MustBe#must_notify' for any
|
216
|
+
# contents which do not match `cases'. (Or if `negate' is true, then
|
217
|
+
# `MustBe#must_notify' should be called for any contents that do match
|
218
|
+
# `cases'.)
|
219
|
+
#
|
220
|
+
def self.register(klass, &body)
|
221
|
+
unless klass.is_a? Class
|
222
|
+
raise ArgumentError, "invalid value for Class: #{klass.inspect}"
|
223
|
+
end
|
224
|
+
if REGISTERED_CLASSES[klass]
|
225
|
+
raise ArgumentError, "handler for #{klass} previously provided"
|
226
|
+
end
|
227
|
+
|
228
|
+
REGISTERED_CLASSES[klass] = mod = Module.new
|
229
|
+
mod.send(:include, Base)
|
230
|
+
mod.class_eval &body
|
231
|
+
|
232
|
+
mutator_advice = Module.new
|
233
|
+
mod.instance_methods(false).each do |method_name|
|
234
|
+
mutator_advice.send(:define_method, method_name) do |*args, &block|
|
235
|
+
must_check(lambda { super(*args, &block) }) do |note|
|
236
|
+
note.prefix = nil
|
237
|
+
call_s = Note.new(self.class, method_name, args, block).message
|
238
|
+
call_s.sub!(".", "#")
|
239
|
+
note.prefix = "#{must_only_ever_contain_prefix}#{call_s}: "
|
240
|
+
note
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
mod.const_set(:MutatorAdvice, mutator_advice)
|
245
|
+
mod.instance_eval do
|
246
|
+
def extended(base)
|
247
|
+
base.extend(const_get(:MutatorAdvice))
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
mod
|
252
|
+
end
|
253
|
+
|
254
|
+
def self.registered_class(object)
|
255
|
+
REGISTERED_CLASSES[object.class]
|
256
|
+
end
|
257
|
+
|
258
|
+
def self.unregister(klass)
|
259
|
+
REGISTERED_CLASSES.delete(klass)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def self.must_only_ever_contain(container, cases, negate = false)
|
264
|
+
unless container.singleton_methods.empty?
|
265
|
+
method_name = "must_#{negate ? "never" : "only"}_ever_contain"
|
266
|
+
raise ArgumentError, "#{method_name} adds singleton methods but"\
|
267
|
+
" receiver #{MustBe.short_inspect(container)} already"\
|
268
|
+
" has singleton methods #{container.singleton_methods.inspect}"
|
269
|
+
end
|
270
|
+
|
271
|
+
advice = MustOnlyEverContain.registered_class(container)
|
272
|
+
if advice
|
273
|
+
container.extend advice
|
274
|
+
container.must_only_ever_contain_backtrace = caller
|
275
|
+
container.must_only_ever_contain_negate = negate
|
276
|
+
container.must_only_ever_contain_cases = cases
|
277
|
+
else
|
278
|
+
raise TypeError,
|
279
|
+
"No MustOnlyEverContain.registered_class for #{container.class}"
|
280
|
+
end
|
281
|
+
container
|
282
|
+
end
|
283
|
+
|
284
|
+
def must_only_ever_contain(*cases)
|
285
|
+
MustBe.must_only_ever_contain(self, cases)
|
286
|
+
end
|
287
|
+
|
288
|
+
def must_never_ever_contain(*cases)
|
289
|
+
MustBe.must_only_ever_contain(self, cases, true)
|
290
|
+
end
|
291
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module MustBe::MustOnlyEverContain
|
2
|
+
|
3
|
+
### Array ###
|
4
|
+
|
5
|
+
register Array do
|
6
|
+
must_check_contents_after :collect!, :map!, :flatten!
|
7
|
+
|
8
|
+
def <<(obj)
|
9
|
+
must_check_member(obj)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def []=(*args)
|
14
|
+
if args.size == 3 or args[0].is_a? Range
|
15
|
+
value = args.last
|
16
|
+
if value.nil?
|
17
|
+
# No check needed.
|
18
|
+
elsif value.is_a? Array
|
19
|
+
value.map {|v| must_check_member(v) }
|
20
|
+
else
|
21
|
+
must_check_member(value)
|
22
|
+
end
|
23
|
+
else
|
24
|
+
must_check_member(args[1])
|
25
|
+
end
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def concat(other_array)
|
30
|
+
must_check_contents(other_array)
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def fill(*args)
|
35
|
+
if block_given?
|
36
|
+
begin
|
37
|
+
super
|
38
|
+
ensure
|
39
|
+
must_check_contents
|
40
|
+
end
|
41
|
+
else
|
42
|
+
must_check_member(args[0])
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def insert(index, *objs)
|
48
|
+
must_check_contents(objs)
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
def push(*objs)
|
53
|
+
must_check_contents(objs)
|
54
|
+
super
|
55
|
+
end
|
56
|
+
|
57
|
+
def replace(other_array)
|
58
|
+
must_check_contents(other_array)
|
59
|
+
super
|
60
|
+
end
|
61
|
+
|
62
|
+
def unshift(*objs)
|
63
|
+
must_check_contents(objs)
|
64
|
+
super
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
### Hash ###
|
69
|
+
|
70
|
+
register Hash do
|
71
|
+
must_check_contents_after :replace, :merge!, :update
|
72
|
+
|
73
|
+
def []=(key, value)
|
74
|
+
must_check_pair(key, value)
|
75
|
+
super
|
76
|
+
end
|
77
|
+
|
78
|
+
def store(key, value)
|
79
|
+
must_check_pair(key, value)
|
80
|
+
super
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|