matchmaker 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README +310 -0
- data/README.markdown +310 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/lib/matchmaker.rb +479 -0
- data/matchmaker.gemspec +55 -0
- data/spec/case_spec.rb +466 -0
- data/spec/spec_helper.rb +9 -0
- metadata +77 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'rake'
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
require 'jeweler'
|
|
6
|
+
Jeweler::Tasks.new do |gem|
|
|
7
|
+
gem.name = "matchmaker"
|
|
8
|
+
gem.summary = %Q{Ruby Pattern Matching}
|
|
9
|
+
gem.description = %Q{A pattern matching library}
|
|
10
|
+
gem.email = "hayeah@gmail.com"
|
|
11
|
+
gem.homepage = "http://github.com/hayeah/case"
|
|
12
|
+
gem.authors = ["Howard Yeh"]
|
|
13
|
+
gem.add_development_dependency "rspec"
|
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
|
15
|
+
end
|
|
16
|
+
rescue LoadError
|
|
17
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
require 'spec/rake/spectask'
|
|
21
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
|
22
|
+
spec.libs << 'lib' << 'spec'
|
|
23
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
|
27
|
+
spec.libs << 'lib' << 'spec'
|
|
28
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
|
29
|
+
spec.rcov = true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
task :spec => :check_dependencies
|
|
33
|
+
|
|
34
|
+
task :default => :spec
|
|
35
|
+
|
|
36
|
+
require 'rake/rdoctask'
|
|
37
|
+
Rake::RDocTask.new do |rdoc|
|
|
38
|
+
if File.exist?('VERSION')
|
|
39
|
+
version = File.read('VERSION')
|
|
40
|
+
else
|
|
41
|
+
version = ""
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
45
|
+
rdoc.title = "case #{version}"
|
|
46
|
+
rdoc.rdoc_files.include('README*')
|
|
47
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
48
|
+
end
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.1
|
data/lib/matchmaker.rb
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
require 'pp'
|
|
3
|
+
require 'stringio'
|
|
4
|
+
|
|
5
|
+
class Case
|
|
6
|
+
class CaseError < StandardError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class NoMatch < CaseError
|
|
10
|
+
def initialize(stack,pattern_stack,msg=nil)
|
|
11
|
+
@stack = stack
|
|
12
|
+
@pattern_stack = pattern_stack
|
|
13
|
+
@msg = msg
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def inspect
|
|
17
|
+
self.to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_s
|
|
21
|
+
io = StringIO.new
|
|
22
|
+
io.puts "#<#{self.class} #{@msg}"
|
|
23
|
+
trace = @stack.zip(@pattern_stack).map { |obj,pat|
|
|
24
|
+
if pat.label
|
|
25
|
+
io.print pat.label
|
|
26
|
+
io.print ": "
|
|
27
|
+
PP.pp(obj,io)
|
|
28
|
+
|
|
29
|
+
else
|
|
30
|
+
PP.pp(obj,io)
|
|
31
|
+
end
|
|
32
|
+
}
|
|
33
|
+
io.string
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class CaseFail < CaseError
|
|
38
|
+
def initialize(errors)
|
|
39
|
+
@errors = errors
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_s
|
|
43
|
+
io = StringIO.new
|
|
44
|
+
@errors.each { |e|
|
|
45
|
+
io.puts e
|
|
46
|
+
io.puts ""
|
|
47
|
+
}
|
|
48
|
+
io.string
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class NoClauses < CaseError
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class UnboundVariable < CaseError
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class Pattern
|
|
59
|
+
attr_reader :matcher, :guard, :variable, :label
|
|
60
|
+
def initialize(matcher,guard,variable,label=nil)
|
|
61
|
+
@matcher = matcher # Proc || Pattern
|
|
62
|
+
@guard = guard # Proc || nil
|
|
63
|
+
@variable = variable.to_s.downcase.to_sym if !variable.nil?
|
|
64
|
+
@label = label
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def match(context)
|
|
68
|
+
case @matcher
|
|
69
|
+
when Pattern
|
|
70
|
+
result = @matcher.match(context)
|
|
71
|
+
when Proc
|
|
72
|
+
if @matcher.arity == 2
|
|
73
|
+
result = @matcher.call(context.current,context)
|
|
74
|
+
else
|
|
75
|
+
result = @matcher.call(context.current)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
end
|
|
79
|
+
context.fail unless result
|
|
80
|
+
if @guard
|
|
81
|
+
result = @guard.call(context.current)
|
|
82
|
+
context.fail("guard failed") unless result
|
|
83
|
+
end
|
|
84
|
+
context.bind(@variable,context.current) if @variable
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def when(&block)
|
|
89
|
+
@guard = block
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def bind(var)
|
|
94
|
+
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# matches the tail of an array
|
|
99
|
+
class StarPattern # [*pattern]
|
|
100
|
+
attr_reader :pattern, :variable, :guard
|
|
101
|
+
def initialize(pattern,guard,variable)
|
|
102
|
+
@pattern = pattern
|
|
103
|
+
@guard = guard
|
|
104
|
+
@variable = variable
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class MatchContext
|
|
109
|
+
def initialize(pattern,object)
|
|
110
|
+
# stack of references we are destructuring,
|
|
111
|
+
# we start off with the object itself.
|
|
112
|
+
@stack = [object]
|
|
113
|
+
@pattern_stack = [pattern]
|
|
114
|
+
@bindings = {}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def current
|
|
118
|
+
@stack.last
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def bind(var,val)
|
|
122
|
+
if @bindings.has_key?(var)
|
|
123
|
+
self.fail unless @bindings[var] == val
|
|
124
|
+
else
|
|
125
|
+
@bindings[var] = val
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def fail(msg=nil)
|
|
130
|
+
raise NoMatch.new(@stack,@pattern_stack,msg) # what object is failing patter match
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
unless defined?(BasicObject)
|
|
134
|
+
# for ruby 1.8
|
|
135
|
+
class BasicObject #:nodoc:
|
|
136
|
+
instance_methods.each { |m| undef_method m unless m =~ /^__|instance_eval|object_id/ }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class CallContext < BasicObject
|
|
141
|
+
def initialize(bindings)
|
|
142
|
+
@bindings = bindings
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def method_missing(var,*args)
|
|
146
|
+
::Kernel.raise ::Case::UnboundVariable.new unless @bindings.has_key?(var)
|
|
147
|
+
@bindings[var]
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
IS_RUBY_19 = (RUBY_VERSION > "1.9")
|
|
152
|
+
def call(&block)
|
|
153
|
+
return block.call if @bindings.empty?
|
|
154
|
+
context = CallContext.new(@bindings)
|
|
155
|
+
|
|
156
|
+
#http://coderrr.wordpress.com/2009/06/02/fixing-constant-lookup-in-dsls-in-ruby-1-9/
|
|
157
|
+
#http://coderrr.wordpress.com/2009/05/18/dynamically-adding-a-constant-nesting-in-ruby-1-9/
|
|
158
|
+
if IS_RUBY_19
|
|
159
|
+
# what a fail
|
|
160
|
+
l = lambda { context.instance_eval(&block) }
|
|
161
|
+
modules = block.binding.eval "Module.nesting"
|
|
162
|
+
modules.reverse.inject(l) {|l, k| lambda { k.class_eval(&l) } }.call
|
|
163
|
+
else
|
|
164
|
+
context.instance_eval &block
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def nest(object,pattern)
|
|
169
|
+
@stack.push(object)
|
|
170
|
+
@pattern_stack.push(pattern)
|
|
171
|
+
if pattern
|
|
172
|
+
pattern.match(self)
|
|
173
|
+
else
|
|
174
|
+
yield
|
|
175
|
+
end
|
|
176
|
+
@stack.pop
|
|
177
|
+
@pattern_stack.pop
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class Clause
|
|
182
|
+
def initialize(pattern,action)
|
|
183
|
+
raise "badarg" unless Pattern === pattern
|
|
184
|
+
@pattern = pattern
|
|
185
|
+
@action = action
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def match(object)
|
|
189
|
+
context = MatchContext.new(@pattern,object)
|
|
190
|
+
@pattern.match(context)
|
|
191
|
+
@action.nil? ? true : context.call(&@action)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def self.pattern(&block)
|
|
196
|
+
pat = nil
|
|
197
|
+
Case.new {
|
|
198
|
+
pat = is(self.instance_eval(&block))
|
|
199
|
+
of(pat) # dummy clause to prevent raising error
|
|
200
|
+
}
|
|
201
|
+
pat
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def initialize(&block)
|
|
205
|
+
@clauses = []
|
|
206
|
+
self.instance_eval(&block)
|
|
207
|
+
raise NoClauses if @clauses.empty?
|
|
208
|
+
self
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def of(o,&action)
|
|
212
|
+
case o
|
|
213
|
+
when StarPattern
|
|
214
|
+
raise "badarg: star pattern only allowed in structural patterns."
|
|
215
|
+
when Pattern
|
|
216
|
+
pattern = o
|
|
217
|
+
else
|
|
218
|
+
pattern = is(o)
|
|
219
|
+
end
|
|
220
|
+
@clauses << Clause.new(pattern,action)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# this can be used to coerce literal values into pattern
|
|
224
|
+
def is(o,var=nil,&guard)
|
|
225
|
+
case o
|
|
226
|
+
when Pattern
|
|
227
|
+
#bind(o,var,&guard)
|
|
228
|
+
o # return a is
|
|
229
|
+
when Regexp
|
|
230
|
+
regexp = o
|
|
231
|
+
string(regexp,var,&guard)
|
|
232
|
+
when Array
|
|
233
|
+
array(o,var)
|
|
234
|
+
when Range
|
|
235
|
+
integer(o,var,&guard)
|
|
236
|
+
when Hash
|
|
237
|
+
hash(o,var,&guard)
|
|
238
|
+
when Class
|
|
239
|
+
a(o,var,&guard)
|
|
240
|
+
else
|
|
241
|
+
literal(o,var,&guard)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def literal(val,var=nil,&guard)
|
|
246
|
+
matcher = lambda { |obj|
|
|
247
|
+
obj == val
|
|
248
|
+
}
|
|
249
|
+
Pattern.new(matcher,guard,var,"Literal(#{val})")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def a(klass,var=nil,&guard)
|
|
253
|
+
# TODO should assert var to be symbol
|
|
254
|
+
matcher = lambda { |o|
|
|
255
|
+
o.is_a?(klass)
|
|
256
|
+
}
|
|
257
|
+
Pattern.new(matcher,guard,var,"Class(#{klass})")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def integer(o=nil,var=nil,&guard)
|
|
261
|
+
case o
|
|
262
|
+
when Integer
|
|
263
|
+
literal(o,var,&guard)
|
|
264
|
+
when Range
|
|
265
|
+
range = o
|
|
266
|
+
matcher_lambda = lambda { |o|
|
|
267
|
+
range.include?(o)
|
|
268
|
+
}
|
|
269
|
+
Pattern.new(matcher_lambda,guard,var,"Integer(#{range})")
|
|
270
|
+
when Array
|
|
271
|
+
set = Set.new(o)
|
|
272
|
+
matcher_lambda = lambda { |o|
|
|
273
|
+
set.include?(o)
|
|
274
|
+
}
|
|
275
|
+
Pattern.new(matcher_lambda,guard,var,"Integer(#{o.join(",")})")
|
|
276
|
+
when nil
|
|
277
|
+
a(Integer,var,&guard)
|
|
278
|
+
else
|
|
279
|
+
raise "badarg"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def symbol(o=nil,var=nil,&guard)
|
|
284
|
+
case o
|
|
285
|
+
when Symbol
|
|
286
|
+
literal(o.to_sym,var,&guard)
|
|
287
|
+
when Regexp
|
|
288
|
+
re = o
|
|
289
|
+
matcher_lambda = lambda { |o|
|
|
290
|
+
o.is_a?(Symbol) && o.to_s =~ re
|
|
291
|
+
}
|
|
292
|
+
Pattern.new(matcher_lambda,guard,var,"Symbol")
|
|
293
|
+
when nil
|
|
294
|
+
a(Symbol,var,&guard)
|
|
295
|
+
else
|
|
296
|
+
raise "badarg"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def string(o=nil,var=nil,&guard)
|
|
301
|
+
case o
|
|
302
|
+
when String
|
|
303
|
+
literal(o.to_s,var,&guard)
|
|
304
|
+
when Regexp
|
|
305
|
+
re = o
|
|
306
|
+
matcher_lambda = lambda { |o|
|
|
307
|
+
o.is_a?(String) && o.to_s =~ re
|
|
308
|
+
}
|
|
309
|
+
Pattern.new(matcher_lambda,guard,var,"String")
|
|
310
|
+
when nil
|
|
311
|
+
a(String,var,&guard)
|
|
312
|
+
else
|
|
313
|
+
raise "badarg"
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def bind(o,var,&guard)
|
|
318
|
+
Pattern.new(is(o),guard,var)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def one_of(patterns,var=nil,&guard)
|
|
322
|
+
patterns = patterns.map { |o|
|
|
323
|
+
case o
|
|
324
|
+
when Pattern, StarPattern
|
|
325
|
+
o
|
|
326
|
+
else
|
|
327
|
+
is(o) # coerce into pattern
|
|
328
|
+
end
|
|
329
|
+
}
|
|
330
|
+
matcher = lambda { |o,context|
|
|
331
|
+
r = false
|
|
332
|
+
patterns.each { |pat|
|
|
333
|
+
begin
|
|
334
|
+
context.nest(o,pat)
|
|
335
|
+
r = true
|
|
336
|
+
break
|
|
337
|
+
rescue NoMatch
|
|
338
|
+
next
|
|
339
|
+
end
|
|
340
|
+
}
|
|
341
|
+
return r
|
|
342
|
+
}
|
|
343
|
+
Pattern.new(matcher,guard,var,"OneOf")
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def array(os,var=nil,&guard)
|
|
347
|
+
# build structrual pattern
|
|
348
|
+
patterns = os.map { |o|
|
|
349
|
+
case o
|
|
350
|
+
when Pattern, StarPattern
|
|
351
|
+
o
|
|
352
|
+
else
|
|
353
|
+
is(o) # coerce into pattern
|
|
354
|
+
end
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#allow star pattern only for the last position
|
|
358
|
+
star_pattern = nil
|
|
359
|
+
# this works with 1.8.6
|
|
360
|
+
patterns.each_with_index { |pattern,i|
|
|
361
|
+
# allows star pattern only at the end of the pattern
|
|
362
|
+
if pattern.is_a?(StarPattern)
|
|
363
|
+
unless i == patterns.length - 1
|
|
364
|
+
raise "badarg: star pattern only allowed at the end of the array."
|
|
365
|
+
end
|
|
366
|
+
star_pattern = pattern
|
|
367
|
+
end
|
|
368
|
+
}
|
|
369
|
+
if star_pattern
|
|
370
|
+
patterns = patterns[0..-2]
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
matcher = lambda { |o,context|
|
|
374
|
+
return false unless o.is_a?(Array)
|
|
375
|
+
if star_pattern
|
|
376
|
+
# this is an array pattern with star pattern to match tial
|
|
377
|
+
context.fail("not enough elements") if patterns.length > o.length + 1
|
|
378
|
+
else
|
|
379
|
+
# no star pattern
|
|
380
|
+
context.fail("not enough elements") if patterns.length != o.length
|
|
381
|
+
end
|
|
382
|
+
# match mandatory elements
|
|
383
|
+
patterns.each_with_index { |pattern,i|
|
|
384
|
+
context.nest(o[i],pattern)
|
|
385
|
+
}
|
|
386
|
+
# match tail
|
|
387
|
+
if star_pattern
|
|
388
|
+
tail = o[patterns.size..-1]
|
|
389
|
+
tail.each do |tail_element|
|
|
390
|
+
context.nest(tail_element,star_pattern.pattern)
|
|
391
|
+
end
|
|
392
|
+
return false if star_pattern.guard && star_pattern.guard.call(tail) == false
|
|
393
|
+
context.bind(star_pattern.variable,tail) if star_pattern.variable
|
|
394
|
+
end
|
|
395
|
+
true
|
|
396
|
+
}
|
|
397
|
+
Pattern.new(matcher,guard,var,"Array")
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# ["key"] => optional key
|
|
401
|
+
## /regexp/ => across all key that matches
|
|
402
|
+
# literal => required key
|
|
403
|
+
# bleh..
|
|
404
|
+
def hash(hash=nil,var=nil,&guard)
|
|
405
|
+
# coerce literals into patterns
|
|
406
|
+
patterns = hash.to_a.map! { |(k,v)|
|
|
407
|
+
[k,(v.is_a?(Pattern) ? v : is(v))]
|
|
408
|
+
}
|
|
409
|
+
matcher = lambda { |h,context|
|
|
410
|
+
context.fail unless h.is_a?(Hash)
|
|
411
|
+
patterns.each { |(k,value_pattern)|
|
|
412
|
+
case k
|
|
413
|
+
when Array
|
|
414
|
+
# optional key
|
|
415
|
+
k = k.first
|
|
416
|
+
# try matching iff the value is non-nil
|
|
417
|
+
if value=h[k]
|
|
418
|
+
context.nest(value,value_pattern)
|
|
419
|
+
end
|
|
420
|
+
# regexp match is a bit silly...
|
|
421
|
+
# when Regexp
|
|
422
|
+
# # pattern applies to all keys that matches regexp
|
|
423
|
+
# re = k
|
|
424
|
+
# hash.keys.each do |k|
|
|
425
|
+
# if k =~ re
|
|
426
|
+
# context.nest(h[k],value_pattern)
|
|
427
|
+
# end
|
|
428
|
+
# end
|
|
429
|
+
|
|
430
|
+
else
|
|
431
|
+
# required key
|
|
432
|
+
context.fail("no required key: #{k}") unless h.has_key?(k)
|
|
433
|
+
context.nest(h[k],value_pattern)
|
|
434
|
+
end
|
|
435
|
+
}
|
|
436
|
+
true
|
|
437
|
+
}
|
|
438
|
+
Pattern.new(matcher,guard,var,"Hash")
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def _(var=nil,&guard)
|
|
442
|
+
matcher = lambda { |o| true }
|
|
443
|
+
Pattern.new(matcher, guard, var)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def _!(pattern,var=nil,&guard)
|
|
447
|
+
pattern = is(pattern) unless Pattern === pattern
|
|
448
|
+
StarPattern.new(pattern,guard,var)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def match(o)
|
|
452
|
+
errors = []
|
|
453
|
+
@clauses.each { |c|
|
|
454
|
+
begin
|
|
455
|
+
return c.match(o)
|
|
456
|
+
rescue NoMatch
|
|
457
|
+
errors << $!
|
|
458
|
+
end
|
|
459
|
+
}
|
|
460
|
+
raise CaseFail.new(errors)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def to_s
|
|
464
|
+
"#<#{self.class}>"
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def inspect
|
|
468
|
+
self.to_s
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
module MatchMaker
|
|
473
|
+
Case = ::Case
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def Case(obj,&block)
|
|
477
|
+
Case.new(&block).match(obj)
|
|
478
|
+
end
|
|
479
|
+
|