patme 0.1.0 → 0.1.1

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: f270e55081adb2c27a467a59a88dcce06996a14f
4
- data.tar.gz: 5fb92273c02545651cb7b0b2033c61c5f87c7f6a
3
+ metadata.gz: 34560aa2680a0d13d4447e3ce353dacfb6e69e8a
4
+ data.tar.gz: f2a5fd24660340288c657f2bd1905033944335cb
5
5
  SHA512:
6
- metadata.gz: 60c5a51bb417bf20fe806040ab9f1e14428e4aa57021f134ddc007154fc69b955856aa97942abdf18fb6619fff21def8161f91fbaaf62e310ffe4ca846bcf184
7
- data.tar.gz: 8d5b443bab40c29f57b1982931f0827d1a53227e9824b9fbcbf3b2606f3883480d9b48390691312d61f729abb162d65f488dbc895406a13c7b6827aada0c52ef
6
+ metadata.gz: 5063c52ccbd5e41b766b8bcd648139db2fb2cd51faeef3078e28045a6d7e4b03fe1d366247938f23d1c4b901ac13b94cfc1a395435dfddf31841333bd30489bc
7
+ data.tar.gz: 6e3f90a826db1cd5676aaa8cb6c6cc441f8b1b1895ee2875c6f033016f4c6ae90fb0967882b954fe3125b20136be8c991fee4ae07e6688d26b3da28bd164cc2a
data/README.md CHANGED
@@ -2,24 +2,18 @@
2
2
 
3
3
  [![Build Status](https://travis-ci.org/vizvamitra/patme.svg?branch=master)](https://travis-ci.org/vizvamitra/patme)
4
4
 
5
- This gem is my experiment on elixir-style pattern matching in ruby.
5
+ This gem is my experiment on elixir-style pattern matching in ruby. In it's current state it is no more than a proof of concept and I suggest not to use it in production.
6
6
 
7
- The implementation is neither production-ready, nor complete. It is just a proof of concept.
8
7
 
9
8
  ## Info
10
9
 
11
- Patme module stores your instance methods internally and removes them from your class. Then, when method is called, it tries to pattern-match given arguments with existing method implementations. If implementation was found, it executes it, othervise you'll get a NoMethodError.
10
+ Patme module stores your instance methods internally and removes them from your class. Then, when method is called, it tries to pattern-match given arguments with existing method implementations. If implementation was found, it executes it, otherwise you'll get a NoMethodError.
12
11
 
13
12
  Currently this gem supports 3 types of arguments: specific, arbitrary and optional. In method definition `def foo(agr1, arg2=1, _arg3=false)` arg1 is an arbitrary argument, arg2 is specific and arg3 is optional.
14
13
 
15
14
  Patme supports class-based pattern matching. If method's specific argument is a `Class`, then it will be matched based on given argument's class using `is_a?`
16
15
 
17
16
 
18
- ## Limitations
19
-
20
- Only pattern matching on instance methods is supported. Also you must use parentheses around method arguments.
21
-
22
-
23
17
  ## Usage
24
18
 
25
19
  #### Recursion example
@@ -35,7 +29,7 @@ class Factorial
35
29
  end
36
30
 
37
31
  def calculate(n)
38
- n * self.of(n-1)
32
+ n * calculate(n-1)
39
33
  end
40
34
  end
41
35
 
@@ -50,7 +44,7 @@ factorial_calculator.calculate(-1) # => endless recursion, don't do so ^_^
50
44
  #### Class-based pattern matching
51
45
 
52
46
  ```ruby
53
- require 'parme'
47
+ require 'patme'
54
48
 
55
49
  SeriousError = Class.new(StandardError)
56
50
  RegularError = Class.new(StandardError)
@@ -116,85 +110,138 @@ class MyClass
116
110
 
117
111
  # This method will match only when arg1 ='test'
118
112
  def foo(arg1='test')
119
- "runned foo('test') with #{arg1}"
113
+ "ran foo('test') with #{arg1}"
120
114
  end
121
115
 
122
116
  # Will match only when arg1 ='other'
123
117
  def foo(arg1='other')
124
- "runned foo('other') with #{arg1}"
118
+ "ran foo('other') with #{arg1}"
125
119
  end
126
120
 
127
121
  # You can also use other basic types
128
122
  def foo(arg1={a: 1, b: 2})
129
- "runned foo({a: 1, b: 2}) with #{arg1}"
123
+ "ran foo({a: 1, b: 2}) with #{arg1}"
130
124
  end
131
125
 
132
126
  # Will match when arg2 = 'test' no matter what arg1 is
133
127
  def foo(arg1, arg2='test')
134
- "runned foo(any, 'test') with [#{arg1}, #{arg2}]"
128
+ "ran foo(any, 'test') with [#{arg1}, #{arg2}]"
135
129
  end
136
130
 
137
131
  # Will match with any arg1 and both with and without arg2
138
132
  # if arg2 is not supplied, method will receive arg2 = 'default'
139
133
  def foo(arg1, _arg2="default")
140
- "runned foo(any, optional) with [#{arg1}, #{_arg2}]"
134
+ "ran foo(any, optional) with [#{arg1}, #{_arg2}]"
141
135
  end
142
136
 
143
137
 
144
138
  # Will match with any one argument.
145
139
  def bar(arg1)
146
- "runned bar(any) with #{arg1}"
140
+ "ran bar(any) with #{arg1}"
147
141
  end
148
142
 
149
143
  # Won't ever match because previous definition of bar will be pattern-matched
150
144
  # before this one
151
145
  def bar(arg1='never')
152
- "runned bar('never') with #{arg1}"
146
+ "ran bar('never') with #{arg1}"
153
147
  end
154
148
 
155
149
 
156
150
  def baz(arg1='test')
157
- "runned baz('test') with #{arg1}"
151
+ "ran baz('test') with #{arg1}"
158
152
  end
159
153
  end
160
154
 
161
155
  my_obj = MyClass.new
162
156
 
163
- my_obj.foo('test') # => "runned foo('test') with test"
164
- my_obj.foo('other') # => "runned foo('other') with other"
165
- my_obj.foo({a: 1, b: 2}) # => "runned foo({a: 1, b: 2}) with {:a => 1, :b => 2}"
166
- my_obj.foo(1, 'test') # => "runned foo(any, 'test') with [1, test]"
167
- my_obj.foo(1) # => "runned foo(any, optional) with [1, default]"
168
- my_obj.foo(1, 'some') # => "runned foo(any, optional) with [1, some]"
157
+ my_obj.foo('test') # => "ran foo('test') with test"
158
+ my_obj.foo('other') # => "ran foo('other') with other"
159
+ my_obj.foo({a: 1, b: 2}) # => "ran foo({a: 1, b: 2}) with {:a => 1, :b => 2}"
160
+ my_obj.foo(1, 'test') # => "ran foo(any, 'test') with [1, test]"
161
+ my_obj.foo(1) # => "ran foo(any, optional) with [1, default]"
162
+ my_obj.foo(1, 'some') # => "ran foo(any, optional) with [1, some]"
169
163
 
170
- my_obj.bar(1) # => "runned bar(any) with 1"
171
- my_obj.bar('never') # => "runned bar(any) with never"
164
+ my_obj.bar(1) # => "ran bar(any) with 1"
165
+ my_obj.bar('never') # => "ran bar(any) with never"
172
166
 
173
- my_obj.baz('test') # => "runned baz('test') with test"
167
+ my_obj.baz('test') # => "ran baz('test') with test"
174
168
  my_obj.baz(1) # => NoMethodError
175
169
  ```
176
170
 
177
171
 
172
+ ## Limitations
173
+
174
+ Pattern-matching in it's current implementation is slow and the more implementations of a method you have the slower it would be:
175
+
176
+ ```ruby
177
+ require 'spec_helper'
178
+ require 'benchmark'
179
+
180
+ class BenchmarkDummy
181
+ def no_pm(arg1, arg2)
182
+ ['no_pm', arg1, arg2]
183
+ end
184
+
185
+ include Patme::PatternMatching
186
+
187
+ def pm(arg1='test', arg2='lol')
188
+ ['specific', arg1, arg2]
189
+ end
190
+
191
+ def pm(arg1, arg2)
192
+ ['arbitrary', arg1, arg2]
193
+ end
194
+ end
195
+
196
+ n = 1_000_000
197
+ foo = BenchmarkDummy.new
198
+
199
+ Benchmark.bm do |x|
200
+ x.report('no_pm '){ n.times{foo.no_pm('test', 'lol')} }
201
+ x.report('pm_specific '){ n.times{foo.pm('test', 'lol')} }
202
+ x.report('pm_arbitrary'){ n.times{foo.pm('some', 'arg')} }
203
+ end
204
+
205
+ # user system total real
206
+ # no_pm 0.200000 0.000000 0.200000 ( 0.205747)
207
+ # pm_specific 3.570000 0.000000 3.570000 ( 3.574598)
208
+ # pm_arbitrary 5.210000 0.000000 5.210000 ( 5.210410)
209
+ ```
210
+
211
+ Only pattern matching on instance methods is supported.
212
+
213
+ Including Patme::PatternMatching into your class makes all of the methods below the `include` statement be pattern-matched (I think I'll fix this issue later).
214
+
215
+
178
216
  ## Todos
179
217
 
180
- 1. Add support for keyword arguments (`key:` - arbitrary, `key: value` - specific, `_key: value` - optional)
181
- 2. Add support for class methods
182
- 3. Add something to tell Parme not to pattern-match on given methods
218
+ 1. Add support for class methods
219
+ 2. Add something to tell Patme not to pattern-match on every method
183
220
 
184
221
  ```ruby
185
222
  # Possible example
186
223
  class Hello
187
- include Parme::PatternMatching
224
+ include Patme::PatternMatching
188
225
 
189
- # Comment after method header will tell Parme not to touch this method
226
+ # Comment after method header will tell Patme not to touch this method
190
227
  def tell(name) # ::pm_off
191
228
  puts "Hello, #{name}"
192
229
  end
230
+
231
+ # Or maybe it'll be better to use an annotation
232
+ pm_start
233
+ def yell(name="Boss", message)
234
+ raise CommonSenceError, "don't yell on your boss"
235
+ end
236
+
237
+ def yell(name, message)
238
+ puts message.upcase
239
+ end
240
+ pm_stop
193
241
  end
194
242
  ```
195
243
 
196
- 4. Add support for method headers without parentheses
197
- 5. Add watchers
244
+ 3. Add guards
198
245
 
199
246
  ```ruby
200
247
  # Possible example
@@ -215,7 +262,8 @@ my_obj.baz(1) # => NoMethodError
215
262
  end
216
263
  ```
217
264
 
218
- 6. Your suggestions?
265
+ 4. Add support for keyword arguments (`key:` - arbitrary, `key: value` - specific, `_key: value` - optional)
266
+ 5. Your suggestions?
219
267
 
220
268
 
221
269
  ## Contributing
@@ -226,4 +274,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/vizvam
226
274
  ## License
227
275
 
228
276
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
229
-
@@ -1,5 +1,6 @@
1
1
  Dir[File.join(__dir__, "/patme/arguments/*")].each{|path| require path}
2
2
  Dir[File.join(__dir__, "/patme/*.rb")].each{|path| require path}
3
+ require 'parser/current'
3
4
 
4
5
  module Patme
5
6
 
@@ -7,7 +7,7 @@ module Patme
7
7
 
8
8
  # *given is an array to distinguish cases with no value or nil
9
9
  def get_value(*given)
10
- given.size == 1 ? given[0] : @default_value
10
+ given.size == 1 ? given.first : @default_value
11
11
  end
12
12
 
13
13
  def ==(other)
@@ -1,59 +1,48 @@
1
1
  module Patme
2
2
 
3
- # Gets method's header (for example, def foo(arg1, arg2='spec', _arg3='opt') )
4
- # parses it and creates method implementation with the following arguments:
5
- # arg1 - any argument
6
- # arg2 - specific argument, 'spec'
7
- # _arg3 - optional argument which defaults to 'opt'
3
+ # Parses method's arguments and creates method implementation.
4
+ #
5
+ # For example, if method looks like this:
6
+ #
7
+ # def foo(arg1, arg2='foo', _arg3='bar')
8
+ # # some code
9
+ # end
10
+ #
11
+ # then implementation will be created with the following arguments:
12
+ # arg1 - arbitrary argument
13
+ # arg2 - specific argument, 'foo'
14
+ # _arg3 - optional argument, defaults to 'bar'
8
15
  class ImplementationBuilder
9
16
  def initialize(method_obj)
10
17
  @method_obj = method_obj
11
18
  end
12
19
 
13
20
  def build
14
- values = get_values(method_header, method_params)
15
- args = build_args(method_params, values)
16
-
17
- Patme::Implementation.new(@method_obj, args)
21
+ Patme::Implementation.new(@method_obj, arguments)
18
22
  end
19
23
 
20
24
  private
21
25
 
22
- def get_values(header, params)
23
- return {} if params.size == 0
24
-
25
- regex_str = params.map do |type, name|
26
- case type
27
- when :opt then "#{name}=(?<#{name}>.+)"
28
- when :req
29
- name =~ /^_/ ? "#{name}=(?<#{name}>.+)" : "#{name}"
30
- end
31
- end.join(', ') + '\s*\)\s*$'
32
-
33
- match_data = header.match( Regexp.new(regex_str) )
34
- match_data.names.map do |name|
35
- [name.to_sym, eval(match_data[name.to_s])]
36
- end.to_h
37
- end
26
+ def arguments
27
+ method_ast = Parser::CurrentRuby.parse(@method_obj.source)
28
+ arguments_ast = method_ast.children[1].children
38
29
 
39
- def method_header
40
- path, line = @method_obj.source_location
41
- File.read(path).split("\n")[line - 1]
30
+ arguments_ast.map{|arg_ast| build_argument(arg_ast)}
42
31
  end
43
32
 
44
- def method_params
45
- @method_obj.parameters
46
- end
47
-
48
- def build_args(params, values)
49
- return [] if params.size == 0
50
-
51
- params.map do |type, name|
52
- case type
53
- when :opt
54
- (name =~ /^_/ ? Arguments::Optional : Arguments::Specific).new(values[name])
55
- when :req then Arguments::Arbitrary.new
56
- end
33
+ def build_argument(ast_node)
34
+ name = ast_node.children[0]
35
+ type = ast_node.type
36
+
37
+ case type
38
+ when :optarg
39
+ default_value = eval ast_node.children[1].location.expression.source
40
+ arg_class = name[0] == '_' ? Arguments::Optional : Arguments::Specific
41
+ arg_class.new(default_value)
42
+ when :arg
43
+ Arguments::Arbitrary.new
44
+ else
45
+ raise "Argument #{name} has unsupported argument type: #{type}"
57
46
  end
58
47
  end
59
48
  end
@@ -1,3 +1,3 @@
1
1
  module Patme
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -21,6 +21,8 @@ Gem::Specification.new do |spec|
21
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
22
  spec.require_paths = ["lib"]
23
23
 
24
+ spec.add_runtime_dependency 'parser', "~> 2.4"
25
+
24
26
  spec.add_development_dependency "bundler", "~> 1.14"
25
27
  spec.add_development_dependency "rake", "~> 10.0"
26
28
  spec.add_development_dependency "rspec", "~> 3.0"
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: patme
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitrii Krasnov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-03-06 00:00:00.000000000 Z
11
+ date: 2017-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.4'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement