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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e9e34384af7d008d577ba4062c834b0bf5c8cedb
4
- data.tar.gz: f47b158a59a3fd77b9013d63312101be8efc8e0f
3
+ metadata.gz: 8322ccc3484573bae624022558dcd1c09f590617
4
+ data.tar.gz: 92eeb1370d980bc3a811fe5d2656ba82f78d53cd
5
5
  SHA512:
6
- metadata.gz: a6665014e7b2ec843c58877678c9b4e8df1cc16c32cfed0619ec524e10df9158e2cd919d83fd9aa1a54b2331fc4ef3e65d3fe85378311ecd2c52151234c05ee4
7
- data.tar.gz: ca145ca000f736042934203cca6a68fb6e5e7fb8da5a81e8ded06e350f03485254b14f2e1dca76585ad81de9a652d654e087a082f7855a3490ee0271743c45f6
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) Algerba - a type-matching thing for Ruby.
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.HashOf(name: String, age: Numeric) === {name: 'Jane', age: 22, accepted_license_agreement: true} #=> true
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.2'
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
- private_constant :NilT, :BoolT, :EitherT, :MaybeT, :ArrayT, :HashT, :BoolT, :HashOfOnlyT, :MaxLengthT, :MinLengthT
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(NilClass, matchable)
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
- # HashOf(name: String)
345
+ #
346
+ # HashWith(name: String)
347
+ #
187
348
  # Since the match is non-strict, it will also match a Hash having more keys
188
- # HashOf(name: String) === {name: 'John Doe', age: 21} #=> true
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
@@ -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.2 ruby lib
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.2"
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-16"
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
@@ -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
@@ -5,5 +5,7 @@ RSpec.configure do |config|
5
5
  end
6
6
 
7
7
  require 'simplecov'
8
- SimpleCov.start
8
+ SimpleCov.start do
9
+ add_filter "/spec/"
10
+ end
9
11
  require_relative '../lib/mal'
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.2
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-16 00:00:00.000000000 Z
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