Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Refactor read_multi support to be more general.

Most changes are consolidated to the engine and builder classes.
High-level, builder now defers the actual rendering (to hash) until
later on, allowing the engine a chance to evaluate all of the various
extends and so forth that may contain a cache directive. Once all of the
cache directives are found, it can generate all of the cache keys and fetch
them simultaneously using read_multi.
  • Loading branch information...
commit f8ebbb6d20003c1bd9240f83382d0b699809394d 1 parent 95875ee
@ahlatimer authored
View
72 lib/rabl/builder.rb
@@ -16,27 +16,28 @@ def initialize(options={}, &block)
# build(@user, :format => "json", :attributes => { ... }, :root_name => "user")
def build(object, options={})
@_object = object
-
- cache_results do
+ if options[:keep_engines]
+ compile_engines
+ else
compile_hash(options)
end
end
+ def engines
+ @_engines ||= []
+ end
+
protected
- # Returns a hash representation of the data object
- # compile_hash(:root_name => false)
- # compile_hash(:root_name => "user")
- def compile_hash(options={})
- @_result = {}
+ # Returns the builder with all engine-producing options evaluated.
+ # (extends, node, children, glues)
+ def compile_engines
+ @_engines = []
+
# Extends
@options[:extends].each do |settings|
extends(settings[:file], settings[:options], &settings[:block])
end if @options.has_key?(:extends)
- # Attributes
- @options[:attributes].each_pair do |attribute, settings|
- attribute(attribute, settings)
- end if @options.has_key?(:attributes)
# Node
@options[:node].each do |settings|
node(settings[:name], settings[:options], &settings[:block])
@@ -49,6 +50,40 @@ def compile_hash(options={})
@options[:glue].each do |settings|
glue(settings[:data], &settings[:block])
end if @options.has_key?(:glue)
+ end
+
+ # Returns a hash representation of the data object
+ # compile_hash(:root_name => false)
+ # compile_hash(:root_name => "user")
+ def compile_hash(options={})
+ @_result = {}
+
+ compile_engines if engines.empty?
+
+ # Attributes
+ @options[:attributes].each_pair do |attribute, settings|
+ attribute(attribute, settings)
+ end if @options.has_key?(:attributes)
+
+ # Turn engines into hashes
+ @_engines.each do |engine|
+ if engine.is_a?(Hash)
+ engine.each do |key, value|
+ if value.is_a?(Rabl::Engine)
+ value = value.render
+ if value
+ engine[key] = value
+ else
+ engine.delete(key)
+ end
+ end
+ end
+ elsif engine.is_a?(Rabl::Engine)
+ engine = engine.render
+ end
+
+ @_result.merge!(engine) if engine.is_a?(Hash)
+ end
# Wrap result in root
if options[:root_name].present?
@@ -61,6 +96,11 @@ def compile_hash(options={})
@_root_name ? { @_root_name => @_result } : @_result
end
+ def replace_engine(engine, value)
+ @_engines.delete(engine)
+ @_result.merge!(value)
+ end
+
# Indicates an attribute or method should be included in the json output
# attribute :foo, :as => "bar"
# attribute :foo, :as => "bar", :if => lambda { |m| m.foo }
@@ -95,7 +135,7 @@ def child(data, options={}, &block)
include_root = is_collection?(object) && @options[:child_root] # child @users
engine_options = @options.slice(:child_root).merge(:root => include_root)
object = { object => name } if data.respond_to?(:each_pair) && object # child :users => :people
- @_result[name] = self.object_to_hash(object, engine_options, &block)
+ @_engines << { name => self.object_to_engine(object, engine_options, &block) }
end
# Glues data from a child node to the json_output
@@ -103,16 +143,16 @@ def child(data, options={}, &block)
def glue(data, &block)
return false unless data.present?
object = data_object(data)
- glued_attributes = self.object_to_hash(object, :root => false, &block)
- @_result.merge!(glued_attributes) if glued_attributes
+ glued_attributes = self.object_to_engine(object, :root => false, &block)
+ @_engines << glued_attributes
end
# Extends an existing rabl template with additional attributes in the block
# extends("users/show") { attribute :full_name }
def extends(file, options={}, &block)
options = @options.slice(:child_root).merge(:object => @_object).merge(options)
- result = self.partial(file, options, &block)
- @_result.merge!(result) if result.is_a?(Hash)
+ result = self.partial_as_engine(file, options, &block)
+ @_engines << result
end
# resolve_condition(:if => true) => true
View
76 lib/rabl/engine.rb
@@ -19,7 +19,7 @@ def source=(string)
# Renders the representation based on source, object, scope and locals
# Rabl::Engine.new("...source...", { :format => "xml" }).render(scope, { :foo => "bar", :object => @user })
- def render(scope, locals, &block)
+ def apply(scope, locals, &block)
reset_options!
@_locals, @_scope = locals, scope
self.copy_instance_variables_from(@_scope, [:@assigns, :@helpers])
@@ -34,26 +34,20 @@ def render(scope, locals, &block)
instance_eval(@_source) if @_source.present?
end
instance_exec(@_data_object, &block) if block_given?
- cache_results { self.send("to_" + @_options[:format].to_s) }
- end
- def apply_without_rendering(scope, locals, &block)
- reset_options!
- @_locals, @_scope = locals, scope
- self.copy_instance_variables_from(@_scope, [:@assigns, :@helpers])
- @_options[:scope] = @_scope
- @_options[:format] ||= self.request_format
- data = locals[:object].nil? ? self.default_object : locals[:object]
- @_data_object, @_data_name = data_object(data), data_name(data)
- if @_options[:source_location]
- instance_eval(@_source, @_options[:source_location]) if @_source.present?
- else # without source location
- instance_eval(@_source) if @_source.present?
- end
- instance_exec(data_object(@_data), &block) if block_given?
self
end
+ def cache_key
+ _cache = @_cache if defined?(@_cache)
+ cache_key, _ = *_cache || nil
+ cache_key
+ end
+
+ def render
+ cache_results { self.send("to_#{@_options[:format]}") }
+ end
+
# Returns a hash representation of the data object
# to_hash(:root => true, :child_root => true)
def to_hash(options={})
@@ -65,45 +59,37 @@ def to_hash(options={})
if is_object?(data) || !data # object @user
builder.build(data, options)
elsif is_collection?(data) # collection @users
- if options[:read_multi] && template_cache_configured?
- fetch_results_from_cache(builder, data, options)
+ if template_cache_configured?
+ read_multi(data, builder, options)
else
data.map { |object| builder.build(object, options) }
end
end
end
- def fetch_results_from_cache(builder, data, options)
- key_to_object = data.inject({}) do |hash, object|
- cache_key = if options.has_key?(:extends)
- settings = options[:extends].last
- settings[:options] = options.slice(:child_root).merge(:object => object).merge(settings[:options])
- engine = self.partial_without_rendering(settings[:file], settings[:options], &settings[:block])
- cache_key, _ = *engine.instance_variable_get(:"@_cache")
- Array(cache_key) + [options[:root_name], options[:format]]
- else
- [object, options[:root_name], options[:format]]
+ def read_multi(data, options = {})
+ keys_to_builder = {}
+ data.each do |object|
+ builder.build(object, options.merge(:keep_engines => true))
+ builder.engines.each do |engine|
+ if engine.cache_key
+ result_cache_key = ActiveSupport::Cache.expand_cache_key(cache_key, :rabl)
+ keys_to_builder[result_cache_key] = builder
+ end
end
- result_cache_key = ActiveSupport::Cache.expand_cache_key(cache_key, :rabl)
- hash[result_cache_key] = object
- hash
end
- mutable_keys = key_to_object.keys.map { |k| k.dup }
+ mutable_keys = keys_to_engines.keys.map { |k| k.dup }
result_hash = Rabl.configuration.cache_engine.read_multi(mutable_keys)
- result_hash.each do |key ,value|
+ result_hash.each do |key, value|
if value
- key_to_object[key] = value
+ keys_to_builder[key].replace_engine(engine, value)
end
end
- key_to_object.map do |key, object|
- if object.is_a?(Hash)
- object
- else
- builder.build(object, options)
- end
+ keys_to_builder.values.map do |builder|
+ builder.compile_hash
end
end
@@ -304,7 +290,13 @@ def respond_to?(name, include_private=false)
# Supports calling helpers defined for the template scope using method_missing hook
def method_missing(name, *args, &block)
- context_scope.respond_to?(name, true) ? context_scope.__send__(name, *args, &block) : super
+ if context_scope.respond_to?(name, true)
+ context_scope.__send__(name, *args, &block)
+ elsif {}.respond_to?(name, true)
+ self.to_hash.__send__(name, *args, &block)
+ else
+ super
+ end
end
def copy_instance_variables_from(object, exclude = []) #:nodoc:
View
21 lib/rabl/partials.rb
@@ -2,19 +2,15 @@ module Rabl
module Partials
include Rabl::Helpers
- # Renders a partial hash based on another rabl template
+ # Returns a hash representing the partial
# partial("users/show", :object => @user)
# options must have :object
# options can have :view_path, :child_root, :root
def partial(file, options={}, &block)
- raise ArgumentError, "Must provide an :object option to render a partial" unless options.has_key?(:object)
- object, view_path = options.delete(:object), options[:view_path] || @_view_path
- source, location = self.fetch_source(file, :view_path => view_path)
- engine_options = options.merge(:source => source, :source_location => location)
- self.object_to_hash(object, engine_options, &block)
+ self.partial_as_engine(file, options, &block).render
end
- def partial_without_rendering(file, options={}, &block)
+ def partial_as_engine(file, options={}, &block)
raise ArgumentError, "Must provide an :object option to render a partial" unless options.has_key?(:object)
object, view_path = options.delete(:object), options[:view_path] || @_view_path
source, location = self.fetch_source(file, :view_path => view_path)
@@ -22,24 +18,17 @@ def partial_without_rendering(file, options={}, &block)
self.object_to_engine(object, engine_options, &block)
end
- # Returns a hash based representation of any data object given ejs template block
+ # Returns an Engine based representation of any data object given ejs template block
# object_to_hash(@user) { attribute :full_name } => { ... }
# object_to_hash(@user, :source => "...") { attribute :full_name } => { ... }
# object_to_hash([@user], :source => "...") { attribute :full_name } => { ... }
# options must have :source (rabl file contents)
# options can have :source_location (source filename)
- def object_to_hash(object, options={}, &block)
- return object if object.nil?
- return [] if is_collection?(object) && object.blank? # empty collection
- engine_options = options.reverse_merge(:format => "hash", :view_path => @_view_path, :root => (options[:root] || false))
- Rabl::Engine.new(options[:source], engine_options).render(@_scope, :object => object, &block)
- end
-
def object_to_engine(object, options={}, &block)
return object unless is_object?(object) || is_collection?(object)
return [] if is_collection?(object) && object.blank? # empty collection
engine_options = options.reverse_merge(:format => "hash", :view_path => @_view_path, :root => (options[:root] || false))
- Rabl::Engine.new(options[:source], engine_options).apply_without_rendering(@_scope, :object => object, &block)
+ Rabl::Engine.new(options[:source], engine_options).apply(@_scope, :object => object, &block)
end
# Returns source for a given relative file
View
2  lib/rabl/renderer.rb
@@ -46,7 +46,7 @@ def render(context_scope = nil)
context_scope = context_scope ? context_scope : options.delete(:scope) || self
set_instance_variable(object) if context_scope == self
locals = options.fetch(:locals, {}).reverse_merge(:object => object)
- engine.render(context_scope, locals)
+ engine.apply(context_scope, locals).render
end
protected
View
4 lib/rabl/template.rb
@@ -12,7 +12,7 @@ def prepare
end
def evaluate(scope, locals, &block)
- @engine.render(scope, locals, &block)
+ @engine.apply(scope, locals, &block).render
end
end
@@ -59,4 +59,4 @@ def self.call(template)
end
ActionView::Template.register_template_handler :rabl, ActionView::Template::Handlers::Rabl
-end
View
36 test/builder_test.rb
@@ -98,22 +98,28 @@
asserts "that it generates with an object" do
b = builder :child => [{ :data => @user, :options => {}, :block => lambda { |u| attribute :name } }]
+ e = Rabl::Engine.new('')
mock(b).data_name(@user) { :user }
- mock(b).object_to_hash(@user, { :root => false }).returns('xyz').subject
+ mock(e).render.returns('xyz')
+ mock(b).object_to_engine(@user, { :root => false }).returns(e).subject
b.build(@user)
end.equivalent_to({ :user => 'xyz'})
asserts "that it generates with an collection and child_root" do
b = builder :child => [{ :data => @users, :options => {}, :block => lambda { |u| attribute :name } }], :child_root => true
+ e = Rabl::Engine.new('')
mock(b).data_name(@users) { :users }
- mock(b).object_to_hash(@users, { :root => true, :child_root => true }).returns('xyz').subject
+ mock(e).render.returns('xyz')
+ mock(b).object_to_engine(@users, { :root => true, :child_root => true }).returns(e).subject
b.build(@user)
end.equivalent_to({ :users => 'xyz'})
asserts "that it generates with an collection and no child root" do
b = builder :child => [{ :data => @users, :options => {}, :block => lambda { |u| attribute :name } }], :child_root => false
+ e = Rabl::Engine.new('')
mock(b).data_name(@users) { :users }
- mock(b).object_to_hash(@users, { :root => false, :child_root => false }).returns('xyz').subject
+ mock(e).render.returns('xyz')
+ mock(b).object_to_engine(@users, { :root => false, :child_root => false }).returns('xyz').subject
b.build(@user)
end.equivalent_to({ :users => 'xyz'})
end
@@ -125,7 +131,9 @@
asserts "that it generates the glue attributes" do
b = builder :glue => [{ :data => @user, :block => lambda { |u| attribute :name }}]
- mock(b).object_to_hash(@user, { :root => false }).returns({:user => 'xyz'}).subject
+ e = Rabl::Engine.new('')
+ mock(e).render.returns({:user => 'xyz'})
+ mock(b).object_to_engine(@user, { :root => false }).returns(e).subject
b.build(@user)
end.equivalent_to({ :user => 'xyz' })
@@ -136,27 +144,35 @@
asserts "that it does not generate new attributes if no glue attributes are present" do
b = builder :glue => [{ :data => @user, :block => lambda { |u| attribute :name }}]
- mock(b).object_to_hash(@user,{ :root => false }).returns({}).subject
+ e = Rabl::Engine.new('')
+ mock(e).render.returns({})
+ mock(b).object_to_engine(@user,{ :root => false }).returns(e).subject
b.build(@user)
end.equals({})
end
- context "#extend" do
- asserts "that it does not genereate if no data is present" do
+ context "#extends" do
+ asserts "that it does not generate if no data is present" do
b = builder :extends => [{ :file => 'users/show', :options => {}, :block => lambda { |u| attribute :name }}]
- mock(b).partial('users/show',{ :object => @user}).returns({}).subject
+ e = Rabl::Engine.new('users/show')
+ mock(b).partial_as_engine('users/show',{ :object => @user}).returns(e)
+ mock(e).render.returns({}).subject
b.build(@user)
end.equals({})
asserts "that it generates if data is present" do
b = builder :extends => [{ :file => 'users/show', :options => {}, :block => lambda { |u| attribute :name }}]
- mock(b).partial('users/show', { :object => @user }).returns({:user => 'xyz'}).subject
+ e = Rabl::Engine.new('users/show')
+ mock(b).partial_as_engine('users/show',{ :object => @user}).returns(e)
+ mock(e).render.returns({:user => 'xyz'}).subject
b.build(@user)
end.equivalent_to({:user => 'xyz'})
asserts "that it generates if local data is present but object is false" do
b = builder :extends => [{ :file => 'users/show', :options => { :object => @user }, :block => lambda { |u| attribute :name }}]
- mock(b).partial('users/show', { :object => @user }).returns({:user => 'xyz'}).subject
+ e = Rabl::Engine.new('users/show')
+ mock(b).partial_as_engine('users/show',{ :object => @user}).returns(e)
+ mock(e).render.returns({:user => 'xyz'}).subject
b.build(false)
end.equivalent_to({:user => 'xyz'})
end
Please sign in to comment.
Something went wrong with that request. Please try again.