Skip to content

Commit

Permalink
First pass at widget cache. Also took out '&blk' parameter to #to_s s…
Browse files Browse the repository at this point in the history
…ince there were no tests and it made things weird.
  • Loading branch information
alexch committed Dec 21, 2009
1 parent 96e2d31 commit 9c6bdca
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 24 deletions.
1 change: 1 addition & 0 deletions lib/erector.rb
Expand Up @@ -6,6 +6,7 @@
require "erector/raw_string"
require "erector/externals"
require "erector/output"
require "erector/cache"
require "erector/widget"
require "erector/inline"
require "erector/unicode"
Expand Down
30 changes: 30 additions & 0 deletions lib/erector/cache.rb
@@ -0,0 +1,30 @@
module Erector
class Cache
def initialize
@stores = {}
end

def store_for(klass)
@stores[klass] ||= {}
end

def []=(*args)
value = args.pop
klass = args.shift
params = args.first || {}
store_for(klass)[params] = value
end

def [](klass, params = {})
store_for(klass)[params]
end

def delete(klass, params = {})
store_for(klass).delete(params)
end

def delete_all(klass)
@stores.delete(klass)
end
end
end
79 changes: 58 additions & 21 deletions lib/erector/widget.rb
Expand Up @@ -185,6 +185,19 @@ def self.prettyprint_default=(enabled)
@@prettyprint_default = enabled
end

@@cache = nil
def cache
self.class.cache
end

def self.cache
@@cache
end

def self.cache=(c)
@@cache = c
end

NON_NEWLINEY = {'i' => true, 'b' => true, 'small' => true,
'img' => true, 'span' => true, 'a' => true,
'input' => true, 'textarea' => true, 'button' => true, 'select' => true
Expand All @@ -193,12 +206,12 @@ def self.prettyprint_default=(enabled)
RESERVED_INSTANCE_VARS = [:helpers, :assigns, :block, :output, :prettyprint, :indentation]

attr_reader *RESERVED_INSTANCE_VARS
attr_reader :parent
attr_reader :parent # making this reserved causes breakage
attr_writer :block

def initialize(assigns={}, &block)
unless assigns.is_a? Hash
raise "Erector's API has changed. Now you should pass only an options hash into Widget.new; the rest come in via to_s, or by using #widget."
raise "Erector widgets are initialized with only a parameter hash. (Other parameters are passed to to_s, or the #widget method.)"
end
@assigns = assigns
assign_instance_variables(assigns)
Expand Down Expand Up @@ -274,18 +287,18 @@ def to_text
# Rails view object.
# content_method_name:: in case you want to call a method other than
# #content, pass its name in here.
def to_s(options = {}, &blk)
def to_s(options = {})
raise "Erector::Widget#to_s now takes an options hash, not a symbol. Try calling \"to_s(:content_method_name=> :#{options})\"" if options.is_a? Symbol
_render(options, &blk).to_s
_render(options).to_s
end

# Entry point for rendering a widget (and all its children). Same as #to_s
# only it returns an array, for theoretical performance improvements when using a
# Rack server (like Sinatra or Rails Metal).
#
# # Options: see #to_s
def to_a(options = {}, &blk)
_render(options, &blk).to_a
def to_a(options = {})
_render(options).to_a
end

def _render(options = {}, &blk)
Expand All @@ -306,11 +319,24 @@ def _render(options = {}, &blk)
end

context(options[:parent], output, options[:helpers]) do
send(options[:content_method_name], &blk)
if should_cache?
if (cached_string = cache[self.class, @assigns])
output << cached_string
else
send(options[:content_method_name], &blk)
cache[self.class, @assigns] = output.to_s
end
else
send(options[:content_method_name], &blk)
end
output
end
end

def should_cache?
cache && @block.nil?
end

# Template method which must be overridden by all widget subclasses.
# Inside this method you call the magic #element methods which emit HTML
# and text to the output string. If you call "super" (or don't override
Expand All @@ -337,14 +363,18 @@ def call_block
@block.call(self) if @block
end

# To call one widget from another, inside the parent widget's +content+
# method, instantiate the child widget and call its +write_via+ method,
# passing in +self+. This assures that the same output string is used,
# which gives better performance than using +capture+ or +to_s+. You can
# also use the +widget+ method.
def write_via(parent)
context(parent, parent.output, parent.helpers) do
content
if should_cache?
cached_string = cache[self.class, @assigns]
if cached_string.nil?
cached_string = capture { content }
cache[self.class, @assigns] = cached_string
end
rawtext cached_string
else
content # call the subclass' content method
end
end
end

Expand All @@ -354,11 +384,15 @@ def write_via(parent)
# If the first argument is an instance then the hash must be unspecified
# (or empty). If a block is passed to this method, then it gets set as the
# rendered widget's block.
def widget(target, assigns={}, &block)
#
# This is the preferred way to call one widget from inside another. This
# method assures that the same output string is used, which gives better
# performance than using +capture+ or +to_s+.
def widget(target, parameters={}, &block)
child = if target.is_a? Class
target.new(assigns, &block)
target.new(parameters, &block)
else
unless assigns.empty?
unless parameters.empty?
raise "Unexpected second parameter. Did you mean to pass in variables when you instantiated the #{target.class.to_s}?"
end
target.block = block unless block.nil?
Expand Down Expand Up @@ -441,11 +475,14 @@ def close_tag(tag_name)
end
end

# Emits text. If a string is passed in, it will be HTML-escaped. If a
# widget or the result of calling methods such as raw is passed in, the
# HTML will not be HTML-escaped again. If another kind of object is passed
# in, the result of calling its to_s method will be treated as a string
# would be.
# Emits text. If a string is passed in, it will be HTML-escaped. If the
# result of calling methods such as raw is passed in, the HTML will not be
# HTML-escaped again. If another kind of object is passed in, the result
# of calling its to_s method will be treated as a string would be.
#
# You shouldn't pass a widget in to this method, as that will cause
# performance problems (as well as being semantically goofy). Use the
# #widget method instead.
def text(value)
if value.is_a? Widget
widget value
Expand Down
126 changes: 126 additions & 0 deletions spec/erector/cache_spec.rb
@@ -0,0 +1,126 @@
require File.expand_path("#{File.dirname(__FILE__)}/../spec_helper")

module CacheSpec
describe Erector::Cache do
before do
@cache = Erector::Cache.new
end

class Johnny < Erector::Widget
end

class June < Erector::Widget
end

it 'caches a class with no parameters' do
@cache[Johnny] = "ring of fire"
@cache[Johnny].should == "ring of fire"
end

it 'caches two classes with no parameters' do
@cache[Johnny] = "ring of fire"
@cache[June] = "wildwood flower"
@cache[Johnny].should == "ring of fire"
@cache[June].should == "wildwood flower"
end

it "stores different slots for the same class with different parameters" do
@cache[Johnny, {:flames => "higher"}] = "ring of fire"
@cache[Johnny, {:working => "in a coal mine"}] = "my daddy died young"

@cache[Johnny, {:flames => "higher"}].should == "ring of fire"
@cache[Johnny, {:working => "in a coal mine"}].should == "my daddy died young"
end

describe 'after storing a widget with one parameter' do
before do
@cache[Johnny, {:flames => "higher"}] = "ring of fire"
end

it 'doesn\'t get it when passed the class alone' do
@cache[Johnny].should be_nil
end

it 'doesn\'t get it when passed a different class' do
@cache[June].should be_nil
end

it 'gets it' do
@cache[Johnny, {:flames => "higher"}].should == "ring of fire"
end

it 'doesn\'t get it when passed a different parameter key' do
@cache[Johnny, {:working => "coal mine"}].should be_nil
end

it 'doesn\'t get it when passed a different parameter value' do
@cache[Johnny, {:flames => "lower"}].should be_nil
end

it 'doesn\'t get it when passed an extra parameter key' do
@cache[Johnny, {:flames => "higher", :working => "coal mine"}].should be_nil
end
end

describe 'after storing a widget with more than one parameter' do
before do
@cache[Johnny, {:flames => "higher", :working => "coal mine"}] = "ring of fire"
end

it "gets it" do
@cache[Johnny, {:flames => "higher", :working => "coal mine"}].should == "ring of fire"
end

it 'doesn\'t get it when passed the class alone' do
@cache[Johnny].should be_nil
end

it "doesn't get it when passed a partial parameter set" do
@cache[Johnny, {:flames => "higher"}].should be_nil
end

it 'doesn\'t get it when passed a different class' do
@cache[June].should be_nil
end

it 'doesn\'t get it when passed different a parameter value' do
@cache[Johnny, {:flames => "lower", :working => "coal mine"}].should be_nil
end

it 'doesn\'t get it when passed an extra parameter key' do
@cache[Johnny, {:flames => "higher", :working => "coal mine", :hear => "train a' comin'"}].should be_nil
end
end

describe "expires" do
it 'a class with no parameters' do
@cache[Johnny] = "ring of fire"
@cache.delete(Johnny)
@cache[Johnny].should be_nil
end

it 'all versions of a class' do
@cache[Johnny] = "i fell in"
@cache[Johnny, {:flames => "higher"}] = "ring of fire"
@cache[Johnny, {:working => "in a coal mine"}] = "my daddy died young"

@cache.delete_all(Johnny)

@cache[Johnny].should be_nil
@cache[Johnny, {:flames => "higher"}].should be_nil
@cache[Johnny, {:working => "in a coal mine"}].should be_nil
end

it '...but not other cached values' do
@cache[Johnny] = "ring of fire"
@cache[Johnny, {:flames => 'higher'}] = "higher fire"
@cache[June] = "wildwood flower"
@cache.delete(Johnny)
@cache[Johnny].should be_nil
@cache[Johnny, {:flames => 'higher'}].should == "higher fire"
@cache[June].should == "wildwood flower"
end
end

end
end

0 comments on commit 9c6bdca

Please sign in to comment.