eager_beaver 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.
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