matchete 0.0.1 → 0.0.2
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 +103 -1
- data/lib/matchete.rb +78 -50
- data/lib/matchete/exceptions.rb +7 -0
- data/matchete.gemspec +1 -1
- data/spec/matchete_spec.rb +174 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d82680012c34cb8d63c1e461711b34727036a455
|
4
|
+
data.tar.gz: a23af03a534d39a1295855a959eb362ceebbda82
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 29d6a30c0274d6fbe59c79dcbbc1c78c5359f76bddc2791c8afeb8980e3219bcc44e6eebe837997e4644497fd9f50dc89f7503130c3488a33757a787f2e9d4bf
|
7
|
+
data.tar.gz: 9fae0969b2f2aa630d3608d38db7c91fca1a56f32ca423a0aacfbb11952bf1db45bf025a510faf96694a702e280007cf59468719bbc8138be15f7fa64385b076
|
data/README.md
CHANGED
@@ -1 +1,103 @@
|
|
1
|
-
|
1
|
+
Matchete
|
2
|
+
=========
|
3
|
+
|
4
|
+
Matchete provides a DSL for method overloading based on pattern matching for Ruby.
|
5
|
+
|
6
|
+
It's just a quick hack inspired by weissbier and the use-return-values-of-method-definitions DSL technique used in [harmonic](https://github.com/s2gatev/harmonic)
|
7
|
+
|
8
|
+
Install
|
9
|
+
-----
|
10
|
+
`gem install matchete`
|
11
|
+
|
12
|
+
Usage
|
13
|
+
-----
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
require 'matchete'
|
17
|
+
|
18
|
+
class FactorialStrikesAgain
|
19
|
+
include Matchete
|
20
|
+
|
21
|
+
on 1,
|
22
|
+
def factorial(value)
|
23
|
+
1
|
24
|
+
end
|
25
|
+
|
26
|
+
on -> x { x > 1 },
|
27
|
+
def factorial(value)
|
28
|
+
value * factorial(value - 1)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
FactorialStrikesAgain.new.factorial(4) #24
|
33
|
+
FactorialStrikesAgain.new.factorial(-2) #Matchete::NotResolvedError No matching factorial method for args [-2]
|
34
|
+
```
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
require 'matchete'
|
38
|
+
|
39
|
+
class Converter
|
40
|
+
include Matchete
|
41
|
+
|
42
|
+
on Integer,
|
43
|
+
def convert(value)
|
44
|
+
[:integer, value]
|
45
|
+
end
|
46
|
+
|
47
|
+
on Hash,
|
48
|
+
def convert(values)
|
49
|
+
[:dict, values.map { |k, v| [convert(k), convert(v)] }]
|
50
|
+
end
|
51
|
+
|
52
|
+
on /reserved_/,
|
53
|
+
def convert(value)
|
54
|
+
[:reserved_symbol, value]
|
55
|
+
end
|
56
|
+
|
57
|
+
on String,
|
58
|
+
def convert(value)
|
59
|
+
[:string, value]
|
60
|
+
end
|
61
|
+
|
62
|
+
on ['deleted', [Integer, Any]],
|
63
|
+
def convert(value)
|
64
|
+
['deleted', value[1]]
|
65
|
+
end
|
66
|
+
|
67
|
+
on :not_implemented?,
|
68
|
+
def convert(value)
|
69
|
+
[:fail, value]
|
70
|
+
end
|
71
|
+
|
72
|
+
on free: Integer, method:
|
73
|
+
def convert(free:)
|
74
|
+
[:rofl, free]
|
75
|
+
end
|
76
|
+
|
77
|
+
default def convert(value)
|
78
|
+
[:z, value]
|
79
|
+
end
|
80
|
+
|
81
|
+
def not_implemented?(value)
|
82
|
+
value.is_a? Symbol
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
converter = Converter.new
|
87
|
+
p converter.convert(2) #[:integer, 2]
|
88
|
+
p converter.convert({2 => 4}) #[:dict, [[[:integer, 2], [:integer, 4]]]
|
89
|
+
p converter.convert('reserved_l') #[;reserved_symbol, 'l']
|
90
|
+
p converter.convert('zaza') #[:string, 'zaza']
|
91
|
+
p converter.convert(['deleted', [2, Array]]) #['deleted', [2, Array]]
|
92
|
+
p converter.convert(:f) #[:fail, :f]
|
93
|
+
p converter.convert(free: 2) #[:rofl, 2]
|
94
|
+
p converter.convert(2.2) #[:z, 2.2]
|
95
|
+
```
|
96
|
+
|
97
|
+
Todo
|
98
|
+
-----
|
99
|
+
|
100
|
+
Copyright
|
101
|
+
-----
|
102
|
+
|
103
|
+
Copyright (c) 2014 Alexander Ivanov. See LICENSE for further details.
|
data/lib/matchete.rb
CHANGED
@@ -1,31 +1,38 @@
|
|
1
1
|
require 'set'
|
2
|
+
require_relative 'matchete/exceptions'
|
2
3
|
|
3
4
|
module Matchete
|
4
|
-
class NotResolvedError < StandardError
|
5
|
-
end
|
6
|
-
|
7
5
|
def self.included(klass)
|
8
6
|
klass.extend ClassMethods
|
9
|
-
klass.instance_variable_set "@
|
10
|
-
klass.instance_variable_set "@
|
7
|
+
klass.instance_variable_set "@methods", {}
|
8
|
+
klass.instance_variable_set "@default_methods", {}
|
11
9
|
end
|
12
10
|
|
13
11
|
Any = -> (x) { true }
|
14
12
|
None = -> (x) { false }
|
15
13
|
|
16
14
|
module ClassMethods
|
17
|
-
def on(*
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
def on(*args, **kwargs)
|
16
|
+
if kwargs.count.zero?
|
17
|
+
*guard_args, method_name = args
|
18
|
+
guard_kwargs = {}
|
19
|
+
else
|
20
|
+
method_name = kwargs[:method]
|
21
|
+
kwargs.delete :method
|
22
|
+
guard_args = args
|
23
|
+
guard_kwargs = kwargs
|
24
|
+
end
|
25
|
+
@methods[method_name] ||= []
|
26
|
+
@methods[method_name] << [guard_args, guard_kwargs, instance_method(method_name)]
|
27
|
+
convert_to_matcher method_name
|
21
28
|
end
|
22
29
|
|
23
30
|
def default(method_name)
|
24
|
-
@
|
31
|
+
@default_methods[method_name] = instance_method(method_name)
|
25
32
|
convert_to_matcher method_name
|
26
33
|
end
|
27
34
|
|
28
|
-
def
|
35
|
+
def supporting(*method_names)
|
29
36
|
-> object do
|
30
37
|
method_names.all? do |method_name|
|
31
38
|
object.respond_to? method_name
|
@@ -33,51 +40,72 @@ module Matchete
|
|
33
40
|
end
|
34
41
|
end
|
35
42
|
|
36
|
-
def convert_to_matcher(
|
37
|
-
define_method(
|
38
|
-
|
39
|
-
self.class.match_guards guards, args
|
40
|
-
end
|
41
|
-
|
42
|
-
handler = if guards.nil?
|
43
|
-
default_method = self.class.instance_variable_get('@default_functions')[function]
|
44
|
-
if default_method
|
45
|
-
default_method
|
46
|
-
else
|
47
|
-
raise NotResolvedError.new("not resolved #{function} with #{args}")
|
48
|
-
end
|
49
|
-
else
|
50
|
-
guards[1]
|
51
|
-
end
|
52
|
-
|
53
|
-
handler.bind(self).call *args
|
43
|
+
def convert_to_matcher(method_name)
|
44
|
+
define_method(method_name) do |*args, **kwargs|
|
45
|
+
call_overloaded(method_name, args: args, kwargs: kwargs)
|
54
46
|
end
|
55
47
|
end
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
48
|
+
end
|
49
|
+
|
50
|
+
def call_overloaded(method_name, args: [], kwargs: {})
|
51
|
+
handler = find_handler(method_name, args, kwargs)
|
52
|
+
if handler.parameters.any? do |type, _|
|
53
|
+
[:key, :keyrest, :keyreq].include? type
|
60
54
|
end
|
55
|
+
handler.bind(self).call *args, **kwargs
|
56
|
+
else
|
57
|
+
handler.bind(self).call *args
|
58
|
+
end
|
59
|
+
#insane workaround, because if you have
|
60
|
+
#def z(f);end
|
61
|
+
#and you call it like that
|
62
|
+
#a(2, **{e: 4})
|
63
|
+
#it raises wrong number of arguments (2 for 1)
|
64
|
+
#clean later
|
65
|
+
end
|
66
|
+
|
67
|
+
def find_handler(method_name, args, kwargs)
|
68
|
+
guards = self.class.instance_variable_get('@methods')[method_name].find do |guard_args, guard_kwargs, _|
|
69
|
+
match_guards guard_args, guard_kwargs, args, kwargs
|
61
70
|
end
|
62
71
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
when Proc
|
70
|
-
guard.call arg
|
71
|
-
when String
|
72
|
-
guard == arg
|
73
|
-
when Regexp
|
74
|
-
arg.is_a? String and guard.match arg
|
75
|
-
when Array
|
76
|
-
arg.is_a?(Array) and
|
77
|
-
guard.zip(arg).all? { |child_guard, child| match_guard child_guard, child }
|
78
|
-
else
|
79
|
-
guard == arg
|
72
|
+
if guards.nil?
|
73
|
+
default_method = self.class.instance_variable_get('@default_methods')[method_name]
|
74
|
+
if default_method
|
75
|
+
default_method
|
76
|
+
else
|
77
|
+
raise NotResolvedError.new("No matching #{method_name} method for args #{args}")
|
80
78
|
end
|
79
|
+
else
|
80
|
+
guards[2]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def match_guards(guard_args, guard_kwargs, args, kwargs)
|
85
|
+
return false if guard_args.count != args.count || guard_kwargs.count != kwargs.count
|
86
|
+
guard_args.zip(args).all? do |guard, arg|
|
87
|
+
match_guard guard, arg
|
88
|
+
end and
|
89
|
+
guard_kwargs.all? do |label, guard|
|
90
|
+
match_guard guard, kwargs[label]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def match_guard(guard, arg)
|
95
|
+
case guard
|
96
|
+
when Module
|
97
|
+
arg.is_a? guard
|
98
|
+
when Symbol
|
99
|
+
send guard, arg
|
100
|
+
when Proc
|
101
|
+
guard.call arg
|
102
|
+
when Regexp
|
103
|
+
arg.is_a? String and guard.match arg
|
104
|
+
when Array
|
105
|
+
arg.is_a?(Array) and
|
106
|
+
guard.zip(arg).all? { |child_guard, child| match_guard child_guard, child }
|
107
|
+
else
|
108
|
+
guard == arg
|
81
109
|
end
|
82
110
|
end
|
83
111
|
end
|
data/matchete.gemspec
CHANGED
data/spec/matchete_spec.rb
CHANGED
@@ -69,4 +69,177 @@ describe Matchete do
|
|
69
69
|
a.play([2, 2.2]).should eq [:integer, :float]
|
70
70
|
a.play([[2], Matchete]).should eq :s
|
71
71
|
end
|
72
|
-
|
72
|
+
|
73
|
+
it 'can use a pattern based on exact values' do
|
74
|
+
class A
|
75
|
+
include Matchete
|
76
|
+
|
77
|
+
on 2, Integer,
|
78
|
+
def play(value, obj)
|
79
|
+
2
|
80
|
+
end
|
81
|
+
|
82
|
+
on 4, Integer,
|
83
|
+
def play(value, obj)
|
84
|
+
4
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
a = A.new
|
89
|
+
a.play(2, 4).should eq 2
|
90
|
+
a.play(4, 4).should eq 4
|
91
|
+
-> { a.play(8, 2) }.should raise_error(Matchete::NotResolvedError)
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'can use a pattern based on regexes' do
|
95
|
+
class A
|
96
|
+
include Matchete
|
97
|
+
|
98
|
+
on /z/,
|
99
|
+
def play(value)
|
100
|
+
'z'
|
101
|
+
end
|
102
|
+
|
103
|
+
on /y/,
|
104
|
+
def play(value)
|
105
|
+
'y'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
a = A.new
|
110
|
+
a.play('zewr').should eq 'z'
|
111
|
+
a.play('yy').should eq 'y'
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'can use a default method when everything else fails' do
|
115
|
+
class A
|
116
|
+
include Matchete
|
117
|
+
|
118
|
+
on Integer,
|
119
|
+
def play(value)
|
120
|
+
:integer
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
-> { A.new.play(2.2) }.should raise_error(Matchete::NotResolvedError)
|
125
|
+
|
126
|
+
class A
|
127
|
+
default def play(value)
|
128
|
+
:else
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
A.new.play(2.2).should eq :else
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'can use a pattern based on existing predicate methods given as symbols' do
|
136
|
+
class A
|
137
|
+
include Matchete
|
138
|
+
|
139
|
+
on :even?,
|
140
|
+
def play(value)
|
141
|
+
value
|
142
|
+
end
|
143
|
+
|
144
|
+
def even?(value)
|
145
|
+
value.remainder(2).zero? #so gay and gay
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
A.new.play(2).should eq 2
|
150
|
+
-> { A.new.play(5) }.should raise_error(Matchete::NotResolvedError)
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'can use a pattern based on a lambda predicate' do
|
154
|
+
class A
|
155
|
+
include Matchete
|
156
|
+
|
157
|
+
on -> x { x % 2 == 0 },
|
158
|
+
def play(value)
|
159
|
+
value
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
A.new.play(2).should eq 2
|
164
|
+
-> { A.new.play(7) }.should raise_error(Matchete::NotResolvedError)
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'can use a pattern based on responding to methods' do
|
168
|
+
class A
|
169
|
+
include Matchete
|
170
|
+
|
171
|
+
on supporting(:map),
|
172
|
+
def play(value)
|
173
|
+
value
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
A.new.play([]).should eq []
|
178
|
+
-> { A.new.play(4) }.should raise_error(Matchete::NotResolvedError)
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'can match on different keyword arguments' do
|
182
|
+
class A
|
183
|
+
include Matchete
|
184
|
+
|
185
|
+
on e: Integer, f: String, method:
|
186
|
+
def play(e:, f:)
|
187
|
+
:y
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
A.new.play(e: 0, f: "y").should eq :y
|
192
|
+
-> { A.new.play(e: "f", f: Class)}.should raise_error(Matchete::NotResolvedError)
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'can match on multiple different kinds of patterns' do
|
196
|
+
class A
|
197
|
+
include Matchete
|
198
|
+
end
|
199
|
+
|
200
|
+
A.new.match_guards([Integer, Float], {}, [8, 8.8], {}).should be_true
|
201
|
+
end
|
202
|
+
|
203
|
+
describe '#match_guard' do
|
204
|
+
before :all do
|
205
|
+
class A
|
206
|
+
include Matchete
|
207
|
+
|
208
|
+
def even?(value)
|
209
|
+
value.remainder(2).zero?
|
210
|
+
end
|
211
|
+
end
|
212
|
+
@a = A.new
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'matches modules and classes' do
|
216
|
+
@a.match_guard(Integer, 2).should be_true
|
217
|
+
@a.match_guard(Class, 4).should be_false
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'matches methods given as symbols' do
|
221
|
+
@a.match_guard(:even?, 2).should be_true
|
222
|
+
-> { @a.match_guard(:odd?, 4) }.should raise_error
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'matches predicates given as lambdas' do
|
226
|
+
@a.match_guard(-> x { x == {} }, {}).should be_true
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'matches on regex' do
|
230
|
+
@a.match_guard(/a/, 'aw').should be_true
|
231
|
+
@a.match_guard(/z/, 'lol').should be_false
|
232
|
+
end
|
233
|
+
|
234
|
+
it 'matches on nested arrays' do
|
235
|
+
@a.match_guard([Integer, [:even?]], [2, [4]]).should be_true
|
236
|
+
@a.match_guard([Float, [:even?]], [2.2, [7]]).should be_false
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'matches on exact values' do
|
240
|
+
@a.match_guard(2, 2).should be_true
|
241
|
+
@a.match_guard('d', 'f').should be_false
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: matchete
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexander Ivanov
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- README.md
|
37
37
|
- Rakefile
|
38
38
|
- lib/matchete.rb
|
39
|
+
- lib/matchete/exceptions.rb
|
39
40
|
- matchete.gemspec
|
40
41
|
- spec/matchete_spec.rb
|
41
42
|
- spec/spec_helper.rb
|