mal 0.0.2 → 0.0.4
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 +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
|