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 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