lazy_methods 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README +37 -0
- data/Rakefile +43 -0
- data/lib/lazy_methods/lazy_methods.rb +82 -0
- data/lib/lazy_methods.rb +2 -0
- data/spec/lazy_method_spec.rb +101 -0
- data/spec/method_tester.rb +31 -0
- metadata +64 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2007 Brian Durand
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
== LazyMethods
|
2
|
+
|
3
|
+
So your Rails application is successful beyond your wildest dreams and now your meager servers are straining under the load. You figure you'll add some caching to your views and all will be well again. Unfortunately, your controller actions load up all sorts of instance variables with records from the database. The caching won't do you much good if your database is still getting slammed. You could move your query logic to the views behind the caching layer, but that just feels icky. Besides, that will break all your tests and you really don't feel like fixing them.
|
4
|
+
|
5
|
+
== Enter LazyMethods.
|
6
|
+
|
7
|
+
This plugin adds a virtual lazy version of every method on every class. Lazy methods have the same name as the original method name but prefixed with "lazy_". A lazy method will return a proxy object that looks and acts just like the result from calling the actual method. The trick is that the actual method will not be called until a method is invoked on the proxy object. This way you can continue to set up the business logic in your controller and have it only actually executed only as needed.
|
8
|
+
|
9
|
+
If you add fragment caching to your views and the cache returns a value and bypasses your view code, the method will never be invoked. Thanks to the magic of Ruby the proxy object will even act like the class it is proxying in class to class and kind_of?
|
10
|
+
|
11
|
+
A simple example:
|
12
|
+
|
13
|
+
The normal way to do it (at least according to every tutorial):
|
14
|
+
|
15
|
+
def index
|
16
|
+
@records = MyRecord.find(:all, :conditions => {:name => params[:name]})
|
17
|
+
end
|
18
|
+
|
19
|
+
And in the view:
|
20
|
+
|
21
|
+
<% cache(params[:names]) -%>
|
22
|
+
<% @records.each do |record| -%>
|
23
|
+
<div><%=record.title%></div>
|
24
|
+
<% end -%>
|
25
|
+
<% end -%>
|
26
|
+
|
27
|
+
Now even if you cache the fragments in your view that use @records, the database will still be hit to select and instantiate all the records. You could remove the code from the action and simple add it back into the view. However, this just feels wrong and is inherently harder to test. Instead just use a lazy method.
|
28
|
+
|
29
|
+
def index
|
30
|
+
@records = MyRecord.lazy_find(:all, :conditions => {:name => params[:name]})
|
31
|
+
end
|
32
|
+
|
33
|
+
Now, as long as no methods are invoked on @records, the original find method will never be called. As soon as the first method is called, the original find method will be called. It will never be called more than once. You can even pass in a block to the lazy method.
|
34
|
+
|
35
|
+
== Testing
|
36
|
+
|
37
|
+
Since the proxy object looks and acts just like the real result object, all your view tests should still pass. Your controller tests should pass will little or no tweaking.
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
|
7
|
+
desc 'Default: run unit tests.'
|
8
|
+
task :default => :test
|
9
|
+
|
10
|
+
desc 'Test the lazy_methods plugin.'
|
11
|
+
Spec::Rake::SpecTask.new(:test) do |t|
|
12
|
+
t.spec_files = 'spec/**/*_spec.rb'
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'Generate documentation for the lazy_methods plugin.'
|
16
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
18
|
+
rdoc.options << '--title' << 'LazyMethods' << '--line-numbers' << '--inline-source' << '--main' << 'README'
|
19
|
+
rdoc.rdoc_files.include('README')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
spec = Gem::Specification.new do |s|
|
24
|
+
s.name = "lazy_methods"
|
25
|
+
s.version = "1.0.2"
|
26
|
+
s.author = "Brian Durand"
|
27
|
+
s.platform = Gem::Platform::RUBY
|
28
|
+
s.summary = "Provide lazy method calls for all methods on every object to aid in caching."
|
29
|
+
s.files = FileList["lib/**/*", "MIT-LICENSE", 'Rakefile'].to_a
|
30
|
+
s.require_path = "lib"
|
31
|
+
s.test_files = FileList["{spec}/**/*.rb"].to_a
|
32
|
+
s.has_rdoc = true
|
33
|
+
s.rdoc_options << '--title' << 'LazyMethods' << '--line-numbers' << '--inline-source' << '--main' << 'README'
|
34
|
+
s.extra_rdoc_files = ["README"]
|
35
|
+
s.homepage = "http://lazymethods.rubyforge.org"
|
36
|
+
s.rubyforge_project = "lazymethods"
|
37
|
+
s.email = 'brian@embellishedvisions.com'
|
38
|
+
end
|
39
|
+
|
40
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
41
|
+
pkg.need_tar = true
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Including this module will provide the lazy method handling for the class where any method can be
|
2
|
+
# prefixed with lazy_ to defer execution. By default, the plugin includes it in Object so it is universally
|
3
|
+
# available.
|
4
|
+
module LazyMethods
|
5
|
+
|
6
|
+
module InstanceMethods
|
7
|
+
def self.included (base)
|
8
|
+
base.send :alias_method, :method_missing_without_lazy, :method_missing
|
9
|
+
base.send :alias_method, :method_missing, :method_missing_with_lazy
|
10
|
+
end
|
11
|
+
|
12
|
+
# Override missing method to add the lazy method handling
|
13
|
+
def method_missing_with_lazy (method, *args, &block)
|
14
|
+
if method.to_s[0, 5] == 'lazy_'
|
15
|
+
method = method.to_s
|
16
|
+
return Proxy.new(self, method[5, method.length].to_sym, args, &block)
|
17
|
+
else
|
18
|
+
# Keep track of the current missing method calls to keep out of an infinite loop
|
19
|
+
stack = Thread.current[:lazy_method_missing_methods] ||= []
|
20
|
+
sig = MethodSignature.new(self, method)
|
21
|
+
raise NoMethodError.new("undefined method `#{method}' for #{self}") if stack.include?(sig)
|
22
|
+
begin
|
23
|
+
stack.push(sig)
|
24
|
+
return method_missing_without_lazy(method, *args, &block)
|
25
|
+
ensure
|
26
|
+
stack.pop
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# This class is used to keep track of methods being called.
|
33
|
+
class MethodSignature
|
34
|
+
|
35
|
+
attr_reader :object, :method
|
36
|
+
|
37
|
+
def initialize (obj, method)
|
38
|
+
@object = obj
|
39
|
+
@method = method
|
40
|
+
end
|
41
|
+
|
42
|
+
def eql? (sig)
|
43
|
+
sig.kind_of(MethodSignature) and sig.object == @object and sig.method == @method
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
# The proxy object does all the heavy lifting.
|
49
|
+
class Proxy
|
50
|
+
# These methods we don't want to override. All other existing methods will be redefined.
|
51
|
+
PROTECTED_METHODS = %w(initialize __proxy_result__ __proxy_loaded__ method_missing)
|
52
|
+
|
53
|
+
def initialize (obj, method, args = nil, &block)
|
54
|
+
@object = obj
|
55
|
+
@method = method
|
56
|
+
@args = args || []
|
57
|
+
@block = block
|
58
|
+
|
59
|
+
# Override already defined methods on Object to proxy them to the result object
|
60
|
+
methods.each do |m|
|
61
|
+
eval "def self.#{m} (*args, &block); __proxy_result__.send(:#{m}, *args, &block); end" unless PROTECTED_METHODS.include?(m)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get the result of the original method call. The original method will only be called once.
|
66
|
+
def __proxy_result__
|
67
|
+
@proxy_result = @object.send(@method, *@args, &@block) unless defined?(@proxy_result)
|
68
|
+
@proxy_result
|
69
|
+
end
|
70
|
+
|
71
|
+
# Helper method that indicates if the proxy has loaded the original method results yet.
|
72
|
+
def __proxy_loaded__
|
73
|
+
!!defined?(@proxy_result)
|
74
|
+
end
|
75
|
+
|
76
|
+
# All missing methods are proxied to the original result object.
|
77
|
+
def method_missing (method, *args, &block)
|
78
|
+
__proxy_result__.send(method, *args, &block)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
data/lib/lazy_methods.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'lazy_methods', 'lazy_methods'))
|
2
|
+
Object.send(:include, LazyMethods::InstanceMethods) unless Object.include?(LazyMethods::InstanceMethods)
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + '/method_tester')
|
4
|
+
|
5
|
+
context "LazyMethods InstanceMethods" do
|
6
|
+
|
7
|
+
setup do
|
8
|
+
@object = MethodTester.new
|
9
|
+
end
|
10
|
+
|
11
|
+
specify "should inject lazy method handling" do
|
12
|
+
proxy = @object.lazy_test("arg")
|
13
|
+
proxy.to_s.should == "ARG"
|
14
|
+
proxy.__proxy_loaded__.should == true
|
15
|
+
end
|
16
|
+
|
17
|
+
specify "should return a proxy object that has not been invoked yet" do
|
18
|
+
proxy = @object.lazy_test("arg")
|
19
|
+
proxy.__proxy_loaded__.should == false
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
context "LazyMethods Proxy" do
|
25
|
+
|
26
|
+
setup do
|
27
|
+
@object = MethodTester.new
|
28
|
+
end
|
29
|
+
|
30
|
+
specify "should be able to wrap a method without executing it" do
|
31
|
+
proxy = @object.lazy_test("arg")
|
32
|
+
@object.test_called.should == 0
|
33
|
+
end
|
34
|
+
|
35
|
+
specify "should execute the wrapped method when it needs to" do
|
36
|
+
proxy = @object.lazy_test("arg")
|
37
|
+
proxy.to_s
|
38
|
+
@object.test_called.should == 1
|
39
|
+
end
|
40
|
+
|
41
|
+
specify "should only execute the wrapped method once" do
|
42
|
+
proxy = @object.lazy_test("arg")
|
43
|
+
proxy.to_s
|
44
|
+
proxy.to_s
|
45
|
+
@object.test_called.should == 1
|
46
|
+
end
|
47
|
+
|
48
|
+
specify "should allow nil as a valid proxied value" do
|
49
|
+
proxy = @object.lazy_test(nil)
|
50
|
+
proxy.should_not
|
51
|
+
@object.test_called.should == 1
|
52
|
+
end
|
53
|
+
|
54
|
+
specify "should allow blocks in the lazy method" do
|
55
|
+
n = 1
|
56
|
+
proxy = @object.lazy_test("arg") do
|
57
|
+
n = 2
|
58
|
+
end
|
59
|
+
n.should == 1
|
60
|
+
proxy.to_s
|
61
|
+
n.should == 2
|
62
|
+
end
|
63
|
+
|
64
|
+
specify "should be indistinguishable from the real object" do
|
65
|
+
proxy = @object.lazy_test("arg")
|
66
|
+
proxy.class.should == String
|
67
|
+
proxy.kind_of?(String).should == true
|
68
|
+
end
|
69
|
+
|
70
|
+
specify "should proxy core methods on Object" do
|
71
|
+
proxy = "xxx".lazy_to_s
|
72
|
+
proxy.should == "xxx"
|
73
|
+
end
|
74
|
+
|
75
|
+
specify "should proxy missing methods" do
|
76
|
+
proxy = @object.lazy_find_test
|
77
|
+
proxy.to_s.should == "FINDER"
|
78
|
+
end
|
79
|
+
|
80
|
+
specify "should allow blocks in the lazy missing methods" do
|
81
|
+
n = 1
|
82
|
+
proxy = @object.lazy_find_test do
|
83
|
+
n = 2
|
84
|
+
end
|
85
|
+
n.should == 1
|
86
|
+
proxy.to_s
|
87
|
+
n.should == 2
|
88
|
+
end
|
89
|
+
|
90
|
+
specify "should not interfere with the proxied object's method_missing" do
|
91
|
+
real = @object.find_test
|
92
|
+
real.to_s.should == "FINDER"
|
93
|
+
end
|
94
|
+
|
95
|
+
specify "should not interfere with real methods that begin with lazy_" do
|
96
|
+
@object.lazy_real_method_called.should == false
|
97
|
+
@object.lazy_real_method
|
98
|
+
@object.lazy_real_method_called.should == true
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# This class is used for testing the lazy_methods plugin functionality.
|
2
|
+
|
3
|
+
class MethodTester
|
4
|
+
|
5
|
+
attr_reader :test_called, :lazy_real_method_called
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@test_called = 0
|
9
|
+
@lazy_real_method_called = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def test (arg)
|
13
|
+
@test_called += 1
|
14
|
+
yield if block_given?
|
15
|
+
arg.upcase if arg
|
16
|
+
end
|
17
|
+
|
18
|
+
def lazy_real_method
|
19
|
+
@lazy_real_method_called = true
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_missing (method, *args, &block)
|
23
|
+
if method.to_s.starts_with?('find_')
|
24
|
+
yield if block_given?
|
25
|
+
"FINDER"
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lazy_methods
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Durand
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-01-17 00:00:00 -06:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: brian@embellishedvisions.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
files:
|
25
|
+
- lib/lazy_methods
|
26
|
+
- lib/lazy_methods.rb
|
27
|
+
- lib/lazy_methods/lazy_methods.rb
|
28
|
+
- MIT-LICENSE
|
29
|
+
- Rakefile
|
30
|
+
- README
|
31
|
+
has_rdoc: true
|
32
|
+
homepage: http://lazymethods.rubyforge.org
|
33
|
+
post_install_message:
|
34
|
+
rdoc_options:
|
35
|
+
- --title
|
36
|
+
- LazyMethods
|
37
|
+
- --line-numbers
|
38
|
+
- --inline-source
|
39
|
+
- --main
|
40
|
+
- README
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project: lazymethods
|
58
|
+
rubygems_version: 1.0.1
|
59
|
+
signing_key:
|
60
|
+
specification_version: 2
|
61
|
+
summary: Provide lazy method calls for all methods on every object to aid in caching.
|
62
|
+
test_files:
|
63
|
+
- spec/lazy_method_spec.rb
|
64
|
+
- spec/method_tester.rb
|