eager_beaver 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/README.md CHANGED
@@ -1,10 +1,20 @@
1
1
  # EagerBeaver
2
2
 
3
- Facilitates method_missing, respond_to_missing?, and method-generation activities
4
- by providing a simple interface for adding method generators. All related
5
- activities, such as registering with #method_missing and #respond_to_missing?
6
- are handled automatically. Facilitates method name pattern-specific method
7
- generation as well. Generated methods are added to the missing method receiver.
3
+ ## Overview
4
+
5
+ `EagerBeaver` provides an interface for adding `#method_missing`-related abilities
6
+ to a class or module.
7
+
8
+ ## Key Features
9
+
10
+ - Method matchers can be added dynamically and cumulatively, reducing the risk
11
+ of accidentally altering or removing previously-added functionality.
12
+ - Matched methods are automatically reflected in calls to `#respond_to?` and
13
+ `#method`, DRY-ing up code by centralizing method-matching logic.
14
+ - Matched methods are automatically added to the including class/module and
15
+ invoked. Subsequent calls won't trigger `#method_missing`.
16
+ - When a method cannot be matched, `super`'s `#method_missing` is automatically
17
+ invoked.
8
18
 
9
19
  ## Installation
10
20
 
@@ -22,6 +32,12 @@ Or install it yourself as:
22
32
 
23
33
  ## Usage
24
34
 
35
+ ### Inclusion
36
+
37
+ Any class or module which includes `EagerBeaver` will gain the `add_method_matcher`
38
+ pseudo-keyword, which [indirectly] yields an `EagerBeaver::MethodMatcher` to the
39
+ given block:
40
+
25
41
  ```ruby
26
42
  require 'eager_beaver'
27
43
 
@@ -29,27 +45,207 @@ class NeedsMethods
29
45
  include EagerBeaver
30
46
 
31
47
  add_method_matcher do |mm|
32
- mm.matcher = Proc.new do
33
- /\Amake_(\w+)\z/ =~ missing_method_name
34
- @attr_name = Regexp.last_match ? Regexp.last_match[1] : nil
35
- Regexp.last_match
36
- end
37
- mm.new_method_code_maker = Proc.new do
38
- %Q{
39
- def #{missing_method_name}(arg)
40
- puts "#{@attr_name} \#{arg}"
48
+ ...
49
+ end
50
+ end
51
+ ```
52
+
53
+ In this case, the resulting `MethodMatcher` is added to the end of a `MethodMatcher` list
54
+ associated with `NeedsMethods`.
55
+
56
+ Each `MethodMatcher` needs two things: a lambda for matching missing method names
57
+ and a lambda for creating the code for any method names it matches:
58
+
59
+ ```ruby
60
+ add_method_matcher do |mm|
61
+ mm.match = lambda { ... }
62
+ mm.new_method_code = lambda { ...}
63
+ end
64
+ end
65
+ ```
66
+
67
+ ### Matching
68
+
69
+ The `match` lambda should return a true value if the missing method name is one
70
+ can be handled by the `MethodMatcher`. The following example will match
71
+ missing methods of the form `#make_<attr_name>`:
72
+
73
+ ```ruby
74
+ mm.match = lambda {
75
+ /\Amake_(\w+)\z/ =~ context.missing_method_name
76
+ context.attr_name = Regexp.last_match ? Regexp.last_match[1] : nil
77
+ return Regexp.last_match
78
+ }
79
+ ```
80
+
81
+ ### Context
82
+
83
+ As the example shows, each `MethodMatcher` contains a `context` which provides:
84
+
85
+ - the name of the missing method (`context.missing_method_name`)
86
+ - the original method receiver instance (`context.original_receiver`)
87
+ - a place to stash information (`context.<attr_name>` and `context.<attr_name>=`)
88
+
89
+ This `context` is shared between the `match` and `new_method_code` lambdas, and
90
+ is reset between uses of each `MethodMatcher`.
91
+
92
+ ### Code Generation
93
+
94
+ The `new_method_code` lambda should return a string which will create the
95
+ missing method in `NeedsMethods`:
96
+
97
+ ```ruby
98
+ mm.new_method_code = lambda {
99
+ code = %Q{
100
+ def #{context.missing_method_name}(arg)
101
+ puts "method \##{context.missing_method_name} has been called"
102
+ puts "\##{context.missing_method_name} was originally called on #{context.original_receiver}"
103
+ puts "#{context.attr_name} was passed from matching to code generation"
104
+ puts "the current call has arguments: \#{arg}"
105
+ return "result = \#{arg}"
41
106
  end
42
107
  }
43
- end
108
+ return code
109
+ }
110
+ ```
111
+
112
+ As the example shows, it is perfectly reasonable to take advantage of work done
113
+ by the `match` lambda (in this case, the parsing of `<attr_name>`).
114
+
115
+ After the generated code is inserted into `NeedsMethods`, the missing method
116
+ call is resent to the original receiver.
117
+
118
+ ### Complete Example
119
+
120
+ ```ruby
121
+ require 'eager_beaver'
122
+
123
+ class NeedsMethods
124
+ include EagerBeaver
125
+
126
+ add_method_matcher do |mm|
127
+ mm.match = lambda {
128
+ /\Amake_(\w+)\z/ =~ context.missing_method_name
129
+ context.attr_name = Regexp.last_match ? Regexp.last_match[1] : nil
130
+ return Regexp.last_match
131
+ }
132
+
133
+ mm.new_method_code = lambda {
134
+ code = %Q{
135
+ def #{context.missing_method_name}(arg)
136
+ puts "method \##{context.missing_method_name} has been called"
137
+ puts "\##{context.missing_method_name} was originally called on #{context.original_receiver}"
138
+ puts "#{context.attr_name} was passed from matching to code generation"
139
+ puts "the current call has arguments: \#{arg}"
140
+ return "result = \#{arg}"
141
+ end
142
+ }
143
+ return code
144
+ }
44
145
  end
45
146
  end
147
+ ```
148
+
149
+ ## Execution
150
+
151
+ Given the `NeedsMethods` class in the example above, let's work through the
152
+ following code:
153
+
154
+ ```ruby
155
+ nm1 = NeedsMethods.new
156
+ puts nm1.make_thingy(10)
157
+ puts nm1.make_widget("hi")
158
+
159
+ nm2 = NeedsMethods.new
160
+ puts nm2.make_thingy(20)
161
+ puts nm2.make_widget("hello")
162
+
163
+ nm2.dont_make_this
164
+ ```
165
+
166
+ As instances of `NeedsMethods`, `nm1` and `nm2` will automatically hande
167
+ methods of the form `#make_<attr_name>`.
168
+
169
+ The line:
170
+ ```ruby
171
+ puts nm1.make_thingy(10)
172
+ ```
173
+ will trigger `nm1`'s `#method_missing`, which `NeedsMethods` implements thanks to
174
+ `EagerBeaver`. Each `MethodMatcher` associated with `EagerBeaver` is run against
175
+ the method name `make_thingy`, and sure enough one matches. This causes the
176
+ following methods to be inserted to `NeedsMethods`:
177
+ ```ruby
178
+ def make_thingy(arg)
179
+ puts "method #make_thingy has been called"
180
+ puts "#make_thingy was originally called on #<NeedsMethods:0x007fa1bc17f498>"
181
+ puts "thingy was passed from matching to code generation"
182
+ puts "the current call has arguments: #{arg}"
183
+ return "result = #{arg}"
184
+ end
185
+ ```
186
+ and when `#make_thingy` is resent to `nm1`, the existing method is called and
187
+ outputs:
188
+
189
+ > method \#make_thingy has been called<br/>
190
+ > \#make_thingy was originally called on \#\<NeedsMethods:0x007fa1bc17f498\><br/>
191
+ > thingy was passed from matching to code generation<br/>
192
+ > the current call has arguments: 10<br/>
193
+ > result = 10
194
+
195
+ Similarly, the line:
196
+ ```ruby
197
+ puts nm1.make_widget("hi")
198
+ ```
199
+ generates the code:
200
+ ```ruby
201
+ def make_widget(arg)
202
+ puts "method #make_widget has been called"
203
+ puts "#make_widget was originally called on #<NeedsMethods:0x007fa1bc17f498>"
204
+ puts "widget was passed from matching to code generation"
205
+ puts "the current call has arguments: #{arg}"
206
+ return "result = #{arg}"
207
+ end
208
+ ```
209
+ and outputs:
210
+ > method \#make_widget has been called<br/>
211
+ > \#make_widget was originally called on \#\<NeedsMethods:0x007fa1bc17f498\><br/>
212
+ > widget was passed from matching to code generation<br/>
213
+ > the current call has arguments: hi<br/>
214
+ > result = hi
215
+
216
+ Note that the following lines do NOT trigger `#method_missing` because both methods
217
+ have already been added to `NeedsMethods`:
218
+ ```ruby
219
+ puts nm2.make_thingy(20)
220
+ puts nm2.make_widget("hello")
221
+ ```
222
+ This can be seen by examining the identity of the original receiver in the output:
46
223
 
47
- nm = NeedsMethods.new
224
+ > **method \#make_thingy has been called**<br/>
225
+ > **\#make_thingy was originally called on \#\<NeedsMethods:0x007fa1bc17f498\>**<br/>
226
+ > **thingy was passed from matching to code generation**<br/>
227
+ > the current call has arguments: 20<br/>
228
+ > result = 20
48
229
 
49
- nm.make_thingy(10) # thingy 10
50
- nm.make_widget("hi") # widget hi
51
- nm.oh_no! # (NoMethodError)
230
+ > **method \#make_widget has been called**<br/>
231
+ > **\#make_widget was originally called on \#\<NeedsMethods:0x007fa1bc17f498\>**<br/>
232
+ > **widget was passed from matching to code generation**<br/>
233
+ > the current call has arguments: hello<br/>
234
+ > result = hello
235
+
236
+ String substitutions which were part of the generated code body (emphasized)
237
+ reflect the circumstances of the first set of method calls, as opposed to
238
+ those which reflect the current call's argument.
239
+
240
+ Finally, the call:
52
241
  ```
242
+ nm2.dont_make_this
243
+ ```
244
+ will cause `NeedsMethods` to examine all of its `MethodMatcher`s and finally call
245
+ `super`'s `#method_missing`. Because no superclass of `NeedsMethods` handles
246
+ `#dont_make_this`, the output is:
247
+
248
+ > undefined method `dont_make_this' for \#\<NeedsMethods:0x007f8e2b991f90\> (NoMethodError)
53
249
 
54
250
  ## Contributing
55
251
 
data/eager_beaver.gemspec CHANGED
@@ -12,6 +12,8 @@ Gem::Specification.new do |gem|
12
12
  generation as well. Generated methods are added to the missing method receiver.}
13
13
  gem.homepage = "http://github.com/kevinburleigh75/eager_beaver"
14
14
 
15
+ gem.add_development_dependency "rspec", ["~>2.13.0"]
16
+
15
17
  gem.files = `git ls-files`.split($\)
16
18
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
19
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
data/lib/eager_beaver.rb CHANGED
@@ -9,14 +9,12 @@ module EagerBeaver
9
9
 
10
10
  def method_missing(method_name, *args, &block)
11
11
  self.class.method_matchers.each do |method_matcher|
12
- method_matcher.original_caller = self
13
- if method_matcher.match?(method_name)
14
- if method_matcher.new_method
15
- self.class.send(:define_method, method_name, method_matcher.new_method)
16
- elsif method_matcher.new_method_code_maker
17
- method_string = method_matcher.instance_eval &method_matcher.new_method_code_maker
18
- self.class.class_eval method_string, __FILE__, __LINE__ + 1
19
- end
12
+ mm = method_matcher.dup
13
+ self.class.context = mm
14
+ mm.original_receiver = self
15
+ if mm.match?(method_name)
16
+ method_string = mm.evaluate mm.new_method_code_maker
17
+ self.class.class_eval method_string, __FILE__, __LINE__ + 1
20
18
  return self.send(method_name, *args, &block)
21
19
  end
22
20
  end
@@ -36,8 +34,10 @@ module EagerBeaver
36
34
  end
37
35
 
38
36
  def add_method_matcher(&block)
39
- method_matchers << MethodMatcher.new(block)
37
+ method_matchers << MethodMatcher.new(&block)
40
38
  end
39
+
40
+ attr_accessor :context
41
41
  end
42
42
 
43
43
  end
@@ -2,27 +2,59 @@ module EagerBeaver
2
2
 
3
3
  class MethodMatcher
4
4
 
5
- attr_accessor :original_caller
5
+ attr_accessor :original_receiver
6
6
  attr_accessor :matcher
7
- attr_accessor :new_method
8
7
  attr_accessor :new_method_code_maker
9
8
  attr_accessor :missing_method_name
10
9
 
11
- def initialize(block)
10
+ def initialize(&block)
12
11
  block.call(self)
13
12
 
14
- raise ArgumentError, "matcher Proc must be given" \
13
+ raise "matcher must be given" \
15
14
  if matcher.nil?
16
- raise ArgumentError, "exactly one of new_method or new_method_code_maker Proc must be given" \
17
- if (new_method && new_method_code_maker) || (new_method.nil? && new_method_code_maker.nil?)
15
+ raise "matcher lmust be a lambda" \
16
+ unless matcher.lambda?
17
+
18
+ raise "new_method_code_maker must be given" \
19
+ if new_method_code_maker.nil?
20
+ raise "new_method_code_maker must be a lambda" \
21
+ unless new_method_code_maker.lambda?
18
22
 
19
23
  self
20
24
  end
21
25
 
26
+ def match=(lambda_proc)
27
+ self.matcher = lambda_proc
28
+ end
29
+
22
30
  def match?(method_name)
23
31
  self.missing_method_name = method_name.to_s
24
- self.instance_eval &matcher
32
+ return evaluate(matcher)
33
+ end
34
+
35
+ def new_method_code=(lambda_proc)
36
+ self.new_method_code_maker = lambda_proc
25
37
  end
38
+
39
+ def evaluate(inner)
40
+ outer = lambda { |*args|
41
+ args.shift
42
+ inner.call(*args)
43
+ }
44
+ self.instance_eval &outer
45
+ end
46
+
47
+ def method_missing(method_name, *args, &block)
48
+ if /\A(?<attr_name>\w+)=?\z/ =~ method_name
49
+ code = %Q{
50
+ attr_accessor :#{attr_name}
51
+ }
52
+ self.singleton_class.instance_eval code, __FILE__, __LINE__ + 1
53
+ return self.send(method_name, *args, &block)
54
+ end
55
+ super
56
+ end
57
+
26
58
  end
27
59
 
28
60
  end
@@ -1,3 +1,3 @@
1
1
  module EagerBeaver
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe "matcher context" do
4
+
5
+ describe "#missing_method_name" do
6
+ it "provides the name of the missing method" do
7
+ klass = Class.new do
8
+ include EagerBeaver
9
+
10
+ add_method_matcher do |mm|
11
+ mm.matcher = lambda {
12
+ raise context.missing_method_name \
13
+ unless context.missing_method_name == "aaa"
14
+ /\Aaaa\z/ =~ context.missing_method_name
15
+ }
16
+ mm.new_method_code = lambda {
17
+ raise context.missing_method_name \
18
+ unless context.missing_method_name == "aaa"
19
+ %Q{
20
+ def #{context.missing_method_name}
21
+ end
22
+ }
23
+ }
24
+ end
25
+ end
26
+ expect{ klass.new.aaa }.to_not raise_exception
27
+ end
28
+ end
29
+
30
+ describe "#original_receiver" do
31
+ it "provides the orignal method receiver" do
32
+ klass = Class.new do
33
+ include EagerBeaver
34
+
35
+ add_method_matcher do |mm|
36
+ mm.matcher = lambda {
37
+ /\Aaaa\z/ =~ context.missing_method_name
38
+ }
39
+ mm.new_method_code = lambda {
40
+ %Q{
41
+ def #{context.missing_method_name}
42
+ #{context.original_receiver.__id__}
43
+ end
44
+ }
45
+ }
46
+ end
47
+ end
48
+
49
+ instance = klass.new
50
+ instance.aaa.should equal instance.__id__
51
+ end
52
+ end
53
+
54
+ describe "#<attr_name> and #<attr_name>=" do
55
+ it "provide a way to pass data between method matching and code generation" do
56
+ klass = Class.new do
57
+ include EagerBeaver
58
+
59
+ add_method_matcher do |mm|
60
+ mm.matcher = lambda {
61
+ context.my_data = "hello"
62
+ /\Aaaa\z/ =~ context.missing_method_name
63
+ }
64
+ mm.new_method_code = lambda {
65
+ %Q{
66
+ def #{context.missing_method_name}
67
+ "#{context.my_data}"
68
+ end
69
+ }
70
+ }
71
+ end
72
+ end
73
+
74
+ klass.new.aaa.should == "hello"
75
+ end
76
+ end
77
+
78
+ end
@@ -0,0 +1,135 @@
1
+ require 'spec_helper'
2
+
3
+ describe EagerBeaver do
4
+
5
+ describe ".included" do
6
+
7
+ before :each do
8
+ @klass = Class.new do
9
+ include EagerBeaver
10
+ end
11
+ end
12
+
13
+ it "adds Includer.add_method_matcher" do
14
+ expect(@klass.methods).to include :add_method_matcher
15
+ end
16
+
17
+ it "adds Includer.method_matchers" do
18
+ expect(@klass.methods).to include :method_matchers
19
+ end
20
+
21
+ it "adds Includer.context" do
22
+ expect(@klass.methods).to include :context
23
+ end
24
+
25
+ it "adds Includer.context=" do
26
+ expect(@klass.methods).to include :context=
27
+ end
28
+
29
+ it "adds Includer#method_missing" do
30
+ expect(@klass.instance_methods(:false)).to include :method_missing
31
+ end
32
+
33
+ it "adds Includer#respond_to_missing?" do
34
+ expect(@klass.instance_methods(:false)).to include :respond_to_missing?
35
+ end
36
+
37
+ end
38
+
39
+ describe "#add_method_matcher" do
40
+
41
+ it "registers a new method matcher" do
42
+ klass = Class.new do
43
+ include EagerBeaver
44
+
45
+ add_method_matcher do |mm|
46
+ mm.matcher = lambda { true }
47
+ mm.new_method_code = lambda {
48
+ return %Q{
49
+ def #{context.missing_method_name}
50
+ end
51
+ }
52
+ }
53
+ end
54
+ end
55
+
56
+ klass.method_matchers.size.should == 1
57
+ end
58
+
59
+ end
60
+
61
+ describe "#method_missing" do
62
+
63
+ context "method matching" do
64
+
65
+ it "invokes the first matching matcher" do
66
+ klass = Class.new do
67
+ include EagerBeaver
68
+
69
+ add_method_matcher do |mm|
70
+ mm.matcher = lambda { /\Aaaa\z/ =~ context.missing_method_name }
71
+ mm.new_method_code = lambda {
72
+ %Q{
73
+ def #{context.missing_method_name}
74
+ 1
75
+ end
76
+ }
77
+ }
78
+ end
79
+
80
+ add_method_matcher do |mm|
81
+ mm.matcher = lambda { /\Abbb\z/ =~ context.missing_method_name }
82
+ mm.new_method_code = lambda {
83
+ %Q{
84
+ def #{context.missing_method_name}
85
+ 2
86
+ end
87
+ }
88
+ }
89
+ end
90
+
91
+ add_method_matcher do |mm|
92
+ mm.matcher = lambda { /\Abbb\z/ =~ context.missing_method_name }
93
+ mm.new_method_code = lambda {
94
+ %Q{
95
+ def #{context.missing_method_name}
96
+ 3
97
+ end
98
+ }
99
+ }
100
+ end
101
+ end
102
+
103
+ klass.new.bbb.should == 2
104
+ end
105
+
106
+ it "calls super #method_missing if no matcher matches" do
107
+ klass1 = Class.new do
108
+ def method_missing(method_name, *args, &block)
109
+ 10
110
+ end
111
+ end
112
+
113
+ klass2 = Class.new(klass1) do
114
+ include EagerBeaver
115
+
116
+ add_method_matcher do |mm|
117
+ mm.matcher = lambda { /\Aaaa\z/ =~ context.missing_method_name }
118
+ mm.new_method_code = lambda {
119
+ %Q{
120
+ def #{context.missing_method_name}
121
+ 1
122
+ end
123
+ }
124
+ }
125
+ end
126
+ end
127
+
128
+ klass2.new.bbb.should == 10
129
+ end
130
+
131
+ end
132
+
133
+ end
134
+
135
+ end
@@ -0,0 +1,19 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+
12
+ # Run specs in random order to surface order dependencies. If you find an
13
+ # order dependency and want to debug it, you can fix the order by providing
14
+ # the seed, which is printed after each run.
15
+ # --seed 1234
16
+ config.order = 'random'
17
+ end
18
+
19
+ require 'eager_beaver'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eager_beaver
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,8 +9,24 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-04 00:00:00.000000000 Z
13
- dependencies: []
12
+ date: 2013-04-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.13.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 2.13.0
14
30
  description: Facilitates method_missing, respond_to_missing?, and method-generation
15
31
  activities
16
32
  email:
@@ -20,6 +36,7 @@ extensions: []
20
36
  extra_rdoc_files: []
21
37
  files:
22
38
  - .gitignore
39
+ - .rspec
23
40
  - Gemfile
24
41
  - LICENSE
25
42
  - README.md
@@ -28,6 +45,9 @@ files:
28
45
  - lib/eager_beaver.rb
29
46
  - lib/eager_beaver/method_matcher.rb
30
47
  - lib/eager_beaver/version.rb
48
+ - spec/eager_beaver/context_spec.rb
49
+ - spec/eager_beaver/eager_beaver_spec.rb
50
+ - spec/spec_helper.rb
31
51
  homepage: http://github.com/kevinburleigh75/eager_beaver
32
52
  licenses: []
33
53
  post_install_message:
@@ -56,4 +76,7 @@ summary: ! 'Facilitates method_missing, respond_to_missing?, and method-generati
56
76
  activities, such as registering with #method_missing and #respond_to_missing? are
57
77
  handled automatically. Facilitates method name pattern-specific method generation
58
78
  as well. Generated methods are added to the missing method receiver.'
59
- test_files: []
79
+ test_files:
80
+ - spec/eager_beaver/context_spec.rb
81
+ - spec/eager_beaver/eager_beaver_spec.rb
82
+ - spec/spec_helper.rb