patme 0.1.0 → 0.1.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.
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