diff --git a/lib/rabl-fast-json.rb b/lib/rabl-fast-json.rb index f3c43fd..0fcadee 100644 --- a/lib/rabl-fast-json.rb +++ b/lib/rabl-fast-json.rb @@ -5,23 +5,27 @@ require 'active_support/json' require 'active_support/core_ext/class/attribute_accessors' require 'rabl-fast-json/version' -require 'rabl-fast-json/helpers' require 'rabl-fast-json/template' require 'rabl-fast-json/compiler' + +require 'rabl-fast-json/renderer' + require 'rabl-fast-json/library' require 'rabl-fast-json/handler' require 'rabl-fast-json/railtie' + + module RablFastJson extend self - + mattr_accessor :cache_templates @@cache_templates = true def configure yield self end - + def cache_templates? ActionController::Base.perform_caching && @@cache_templates end diff --git a/lib/rabl-fast-json/compiler.rb b/lib/rabl-fast-json/compiler.rb index 5e3c4fe..a8422d1 100644 --- a/lib/rabl-fast-json/compiler.rb +++ b/lib/rabl-fast-json/compiler.rb @@ -4,8 +4,6 @@ module RablFastJson # representing data structure # class Compiler - include Helpers - def initialize @glue_count = 0 end @@ -49,7 +47,7 @@ module RablFastJson # def collection(data, options = {}) object(data) - @template.root_name = options[:root] if root_given?(options) + @template.root_name = options[:root] if options[:root] end # @@ -82,8 +80,8 @@ module RablFastJson # def child(name_or_data, options = {}, &block) data, name = extract_data_and_name(name_or_data) - name = options[:root] if root_given?(options) - if partial_given?(options) + name = options[:root] if options[:root] + if options[:partial] template = Library.instance.get(options[:partial]) @template[name] = template.merge!(:_data => data) else diff --git a/lib/rabl-fast-json/handler.rb b/lib/rabl-fast-json/handler.rb index b95f5e2..c86635c 100644 --- a/lib/rabl-fast-json/handler.rb +++ b/lib/rabl-fast-json/handler.rb @@ -4,7 +4,7 @@ module RablFastJson cattr_accessor :default_format self.default_format = 'application/json' - def self.call(template) + def self.call(template) %{ RablFastJson::Library.instance. get_rendered_template(#{template.source.inspect}, self) diff --git a/lib/rabl-fast-json/helpers.rb b/lib/rabl-fast-json/helpers.rb deleted file mode 100644 index e105b67..0000000 --- a/lib/rabl-fast-json/helpers.rb +++ /dev/null @@ -1,11 +0,0 @@ -module RablFastJson - module Helpers - def root_given?(options) #:nodoc: - options[:root].present? - end - - def partial_given?(options) #:nodoc: - options[:partial].present? - end - end -end \ No newline at end of file diff --git a/lib/rabl-fast-json/library.rb b/lib/rabl-fast-json/library.rb index 32ccaad..62b511e 100644 --- a/lib/rabl-fast-json/library.rb +++ b/lib/rabl-fast-json/library.rb @@ -4,20 +4,18 @@ module RablFastJson class Library include Singleton - attr_accessor :view_renderer - def initialize @cached_templates = {} end def get_rendered_template(source, context) path = context.instance_variable_get(:@virtual_path) - @view_renderer = context.instance_variable_get(:@view_renderer) + @lookup_context = context.lookup_context compiled_template = get_compiled_template(path, source) - compiled_template.context = context - body = compiled_template.render - ActiveSupport::JSON.encode(compiled_template.root_name ? { compiled_template.root_name => body } : body) + + format = context.params[:format] || 'json' + Renderers.const_get(format.upcase!).new(context).render(compiled_template) end def get_compiled_template(path, source) @@ -31,8 +29,8 @@ module RablFastJson def get(path) template = @cached_templates[path] - return template unless template.nil? - t = @view_renderer.lookup_context.find_template(path, [], false) + return template if template + t = @lookup_context.find_template(path, [], false) get_compiled_template(path, t.source) end end diff --git a/lib/rabl-fast-json/renderer.rb b/lib/rabl-fast-json/renderer.rb new file mode 100644 index 0000000..3065875 --- /dev/null +++ b/lib/rabl-fast-json/renderer.rb @@ -0,0 +1,2 @@ +require 'rabl-fast-json/renderers/base' +require 'rabl-fast-json/renderers/json' \ No newline at end of file diff --git a/lib/rabl-fast-json/renderers/base.rb b/lib/rabl-fast-json/renderers/base.rb new file mode 100644 index 0000000..bb8bd9e --- /dev/null +++ b/lib/rabl-fast-json/renderers/base.rb @@ -0,0 +1,114 @@ +module RablFastJson + module Renderers + class PartialError < StandardError; end + + class Base + + def initialize(context) # :nodoc: + @_context = context + setup_render_context + end + + # + # Render a template. + # Uses the compiled template source to get a hash with the actual + # data and then format the result according to the `format_result` + # method defined by the renderer. + # + def render(template) + collection_or_resource = @_context.instance_variable_get(template.data) if template.data + output_hash = collection_or_resource.respond_to?(:each) ? render_collection(collection_or_resource, template.source) : + render_resource(collection_or_resource, template.source) + output_hash = { template.root_name => output_hash } if template.root_name + format_output(output_hash) + end + + # + # Format a hash into the desired output. + # Renderer subclasses must implement this method + # + def format_output(hash) + raise "Muse be implemented by renderer" + end + + protected + + # + # Render a single resource as a hash, according to the compiled + # template source passed. + # + def render_resource(data, source) + source.inject({}) { |output, current| + key, value = current + + out = case value + when Symbol + data.send(value) # attributes + when Proc + instance_exec data, &value # node + when Array # node with condition + next output if !instance_exec data, &(value.first) + instance_exec data, &(value.last) + when Hash + current_value = value.dup + data_symbol = current_value.delete(:_data) + object = data_symbol.nil? ? data : data_symbol.to_s.start_with?('@') ? @_context.instance_variable_get(data_symbol) : data.send(data_symbol) + + if key.to_s.start_with?('_') # glue + current_value.each_pair { |k, v| + output[k] = object.send(v) + } + next output + else # child + object.respond_to?(:each) ? render_collection(object, current_value) : render_resource(object, current_value) + end + end + output[key] = out + output + } + end + + # + # Call the render_resource mtehod on each object of the collection + # and return an array of the returned values. + # + def render_collection(collection, source) + collection.map { |o| render_resource(o, source) } + end + + # + # Allow to use partial inside of node blocks (they are evaluated at) + # rendering time. + # + def partial(template_path, options = {}) + raise PartialError.new("No object was given to partial") unless options[:object] + object = options[:object] + + return [] if object.respond_to?(:empty?) && object.empty? + + template = Library.instance.get(template_path) + object.respond_to?(:each) ? render_collection(object, template.source) : render_resource(object, template.source) + end + + # + # If a method is called inside a 'node' property or a 'if' lambda + # it will be passed to context if it exists or treated as a standard + # missing method. + # + def method_missing(name, *args, &block) + @context.respond_to?(name) ? @context.send(name, *args, &block) : super + end + + # + # Copy assigns from controller's context into this + # renderer context to include instances variables when + # evaluating 'node' properties. + # + def setup_render_context + @_context.instance_variable_get(:@_assigns).each_pair { |k, v| + instance_variable_set("@#{k}", v) unless k.start_with?('_') || k == @data + } + end + end + end +end \ No newline at end of file diff --git a/lib/rabl-fast-json/renderers/json.rb b/lib/rabl-fast-json/renderers/json.rb new file mode 100644 index 0000000..ed36cef --- /dev/null +++ b/lib/rabl-fast-json/renderers/json.rb @@ -0,0 +1,9 @@ +module RablFastJson + module Renderers + class JSON < Base + def format_output(hash) + ActiveSupport::JSON.encode(hash) + end + end + end +end \ No newline at end of file diff --git a/lib/rabl-fast-json/template.rb b/lib/rabl-fast-json/template.rb index 0a755ca..8445de1 100644 --- a/lib/rabl-fast-json/template.rb +++ b/lib/rabl-fast-json/template.rb @@ -1,86 +1,11 @@ module RablFastJson class CompiledTemplate - attr_accessor :source, :data, :root_name, :context + attr_accessor :source, :data, :root_name delegate :[], :[]=, :merge!, :to => :source def initialize @source = {} end - - def has_root_name? - !@root_name.nil? - end - - def render - get_object_from_context - get_assigns_from_context - @object.respond_to?(:each) ? render_collection : render_resource - end - - def render_resource(data = nil, source = nil) - data ||= @object - source ||= @source - - source.inject({}) { |output, current| - key, value = current - - out = case value - when Symbol - data.send(value) # attributes - when Proc - instance_exec data, &value # node - when Array # node with condition - next output if !instance_exec data, &(value.first) #value.first.call(data) - instance_exec data, &(value.last) - when Hash - current_value = value.dup - data_symbol = current_value.delete(:_data) - object = data_symbol.nil? ? data : data_symbol.to_s.start_with?('@') ? @context.instance_variable_get(data_symbol) : data.send(data_symbol) - - if key.to_s.start_with?('_') # glue - current_value.each_pair { |k, v| - output[k] = object.send(v) - } - next output - else # child - object.respond_to?(:each) ? render_collection(object, current_value) : render_resource(object, current_value) - end - end - output[key] = out - output - } - end - - def render_collection(collection = nil, source = nil) - collection ||= @object - collection.inject([]) { |output, o| output << render_resource(o, source) } - end - - def method_missing(name, *args, &block) - @context.respond_to?(name) ? @context.send(name, *args, &block) : super - end - - def partial(template_path, options = {}) - raise "No object was given to partial" if options[:object].nil? - object = options[:object] - - return [] if object.respond_to?(:empty?) && object.empty? - - template = Library.instance.get(template_path) - object.respond_to?(:each) ? template.render_collection(object) : template.render_resource(object) - end - - protected - - def get_object_from_context - @object = @context.instance_variable_get(@data) if @data - end - - def get_assigns_from_context - @context.instance_variable_get(:@_assigns).each_pair { |k, v| - instance_variable_set("@#{k}", v) unless k.start_with?('_') || k == @data - } - end end end \ No newline at end of file diff --git a/test/cache_templates_test.rb b/test/cache_templates_test.rb index 5de4e56..3d792df 100644 --- a/test/cache_templates_test.rb +++ b/test/cache_templates_test.rb @@ -4,31 +4,31 @@ class CacheTemplatesTest < ActiveSupport::TestCase setup do RablFastJson::Library.reset_instance - @library = RablFastJson::Library.instance - end - - test "cache templates if perform_caching is active and cache_templates is enabled" do + @library = RablFastJson::Library.instance RablFastJson.cache_templates = true - ActionController::Base.stub(:perform_caching).and_return(true) + end + + test "cache templates if perform_caching is active and cache_templates is enabled" do + ActionController::Base.stub(:perform_caching).and_return(true) @library.get_compiled_template('some/path', "") t = @library.get_compiled_template('some/path', "attribute :id") - + assert_equal({}, t.source) end - + test "cached templates should not be modifiable in place" do - RablFastJson.cache_templates = true ActionController::Base.stub(:perform_caching).and_return(true) - t = @library.get_compiled_template('some/path', "") - assert_nil t.context - t.context = "foobar" - assert_nil @library.get_compiled_template('some/path', "").context + @library.get_compiled_template('some/path', "") + t = @library.get_compiled_template('some/path', "attribute :id") + + assert_equal({}, t.source) end - + test "don't cache templates cache_templates is enabled but perform_caching is not active" do - RablFastJson.cache_templates = true - ActionController::Base.stub(:perform_caching).and_return(false) - - refute_equal @library.get_compiled_template('some/path', ""), @library.get_compiled_template('some/path', "") + ActionController::Base.stub(:perform_caching).and_return(false) + @library.get_compiled_template('some/path', "") + t = @library.get_compiled_template('some/path', "attribute :id") + + assert_equal({ :id => :id }, t.source) end end \ No newline at end of file diff --git a/test/compiler_test.rb b/test/compiler_test.rb index 862d3d4..c9b550f 100644 --- a/test/compiler_test.rb +++ b/test/compiler_test.rb @@ -11,6 +11,40 @@ class CompilerTest < ActiveSupport::TestCase assert_instance_of RablFastJson::CompiledTemplate, @compiler.compile_source("") end + test "object set data for the template" do + t = @compiler.compile_source(%{ object :@user }) + assert_equal :@user, t.data + assert_equal({}, t.source) + end + + test "object property can define root name" do + t = @compiler.compile_source(%{ object :@user => :author }) + assert_equal :@user, t.data + assert_equal :author, t.root_name + assert_equal({}, t.source) + end + + test "collection set the data for the template" do + t = @compiler.compile_source(%{ collection :@user }) + assert_equal :@user, t.data + assert_equal({}, t.source) + end + + test "collection property can define root name" do + t = @compiler.compile_source(%{ collection :@user => :users }) + assert_equal :@user, t.data + assert_equal :users, t.root_name + assert_equal({}, t.source) + end + + test "collection property can define root name via options" do + t = @compiler.compile_source(%{ collection :@user, :root => :users }) + assert_equal :@user, t.data + assert_equal :users, t.root_name + end + + # Compilation + test "simple attributes are compiled to hash" do t = @compiler.compile_source(%{ attributes :id, :name }) assert_equal({ :id => :id, :name => :name}, t.source) @@ -57,11 +91,10 @@ class CompilerTest < ActiveSupport::TestCase end test "child with succint partial notation" do - @view_renderer = mock() - @view_renderer.stub_chain(:lookup_context, :find_template).with('users/base', [], false).and_return( - mock(:source => %{ attribute :id })) + mock_template = RablFastJson::CompiledTemplate.new + mock_template.source = { :id => :id } RablFastJson::Library.reset_instance - RablFastJson::Library.instance.view_renderer = @view_renderer + RablFastJson::Library.instance.stub(:get).with('users/base').and_return(mock_template) t = @compiler.compile_source(%{child(:user, :partial => 'users/base') }) assert_equal( {:user => { :_data => :user, :id => :id } }, t.source) @@ -84,38 +117,6 @@ class CompilerTest < ActiveSupport::TestCase }, t.source) end - test "object set data for the template" do - t = @compiler.compile_source(%{ object :@user }) - assert_equal :@user, t.data - assert_equal({}, t.source) - end - - test "object property can define root name" do - t = @compiler.compile_source(%{ object :@user => :author }) - assert_equal :@user, t.data - assert_equal :author, t.root_name - assert_equal({}, t.source) - end - - test "collection set the data for the template" do - t = @compiler.compile_source(%{ collection :@user }) - assert_equal :@user, t.data - assert_equal({}, t.source) - end - - test "collection property can define root name" do - t = @compiler.compile_source(%{ collection :@user => :users }) - assert_equal :@user, t.data - assert_equal :users, t.root_name - assert_equal({}, t.source) - end - - test "collection property can define root name via options" do - t = @compiler.compile_source(%{ collection :@user, :root => :users }) - assert_equal :@user, t.data - assert_equal :users, t.root_name - end - test "extends use other template source as itself" do template = mock('template', :source => { :id => :id }) RablFastJson::Library.reset_instance diff --git a/test/deep_nesting_test.rb b/test/deep_nesting_test.rb index 3c3e87c..2dca67a 100644 --- a/test/deep_nesting_test.rb +++ b/test/deep_nesting_test.rb @@ -21,15 +21,11 @@ class DeepNestingTest < ActiveSupport::TestCase @user.stub(:posts).and_return([@post]) @user.stub(:respond_to?).with(:each).and_return(false) - @view_renderer = mock() - @view_renderer.stub_chain(:lookup_context, :find_template).with('comments/show', [], false).and_return( - mock(:source => %{ object :@comment\n attribute :content })) - @context = Context.new @context.stub(:instance_variable_get).with(:@user).and_return(@user) - @context.stub(:instance_variable_get).with(:@view_renderer).and_return(@view_renderer) @context.stub(:instance_variable_get).with(:@virtual_path).and_return('users/show') @context.stub(:instance_variable_get).with(:@_assigns).and_return({}) + @context.stub(:lookup_context).and_return(mock(:find_template => mock(:source => %{ object :@comment\n attribute :content }))) end test "compile and render deep nesting template" do diff --git a/test/non_restful_response_test.rb b/test/non_restful_response_test.rb index 8eadfcd..d0a7e83 100644 --- a/test/non_restful_response_test.rb +++ b/test/non_restful_response_test.rb @@ -3,18 +3,18 @@ require 'test_helper' class NonRestfulResponseTest < ActiveSupport::TestCase setup do RablFastJson::Library.reset_instance - + @user = User.new(1, 'foo', 'male') @user.stub_chain(:posts, :count).and_return(10) @user.stub(:respond_to?).with(:each).and_return(false) - + @context = Context.new @context.stub(:instance_variable_get).with(:@user).and_return(@user) - @context.stub(:instance_variable_get).with(:@view_renderer).and_return(mock()) @context.stub(:instance_variable_get).with(:@virtual_path).and_return('user/show') @context.stub(:instance_variable_get).with(:@_assigns).and_return({'user' => @user}) + @context.stub(:lookup_context) end - + test "compile and render non restful resource" do source = %{ object false @@ -23,13 +23,13 @@ class NonRestfulResponseTest < ActiveSupport::TestCase attributes :id, :name end } - + assert_equal(ActiveSupport::JSON.encode({ :post_count => 10, :user => { :id => 1, :name => 'foo' } - }), RablFastJson::Library.instance.get_rendered_template(source, @context)) + }), RablFastJson::Library.instance.get_rendered_template(source, @context)) end end \ No newline at end of file diff --git a/test/compiled_template_test.rb b/test/renderers/json_renderer_test.rb similarity index 63% rename from test/compiled_template_test.rb rename to test/renderers/json_renderer_test.rb index 7971bbe..b4fd0ec 100644 --- a/test/compiled_template_test.rb +++ b/test/renderers/json_renderer_test.rb @@ -1,72 +1,80 @@ require 'test_helper' -class TestCompiledTemplate < ActiveSupport::TestCase +class TestJsonRenderer < ActiveSupport::TestCase setup do - @context = Context.new @data = User.new(1, 'foobar', 'male') @data.stub(:respond_to?).with(:each).and_return(false) + + @context = Context.new @context.stub(:instance_variable_get).with(:@data).and_return(@data) @context.stub(:instance_variable_get).with(:@_assigns).and_return({}) + @template = RablFastJson::CompiledTemplate.new - @template.context = @context @template.data = :@data end + def render_json_output + RablFastJson::Renderers::JSON.new(@context).render(@template).to_s + end + test "render object wth empty template" do @template.source = {} - assert_equal({}, @template.render) + assert_equal %q({}), render_json_output end test "render collection with empty template" do @context.stub(:instance_variable_get).with(:@data).and_return([@data]) @template.source = {} - assert_equal([{}], @template.render) + assert_equal %q([{}]), render_json_output end test "render single object attributes" do @template.source = { :id => :id, :name => :name } - assert_equal({ :id => 1, :name => 'foobar'}, @template.render) + assert_equal %q({"id":1,"name":"foobar"}), render_json_output end - test "render object as a child" do + test "render child with object association" do + @data.stub(:address).and_return(mock(:city => 'Paris')) + @template.source = { :address => { :_data => :address, :city => :city } } + assert_equal %q({"address":{"city":"Paris"}}), render_json_output + end + + test "render child with arbitrary data source" do @template.source = { :author => { :_data => :@data, :name => :name } } - assert_equal({ :author => { :name => 'foobar' } }, @template.render) + assert_equal %q({"author":{"name":"foobar"}}), render_json_output end test "render glued attributes from single object" do @template.source = { :_glue0 => { :_data => :@data, :name => :name } } - assert_equal({ :name => 'foobar' }, @template.render) + assert_equal %q({"name":"foobar"}), render_json_output end test "render collection with attributes" do @data = [User.new(1, 'foo', 'male'), User.new(2, 'bar', 'female')] @context.stub(:instance_variable_get).with(:@data).and_return(@data) @template.source = { :uid => :id, :name => :name, :gender => :sex } - assert_equal([ - { :uid => 1, :name => 'foo', :gender => 'male'}, - { :uid => 2, :name => 'bar', :gender => 'female'} - ], @template.render) + assert_equal %q([{"uid":1,"name":"foo","gender":"male"},{"uid":2,"name":"bar","gender":"female"}]), render_json_output end test "render node property" do - proc = lambda { |object| object.sex } - @template.source = { :sex => proc } - assert_equal({ :sex => 'male' }, @template.render) + proc = lambda { |object| object.name } + @template.source = { :name => proc } + assert_equal %q({"name":"foobar"}), render_json_output end test "render node property with true condition" do condition = lambda { |u| true } proc = lambda { |object| object.name } @template.source = { :name => [condition, proc] } - assert_equal({ :name => 'foobar' }, @template.render) + assert_equal %q({"name":"foobar"}), render_json_output end test "render node property with false condition" do condition = lambda { |u| false } proc = lambda { |object| object.name } @template.source = { :name => [condition, proc] } - assert_equal({}, @template.render) + assert_equal %q({}), render_json_output end test "partial should be evaluated at rendering time" do @@ -76,27 +84,27 @@ class TestCompiledTemplate < ActiveSupport::TestCase # Stub Library#get t = RablFastJson::CompiledTemplate.new - t.source, t.context = { :name => :name }, @context + t.source = { :name => :name } RablFastJson::Library.reset_instance RablFastJson::Library.instance.should_receive(:get).with('users/base').and_return(t) @template.data = false @template.source = { :user => ->(s) { partial('users/base', :object => @user) } } - assert_equal({ :user => { :name => 'foobar' } }, @template.render) + assert_equal %q({"user":{"name":"foobar"}}), render_json_output end - test "partial with nil values should raise an error" do + test "partial with no values should raise an error" do @template.data = false @template.source = { :user => ->(s) { partial('users/base') } } - assert_raises(RuntimeError) { @template.render } + assert_raises(RablFastJson::Renderers::PartialError) { render_json_output } end test "partial with empty values should not raise an error" do @template.data = false @template.source = { :users => ->(s) { partial('users/base', :object => []) } } - assert_equal({ :users => [] }, @template.render) + assert_equal %q({"users":[]}), render_json_output end end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index f315ce5..c9f4c64 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -35,16 +35,11 @@ class Context def initialize @_assigns = {} - @virtual_path = '/users' end - def set_assign(key, value) - @_assigns[key] = value - end - - def get_assign(key) - @_assigns[key] + def params + {} end end - + User = Struct.new(:id, :name, :sex) \ No newline at end of file