mal 0.0.2 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/lib/mal.rb +212 -7
- data/mal.gemspec +4 -3
- data/spec/mal_external_eval_spec.rb +10 -0
- data/spec/mal_spec.rb +291 -1
- data/spec/spec_helper.rb +3 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8322ccc3484573bae624022558dcd1c09f590617
|
4
|
+
data.tar.gz: 92eeb1370d980bc3a811fe5d2656ba82f78d53cd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2fbc206db73bb0abb489a1ce55c418a20a53ca702ac75e0d4a813a4ccd27ab73be0ddae8daafe14c517e34e80bb6a7570fdae7771340ac5d0f78bdf97ab36f39
|
7
|
+
data.tar.gz: 9e956f361b93855316c5a67cd1f0e322311e2eaf2755113e3050c03ecad3237410f106e1dee9cd21976dd99e171508d1bb2bdf08eebb29ba30e7810755e47b46
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# mal
|
2
2
|
|
3
|
-
Minuscule (Type)
|
3
|
+
Minuscule (Type) Algebra - a type-matching thing for Ruby.
|
4
4
|
|
5
5
|
The library allows you to do simple type matching based on _any_ Ruby types that support the "triqual"
|
6
6
|
operator (type equality). It is distantly related to libraries like rb_dry_types
|
@@ -18,7 +18,7 @@ Array only contains booleans:
|
|
18
18
|
|
19
19
|
You can also match Hashes, which lends itself to JSON assertions quite nicely:
|
20
20
|
|
21
|
-
Mal.
|
21
|
+
Mal.HashWith(name: String, age: Numeric) === {name: 'Jane', age: 22, accepted_license_agreement: true} #=> true
|
22
22
|
|
23
23
|
You can also use these type matchers for case statements:
|
24
24
|
|
data/lib/mal.rb
CHANGED
@@ -1,5 +1,49 @@
|
|
1
|
+
# The module allows you to define simple data structure schemas, and to match your data against
|
2
|
+
# those schemas. Primary use is for HTTP parameters, JSON-derived datastructures and the like.
|
3
|
+
#
|
4
|
+
# Let's start with the basics. Any "typespec" returned by the library responds to `===`. The most
|
5
|
+
# basic (and all-encompassing) typespec there is is called `Anything()`.
|
6
|
+
#
|
7
|
+
# Anything() === false #=> true
|
8
|
+
# Anything() === true #=> true
|
9
|
+
# Anything() === Module #=> true
|
10
|
+
#
|
11
|
+
# A more specific type is an Only(), which is similar to just using a class, module or Regexp (which
|
12
|
+
# all support ===), but it can be composed with other typespecs.
|
13
|
+
#
|
14
|
+
# Only(/hello/) === 123 #=> false
|
15
|
+
# Only(/hello/) === "hello world" #=> true
|
16
|
+
#
|
17
|
+
# Interesting things come into play when you use combinations of the typespecs. For example, you want
|
18
|
+
# to ensure the value referenced by `my_var` is either a Fixnum or a String matching a regular expression.
|
19
|
+
# For this, you need to create a compound matcher using an `Either()` typespec (disjoint union):
|
20
|
+
#
|
21
|
+
# Either(Fixnum, Both(String, /hello/)) === "hello world" #=> true
|
22
|
+
# Either(Fixnum, Both(String, /hello/)) === 123 #=> true
|
23
|
+
# Either(Fixnum, Both(String, /hello/)) === Module #=> false, since it is neither of
|
24
|
+
#
|
25
|
+
# You can also use the `|` operator on most of the typespecs to create these disjoint unions - but
|
26
|
+
# if you have a matchable object on the left side of the expression you mught have to wrap it in an
|
27
|
+
# `Only()`:
|
28
|
+
#
|
29
|
+
# Only(Fixnum) | Only(String) #=> Either(Fixnum, String)
|
30
|
+
#
|
31
|
+
# Even more entertainment becomes possible when you match deeper structures with nesting - hashes for example.
|
32
|
+
# There are two methods for those - `HashWith()` and `HashOf`. `HashWith` checks for the presence of the given
|
33
|
+
# key/value pairs and checks values for matches, but if there are _other_ keys present in the Hash given for
|
34
|
+
# verification it won't complain. `HashOf()`, in contrast, will ensure there are _only_ the mentioned keys
|
35
|
+
# in the Hash, and will not match if something else is present.
|
36
|
+
#
|
37
|
+
# HashWith(age: Fixnum) === {age: 12, name: 'Cisco Kid'} #=> true
|
38
|
+
# HashOf(age: Fixnum) === {age: 12, name: 'Cisco Kid'} #=> false
|
39
|
+
#
|
40
|
+
# Note that it is _entirely_ plausible that you would not want to include Mal into
|
41
|
+
# your object/class/whatever. For that case, calling methods using their qualified
|
42
|
+
# module name (`Mal.Some()...`) can become a nuisance. If it does,
|
43
|
+
# use `Mal.typespec` which is a shortcut to `instance_exec` - but has the convenient
|
44
|
+
# property of not alerting your style watchers to your use of `instance_exec` :-P
|
1
45
|
module Mal
|
2
|
-
VERSION = '0.0.
|
46
|
+
VERSION = '0.0.4'
|
3
47
|
|
4
48
|
class AnythingT
|
5
49
|
def ===(value)
|
@@ -8,6 +52,14 @@ module Mal
|
|
8
52
|
def inspect
|
9
53
|
'Anything()'
|
10
54
|
end
|
55
|
+
|
56
|
+
def |(another)
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def &(another)
|
61
|
+
another # Another is automatically considered more specific, and replaces Anything
|
62
|
+
end
|
11
63
|
end
|
12
64
|
|
13
65
|
class OnlyT
|
@@ -22,13 +74,30 @@ module Mal
|
|
22
74
|
def inspect
|
23
75
|
'Only(%s)' % @matchable.inspect
|
24
76
|
end
|
77
|
+
|
78
|
+
def |(another)
|
79
|
+
if another.is_a?(AnythingT)
|
80
|
+
another
|
81
|
+
else
|
82
|
+
EitherT.new(self, another)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def &(another)
|
87
|
+
if another.is_a?(AnythingT)
|
88
|
+
self
|
89
|
+
else
|
90
|
+
UnionT.new(self, another)
|
91
|
+
end
|
92
|
+
end
|
25
93
|
end
|
26
94
|
|
27
95
|
class NilT < OnlyT
|
28
96
|
def inspect; 'Nil()'; end
|
97
|
+
def |(another); MaybeT.new(another); end
|
29
98
|
end
|
30
99
|
|
31
|
-
class HashT
|
100
|
+
class HashT < OnlyT
|
32
101
|
def initialize(**required_keys_to_matchers)
|
33
102
|
@required_keys_to_matchers = required_keys_to_matchers
|
34
103
|
end
|
@@ -47,6 +116,31 @@ module Mal
|
|
47
116
|
end
|
48
117
|
end
|
49
118
|
|
119
|
+
class ObjectT < OnlyT
|
120
|
+
attr_reader :supported_methods
|
121
|
+
def initialize(*supported_methods)
|
122
|
+
@supported_methods = supported_methods.map(&:to_sym)
|
123
|
+
end
|
124
|
+
|
125
|
+
def ===(value)
|
126
|
+
@supported_methods.each do |m|
|
127
|
+
return false unless value.respond_to?(m)
|
128
|
+
end
|
129
|
+
true
|
130
|
+
end
|
131
|
+
|
132
|
+
def inspect
|
133
|
+
'ObjectWith(%s)' % @supported_methods.map(&:inspect).join(', ')
|
134
|
+
end
|
135
|
+
|
136
|
+
def &(another)
|
137
|
+
if another.is_a?(ObjectT)
|
138
|
+
methods_of_both = (@supported_methods + another.supported_methods).uniq
|
139
|
+
ObjectT.new(*methods_of_both)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
50
144
|
class HashOfOnlyT < HashT
|
51
145
|
def ===(value)
|
52
146
|
return false unless super
|
@@ -57,7 +151,20 @@ module Mal
|
|
57
151
|
'HashOf(%s)' % @required_keys_to_matchers.inspect[1..-2]
|
58
152
|
end
|
59
153
|
end
|
60
|
-
|
154
|
+
|
155
|
+
class HashPermittingT < HashT
|
156
|
+
def ===(value)
|
157
|
+
return false unless super
|
158
|
+
permitted = Set.new(@required_keys_to_matchers.keys)
|
159
|
+
present = Set.new(value.keys)
|
160
|
+
present.subset?(permitted)
|
161
|
+
end
|
162
|
+
|
163
|
+
def inspect
|
164
|
+
'HashPermitting(%s)' % @required_keys_to_matchers.inspect[1..-2]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
61
168
|
class ArrayT < OnlyT
|
62
169
|
def initialize(matcher_for_each_array_element)
|
63
170
|
@matcher_for_each_array_element = matcher_for_each_array_element
|
@@ -86,6 +193,8 @@ module Mal
|
|
86
193
|
return false unless value.respond_to? :length
|
87
194
|
@desired_length == value.length
|
88
195
|
end
|
196
|
+
|
197
|
+
def inspect; 'OfElements(%d)' % @desired_length; end
|
89
198
|
end
|
90
199
|
|
91
200
|
class MinLengthT < LengthT
|
@@ -105,6 +214,7 @@ module Mal
|
|
105
214
|
end
|
106
215
|
|
107
216
|
class EitherT < OnlyT
|
217
|
+
attr_reader :types
|
108
218
|
def initialize(*types)
|
109
219
|
@types = types
|
110
220
|
end
|
@@ -116,9 +226,19 @@ module Mal
|
|
116
226
|
def inspect
|
117
227
|
'Either(%s)' % @types.map{|e| e.inspect }.join(', ')
|
118
228
|
end
|
229
|
+
|
230
|
+
def |(another)
|
231
|
+
types_matched = if another.is_a?(EitherT)
|
232
|
+
(@types + another.types).uniq
|
233
|
+
else
|
234
|
+
(@types + [another]).uniq
|
235
|
+
end
|
236
|
+
EitherT.new(*types_matched)
|
237
|
+
end
|
119
238
|
end
|
120
239
|
|
121
240
|
class UnionT < OnlyT
|
241
|
+
attr_reader :types
|
122
242
|
def initialize(*types)
|
123
243
|
@types = types
|
124
244
|
end
|
@@ -130,19 +250,58 @@ module Mal
|
|
130
250
|
def inspect
|
131
251
|
'Both(%s)' % @types.map{|e| e.inspect }.join(', ')
|
132
252
|
end
|
253
|
+
|
254
|
+
def &(another)
|
255
|
+
if another.is_a?(UnionT)
|
256
|
+
unique_types = (@types + another.types).uniq
|
257
|
+
return UnionT.new(*unique_types)
|
258
|
+
end
|
259
|
+
super
|
260
|
+
end
|
133
261
|
end
|
134
262
|
|
135
263
|
class MaybeT < EitherT
|
264
|
+
def initialize(matchable); super(NilClass, matchable); end
|
136
265
|
def inspect; 'Maybe(%s)' % @types[1].inspect; end
|
137
266
|
end
|
138
267
|
|
139
268
|
class BoolT < EitherT
|
140
269
|
def initialize; super(TrueClass, FalseClass); end
|
141
270
|
def inspect; 'Bool()'; end
|
271
|
+
def |(another); EitherT.new(self, another); end
|
142
272
|
end
|
143
273
|
|
144
|
-
|
274
|
+
class ValueT < OnlyT
|
275
|
+
def ===(value)
|
276
|
+
@matchable == value
|
277
|
+
end
|
278
|
+
def inspect; 'Value(%s)' % @matchable.inspect; end
|
279
|
+
end
|
145
280
|
|
281
|
+
class SatisfyingT < OnlyT
|
282
|
+
def initialize(&blk)
|
283
|
+
super(blk.to_proc) # Use an OnlyT since Proc objects support ===
|
284
|
+
end
|
285
|
+
def inspect; 'Satisfying(&blk)'; end
|
286
|
+
end
|
287
|
+
|
288
|
+
class IncludingT < ValueT
|
289
|
+
def ===(value)
|
290
|
+
@matchable.include?(value)
|
291
|
+
end
|
292
|
+
def inspect; 'IncludedIn(%s)' % @matchable.inspect; end
|
293
|
+
end
|
294
|
+
|
295
|
+
class CoveringT < ValueT
|
296
|
+
def ===(value)
|
297
|
+
@matchable.cover?(value)
|
298
|
+
end
|
299
|
+
def inspect; 'CoveredBy(%s)' % @matchable.inspect; end
|
300
|
+
end
|
301
|
+
|
302
|
+
typespec_consts = self.constants.grep(/[a-z]T$/)
|
303
|
+
private_constant *typespec_consts
|
304
|
+
|
146
305
|
# Specifies a value that may only ever be `nil` and nothing else
|
147
306
|
def Nil()
|
148
307
|
NilT.new(NilClass)
|
@@ -171,7 +330,7 @@ module Mal
|
|
171
330
|
|
172
331
|
# Specifies a value that is either matching the given typespec, or is nil
|
173
332
|
def Maybe(matchable)
|
174
|
-
MaybeT.new(
|
333
|
+
MaybeT.new(matchable)
|
175
334
|
end
|
176
335
|
|
177
336
|
# Specifies an Array of at least 1 element, where each element matches the
|
@@ -183,12 +342,22 @@ module Mal
|
|
183
342
|
# Specifies a Hash containing at least the given keys, with values at those
|
184
343
|
# keys matching the given matchers. For example, for a Hash having at
|
185
344
|
# least the `:name` key with a corresponding value that is a String:
|
186
|
-
#
|
345
|
+
#
|
346
|
+
# HashWith(name: String)
|
347
|
+
#
|
187
348
|
# Since the match is non-strict, it will also match a Hash having more keys
|
188
|
-
#
|
349
|
+
#
|
350
|
+
# HashWith(name: String) === {name: 'John Doe', age: 21} #=> true
|
189
351
|
def HashWith(**keys_to_values)
|
190
352
|
HashT.new(**keys_to_values)
|
191
353
|
end
|
354
|
+
|
355
|
+
# Specifies an object responding to certain methods
|
356
|
+
#
|
357
|
+
# ObjectWith(:downcase) === "foo" #=> true
|
358
|
+
def ObjectWith(*properties)
|
359
|
+
ObjectT.new(*properties)
|
360
|
+
end
|
192
361
|
|
193
362
|
def OfAtLeastElements(n)
|
194
363
|
MinLengthT.new(n)
|
@@ -211,10 +380,46 @@ module Mal
|
|
211
380
|
HashOfOnlyT.new(**keys_to_values)
|
212
381
|
end
|
213
382
|
|
383
|
+
# Specifies a Hash containing the given keys/values or their subset,
|
384
|
+
# will also match an empty Hash. Will _not_ match a Hash having extra
|
385
|
+
# keys.
|
386
|
+
def HashPermitting(**keys_to_values)
|
387
|
+
HashPermittingT.new(**keys_to_values)
|
388
|
+
end
|
389
|
+
|
214
390
|
# Just like it says: will match any value given to it
|
215
391
|
def Anything()
|
216
392
|
AnythingT.new
|
217
393
|
end
|
218
394
|
|
395
|
+
# Matches the contained value exactly using the == operator. Will work well
|
396
|
+
# where exact matches are desired, i.e. for strings, numbers and native language
|
397
|
+
# types.
|
398
|
+
def Value(value)
|
399
|
+
ValueT.new(value)
|
400
|
+
end
|
401
|
+
|
402
|
+
# Matches the given value if the passed block/Proc returns true when called with that value
|
403
|
+
# Satisfying {|x| x > 10 } === 11 #=> true
|
404
|
+
def Satisfying(&blk)
|
405
|
+
SatisfyingT.new(&blk)
|
406
|
+
end
|
407
|
+
|
408
|
+
def CoveredBy(range)
|
409
|
+
CoveringT.new(range)
|
410
|
+
end
|
411
|
+
|
412
|
+
def IncludedIn(*values)
|
413
|
+
IncludingT.new(values)
|
414
|
+
end
|
415
|
+
|
416
|
+
# A shortcut for `instance_exec`, for defining types using shorthand method names
|
417
|
+
# from outside the module:
|
418
|
+
#
|
419
|
+
# Mal.typespec { Either(Value(1), Value(2)) }
|
420
|
+
def self.typespec(&blk)
|
421
|
+
instance_exec(&blk)
|
422
|
+
end
|
423
|
+
|
219
424
|
extend self
|
220
425
|
end
|
data/mal.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: mal 0.0.
|
5
|
+
# stub: mal 0.0.4 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "mal"
|
9
|
-
s.version = "0.0.
|
9
|
+
s.version = "0.0.4"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
13
13
|
s.authors = ["Julik Tarkhanov"]
|
14
|
-
s.date = "2016-09-
|
14
|
+
s.date = "2016-09-28"
|
15
15
|
s.description = "Minuscule algebraic types for Ruby"
|
16
16
|
s.email = "me@julik.nl"
|
17
17
|
s.extra_rdoc_files = [
|
@@ -27,6 +27,7 @@ Gem::Specification.new do |s|
|
|
27
27
|
"Rakefile",
|
28
28
|
"lib/mal.rb",
|
29
29
|
"mal.gemspec",
|
30
|
+
"spec/mal_external_eval_spec.rb",
|
30
31
|
"spec/mal_spec.rb",
|
31
32
|
"spec/spec_helper.rb"
|
32
33
|
]
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Mal' do
|
4
|
+
describe '.typespec' do
|
5
|
+
it 'allows shorthand typespecs to be created without module prefixing' do
|
6
|
+
matcher = Mal.typespec { Maybe(String) }
|
7
|
+
expect(matcher.inspect).to eq('Maybe(String)')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
data/spec/mal_spec.rb
CHANGED
@@ -13,6 +13,34 @@ describe 'Mal' do
|
|
13
13
|
expect(match_v).to eq(false), "Expected #{value.inspect} not to match #{typespec.inspect}"
|
14
14
|
end
|
15
15
|
|
16
|
+
describe '.typespec' do
|
17
|
+
# TODO: we include Mal in this spec as is, so we might need to make another file
|
18
|
+
it 'allows shorthand typespecs to be created'
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'Anything()' do
|
22
|
+
it 'provides a good inspect' do
|
23
|
+
expect(Anything().inspect).to eq('Anything()')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'matches anything' do
|
27
|
+
expect_match_of(Anything(), 0)
|
28
|
+
expect_match_of(Anything(), self)
|
29
|
+
expect_match_of(Anything(), true)
|
30
|
+
expect_match_of(Anything(), nil)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'ORs to itself' do
|
34
|
+
m = Anything() | Bool()
|
35
|
+
expect(m.inspect).to eq('Anything()')
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'ANDs to the right operand' do
|
39
|
+
m = Anything() & Bool()
|
40
|
+
expect(m.inspect).to eq('Bool()')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
16
44
|
describe 'Nil()' do
|
17
45
|
it 'provides a good inspect' do
|
18
46
|
expect(Nil().inspect).to eq('Nil()')
|
@@ -24,6 +52,20 @@ describe 'Mal' do
|
|
24
52
|
expect_no_match_of(Nil(), Object.new)
|
25
53
|
expect_no_match_of(Nil(), self)
|
26
54
|
end
|
55
|
+
|
56
|
+
it 'ORs to a Maybe of Nil() and Bool()' do
|
57
|
+
m = Nil() | Bool()
|
58
|
+
expect(m.inspect).to eq('Maybe(Bool())')
|
59
|
+
expect_match_of(m, nil)
|
60
|
+
expect_match_of(m, true)
|
61
|
+
expect_match_of(m, false)
|
62
|
+
expect_no_match_of(m, 0)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'ANDs to a Both()' do
|
66
|
+
m = Nil() & Bool()
|
67
|
+
expect(m.inspect).to eq('Both(Nil(), Bool())')
|
68
|
+
end
|
27
69
|
end
|
28
70
|
|
29
71
|
describe 'Bool()' do
|
@@ -39,6 +81,21 @@ describe 'Mal' do
|
|
39
81
|
expect_no_match_of(Bool(), 'hello world')
|
40
82
|
expect_no_match_of(Bool(), 123)
|
41
83
|
end
|
84
|
+
|
85
|
+
it 'ORs to a an Either()' do
|
86
|
+
m = Bool() | Fixnum
|
87
|
+
expect(m.inspect).to eq('Either(Bool(), Fixnum)')
|
88
|
+
expect_match_of(m, true)
|
89
|
+
expect_match_of(m, false)
|
90
|
+
expect_match_of(m, 12)
|
91
|
+
expect_no_match_of(m, 'hello world')
|
92
|
+
expect_no_match_of(m, [])
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'ANDs to a Both()' do
|
96
|
+
m = Bool() & TrueClass
|
97
|
+
expect(m.inspect).to eq('Both(Bool(), TrueClass)')
|
98
|
+
end
|
42
99
|
end
|
43
100
|
|
44
101
|
describe 'Either()' do
|
@@ -54,6 +111,31 @@ describe 'Mal' do
|
|
54
111
|
expect_no_match_of(m, nil)
|
55
112
|
expect_no_match_of(m, self)
|
56
113
|
end
|
114
|
+
|
115
|
+
it 'ORs to a union Either()' do
|
116
|
+
m = Either(Fixnum, TrueClass) | Anything()
|
117
|
+
expect(m.inspect).to eq('Either(Fixnum, TrueClass, Anything())')
|
118
|
+
expect_match_of(m, true)
|
119
|
+
expect_match_of(m, false)
|
120
|
+
expect_match_of(m, 12)
|
121
|
+
expect_match_of(m, 'hello world')
|
122
|
+
expect_match_of(m, [])
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'ORs to a union of two Either() types' do
|
126
|
+
m = Either(Fixnum, TrueClass) | Either(Fixnum, FalseClass)
|
127
|
+
expect(m.inspect).to eq('Either(Fixnum, TrueClass, FalseClass)')
|
128
|
+
expect_match_of(m, true)
|
129
|
+
expect_match_of(m, false)
|
130
|
+
expect_match_of(m, 12)
|
131
|
+
expect_no_match_of(m, 'hello world')
|
132
|
+
expect_no_match_of(m, [])
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'ANDs to a Both()' do
|
136
|
+
m = Either(Fixnum, TrueClass) & TrueClass
|
137
|
+
expect(m.inspect).to eq('Both(Either(Fixnum, TrueClass), TrueClass)')
|
138
|
+
end
|
57
139
|
end
|
58
140
|
|
59
141
|
describe 'Maybe()' do
|
@@ -68,6 +150,16 @@ describe 'Mal' do
|
|
68
150
|
expect_no_match_of(m, false)
|
69
151
|
expect_no_match_of(m, self)
|
70
152
|
end
|
153
|
+
|
154
|
+
it 'ORs to an Either()' do
|
155
|
+
m = Maybe(String) | Fixnum
|
156
|
+
expect(m.inspect).to eq('Either(NilClass, String, Fixnum)')
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'ANDs to a Both()' do
|
160
|
+
m = Maybe(String) & TrueClass
|
161
|
+
expect(m.inspect).to eq('Both(Maybe(String), TrueClass)')
|
162
|
+
end
|
71
163
|
end
|
72
164
|
|
73
165
|
describe 'ArrayOf()' do
|
@@ -97,6 +189,19 @@ describe 'Mal' do
|
|
97
189
|
expect_no_match_of(m, [true, false, 1, 'foo'])
|
98
190
|
expect_match_of(m, [true, false, 1])
|
99
191
|
end
|
192
|
+
|
193
|
+
it 'ORs to an Either()' do
|
194
|
+
m = ArrayOf(String) | ArrayOf(Fixnum)
|
195
|
+
expect(m.inspect).to eq('Either(ArrayOf(String), ArrayOf(Fixnum))')
|
196
|
+
expect_match_of(m, ['a', 'b', 'c'])
|
197
|
+
expect_match_of(m, [1, 2, 3])
|
198
|
+
expect_no_match_of(m, ['a', 2, 3])
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'ANDs to a Both()' do
|
202
|
+
m = ArrayOf(Maybe(String)) & ArrayOf(String)
|
203
|
+
expect(m.inspect).to eq('Both(ArrayOf(Maybe(String)), ArrayOf(String))')
|
204
|
+
end
|
100
205
|
end
|
101
206
|
|
102
207
|
describe 'Both()' do
|
@@ -110,6 +215,30 @@ describe 'Mal' do
|
|
110
215
|
expect_match_of(m, 'this is an abc string')
|
111
216
|
expect_no_match_of(m, 'this is a string that does match the second parameter')
|
112
217
|
end
|
218
|
+
|
219
|
+
it 'ORs to an Either()' do
|
220
|
+
m = Both(String, /hello/) | Fixnum
|
221
|
+
expect(m.inspect).to eq('Either(Both(String, /hello/), Fixnum)')
|
222
|
+
expect_match_of(m, 'hello')
|
223
|
+
expect_no_match_of(m, 'goodbye')
|
224
|
+
expect_match_of(m, 123)
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'ANDs to a Both()' do
|
228
|
+
m = Both(String, /hello/) & Maybe(Fixnum)
|
229
|
+
expect(m.inspect).to eq('Both(Both(String, /hello/), Maybe(Fixnum))')
|
230
|
+
expect_no_match_of(m, 'hello')
|
231
|
+
expect_no_match_of(m, 'goodbye')
|
232
|
+
expect_no_match_of(m, 123)
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'ANDs to a flattened Both() if the right hand operand is also a Both' do
|
236
|
+
m = Both(String, /hello/) & Both(String, /he/)
|
237
|
+
expect(m.inspect).to eq('Both(String, /hello/, /he/)')
|
238
|
+
expect_match_of(m, 'hello')
|
239
|
+
expect_no_match_of(m, 'goodbye')
|
240
|
+
expect_no_match_of(m, 123)
|
241
|
+
end
|
113
242
|
end
|
114
243
|
|
115
244
|
describe 'HashWith()' do
|
@@ -145,8 +274,75 @@ describe 'Mal' do
|
|
145
274
|
m = HashWith(name: String)
|
146
275
|
expect_match_of(m, {name: 'John Doe', age: 21})
|
147
276
|
end
|
277
|
+
|
278
|
+
it 'ORs to an Either()' do
|
279
|
+
m = HashWith(name: String) | HashWith(first_name: String, last_name: String)
|
280
|
+
expect(m.inspect).to eq('Either(HashWith(:name=>String), HashWith(:first_name=>String, :last_name=>String))')
|
281
|
+
expect_match_of(m, {name: 'Jane'})
|
282
|
+
expect_match_of(m, {first_name: 'Jane', last_name: 'Doe'})
|
283
|
+
expect_match_of(m, {name: 'Jane', first_name: 'Jane', last_name: 'Doe'})
|
284
|
+
end
|
285
|
+
|
286
|
+
it 'ANDs to a Both' do
|
287
|
+
m = Both(HashWith(name: Anything()), HashWith(age: Anything()))
|
288
|
+
expect(m.inspect).to eq('Both(HashWith(:name=>Anything()), HashWith(:age=>Anything()))')
|
289
|
+
expect_match_of(m, {name: nil, age: 12})
|
290
|
+
expect_no_match_of(m, {name: 'Jane'})
|
291
|
+
expect_no_match_of(m, {age: 21})
|
292
|
+
end
|
148
293
|
end
|
149
|
-
|
294
|
+
|
295
|
+
describe 'HashPermitting()' do
|
296
|
+
it 'provides a good inspect' do
|
297
|
+
m = HashPermitting(foo: Maybe(Fixnum))
|
298
|
+
expect(m.inspect).to eq("HashPermitting(:foo=>Maybe(Fixnum))")
|
299
|
+
end
|
300
|
+
|
301
|
+
it 'does not match other types' do
|
302
|
+
m = HashPermitting(foo: Symbol)
|
303
|
+
expect_no_match_of(m, nil)
|
304
|
+
expect_no_match_of(m, [])
|
305
|
+
expect_no_match_of(m, self)
|
306
|
+
end
|
307
|
+
|
308
|
+
it 'does match an empty Hash when asked to' do
|
309
|
+
m = HashPermitting({})
|
310
|
+
expect_match_of(m, {})
|
311
|
+
end
|
312
|
+
|
313
|
+
it 'does not match a Hash whose value does not satisfy the matcher' do
|
314
|
+
m = HashPermitting(some_key: Nil())
|
315
|
+
expect_no_match_of(m, {some_key: true})
|
316
|
+
end
|
317
|
+
|
318
|
+
it 'does match a Hash whose keys/values do satisfy the matcher' do
|
319
|
+
m = HashPermitting(some_key: Maybe(String))
|
320
|
+
expect_match_of(m, {some_key: nil})
|
321
|
+
expect_match_of(m, {some_key: 'hello world'})
|
322
|
+
end
|
323
|
+
|
324
|
+
it 'does not match a Hash that has more keys than requested' do
|
325
|
+
m = HashPermitting(name: String)
|
326
|
+
expect_no_match_of(m, {name: 'John Doe', age: 21})
|
327
|
+
end
|
328
|
+
|
329
|
+
it 'ORs to an Either()' do
|
330
|
+
m = HashPermitting(name: String) | HashPermitting(first_name: String, last_name: String)
|
331
|
+
expect(m.inspect).to eq('Either(HashPermitting(:name=>String), HashPermitting(:first_name=>String, :last_name=>String))')
|
332
|
+
expect_match_of(m, {name: 'Jane'})
|
333
|
+
expect_match_of(m, {first_name: 'Jane', last_name: 'Doe'})
|
334
|
+
expect_no_match_of(m, {name: 'Jane', first_name: 'Jane', last_name: 'Doe'})
|
335
|
+
end
|
336
|
+
|
337
|
+
it 'ANDs to a Both' do
|
338
|
+
m = Both(HashPermitting(name: Anything()), HashPermitting(name: Anything(), age: Anything()))
|
339
|
+
expect(m.inspect).to eq('Both(HashPermitting(:name=>Anything()), HashPermitting(:name=>Anything(), :age=>Anything()))')
|
340
|
+
expect_no_match_of(m, {name: nil, age: 12})
|
341
|
+
expect_no_match_of(m, {name: 'Jane'})
|
342
|
+
expect_no_match_of(m, {age: 21})
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
150
346
|
describe 'HashOf()' do
|
151
347
|
it 'provides a good inspect' do
|
152
348
|
m = HashOf(foo: Maybe(Fixnum))
|
@@ -180,6 +376,14 @@ describe 'Mal' do
|
|
180
376
|
m = HashOf(name: String)
|
181
377
|
expect_no_match_of(m, {name: 'John Doe', age: 21})
|
182
378
|
end
|
379
|
+
|
380
|
+
it 'ORs to an Either()' do
|
381
|
+
m = HashOf(name: String) | HashOf(first_name: String, last_name: String)
|
382
|
+
expect(m.inspect).to eq('Either(HashOf(:name=>String), HashOf(:first_name=>String, :last_name=>String))')
|
383
|
+
expect_match_of(m, {name: 'Jane'})
|
384
|
+
expect_match_of(m, {first_name: 'Jane', last_name: 'Doe'})
|
385
|
+
expect_no_match_of(m, {name: 'Jane', first_name: 'Jane', last_name: 'Doe'})
|
386
|
+
end
|
183
387
|
end
|
184
388
|
|
185
389
|
describe 'HashOf with complex nested matchers' do
|
@@ -229,5 +433,91 @@ describe 'Mal' do
|
|
229
433
|
expect_match_of(Anything(), true)
|
230
434
|
expect_match_of(Anything(), false)
|
231
435
|
end
|
436
|
+
|
437
|
+
it 'ANDs to the right hand operand' do
|
438
|
+
m = Anything() & Bool()
|
439
|
+
expect(m.inspect).to eq('Bool()')
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
describe 'ObjectWith()' do
|
444
|
+
it 'has a reasonable inspect' do
|
445
|
+
expect(ObjectWith(:upcase, :downcase).inspect).to eq('ObjectWith(:upcase, :downcase)')
|
446
|
+
end
|
447
|
+
|
448
|
+
it 'matches only objects that respond to the methods defined' do
|
449
|
+
m = ObjectWith(:upcase, :downcase)
|
450
|
+
only_downcase = Struct.new(:downcase).new('nope')
|
451
|
+
only_upcase = Struct.new(:upcase).new('nope')
|
452
|
+
both = Struct.new(:upcase, :downcase).new('A', 'a')
|
453
|
+
|
454
|
+
expect_match_of(m, 'foo')
|
455
|
+
expect_match_of(m, both)
|
456
|
+
expect_no_match_of(m, only_downcase)
|
457
|
+
expect_no_match_of(m, only_upcase)
|
458
|
+
end
|
459
|
+
|
460
|
+
it 'ORs to a Both' do
|
461
|
+
m = ObjectWith(:upcase) | Nil()
|
462
|
+
|
463
|
+
expect(m.inspect).to eq('Either(ObjectWith(:upcase), Nil())')
|
464
|
+
end
|
465
|
+
|
466
|
+
it 'ANDs to a single ObjectWith' do
|
467
|
+
m = ObjectWith(:upcase) & ObjectWith(:downcase)
|
468
|
+
|
469
|
+
expect(m.inspect).to eq('ObjectWith(:upcase, :downcase)')
|
470
|
+
|
471
|
+
only_downcase = Struct.new(:downcase).new('nope')
|
472
|
+
only_upcase = Struct.new(:upcase).new('nope')
|
473
|
+
both = Struct.new(:upcase, :downcase).new('A', 'a')
|
474
|
+
|
475
|
+
expect_match_of(m, 'foo')
|
476
|
+
expect_match_of(m, both)
|
477
|
+
expect_no_match_of(m, only_downcase)
|
478
|
+
expect_no_match_of(m, only_upcase)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
describe 'Value()' do
|
483
|
+
it 'provides a good inspect' do
|
484
|
+
expect(Value('ohai').inspect).to eq('Value("ohai")')
|
485
|
+
end
|
486
|
+
|
487
|
+
it 'matches only the exact value given' do
|
488
|
+
expect_match_of(Value(8), 8)
|
489
|
+
expect_no_match_of(Value(8), 4)
|
490
|
+
expect_no_match_of(Value(8), {})
|
491
|
+
expect_no_match_of(Value(:ohai), {})
|
492
|
+
end
|
493
|
+
|
494
|
+
it 'ORs to an Either' do
|
495
|
+
m = Value(7) | Value(5)
|
496
|
+
expect(m.inspect).to eq('Either(Value(7), Value(5))')
|
497
|
+
end
|
498
|
+
|
499
|
+
it 'ANDs to the right operand' do
|
500
|
+
m = Value(10) & Value(2)
|
501
|
+
expect(m.inspect).to eq('Both(Value(10), Value(2))')
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
describe 'CoveredBy()' do
|
506
|
+
it 'provides a good inspect' do
|
507
|
+
expect(CoveredBy(1..4).inspect).to eq('CoveredBy(1..4)')
|
508
|
+
end
|
509
|
+
|
510
|
+
it 'matches values covered by the Range' do
|
511
|
+
m = CoveredBy(1..4)
|
512
|
+
expect_match_of(m, 1)
|
513
|
+
expect_match_of(m, 2)
|
514
|
+
expect_match_of(m, 3)
|
515
|
+
expect_match_of(m, 4)
|
516
|
+
|
517
|
+
expect_no_match_of(m, 0)
|
518
|
+
expect_no_match_of(m, 5)
|
519
|
+
expect_no_match_of(m, 'Ohai')
|
520
|
+
expect_no_match_of(m, nil)
|
521
|
+
end
|
232
522
|
end
|
233
523
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-09-
|
11
|
+
date: 2016-09-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -96,6 +96,7 @@ files:
|
|
96
96
|
- Rakefile
|
97
97
|
- lib/mal.rb
|
98
98
|
- mal.gemspec
|
99
|
+
- spec/mal_external_eval_spec.rb
|
99
100
|
- spec/mal_spec.rb
|
100
101
|
- spec/spec_helper.rb
|
101
102
|
homepage: http://github.com/julik/mal
|