Skip to content

Substantial performance improvements #54

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Sep 6, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
source "http://rubygems.org"

gemspec
gemspec

gem "actionpack"
36 changes: 33 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,20 +1,50 @@
PATH
remote: .
specs:
jbuilder (0.3)
jbuilder (0.4.1)
activesupport (>= 3.0.0)
blankslate (>= 2.1.2.4)

GEM
remote: http://rubygems.org/
specs:
activesupport (3.1.3)
actionpack (3.2.8)
activemodel (= 3.2.8)
activesupport (= 3.2.8)
builder (~> 3.0.0)
erubis (~> 2.7.0)
journey (~> 1.0.4)
rack (~> 1.4.0)
rack-cache (~> 1.2)
rack-test (~> 0.6.1)
sprockets (~> 2.1.3)
activemodel (3.2.8)
activesupport (= 3.2.8)
builder (~> 3.0.0)
activesupport (3.2.8)
i18n (~> 0.6)
multi_json (~> 1.0)
blankslate (2.1.2.4)
multi_json (1.0.4)
builder (3.0.0)
erubis (2.7.0)
hike (1.2.1)
i18n (0.6.1)
journey (1.0.4)
multi_json (1.3.6)
rack (1.4.1)
rack-cache (1.2)
rack (>= 0.4)
rack-test (0.6.1)
rack (>= 1.0)
sprockets (2.1.3)
hike (~> 1.2)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
tilt (1.3.3)

PLATFORMS
ruby

DEPENDENCIES
actionpack
jbuilder!
183 changes: 94 additions & 89 deletions lib/jbuilder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,50 @@
require 'active_support/json'
require 'multi_json'
class Jbuilder < BlankSlate
class KeyFormatter
def initialize(*args)
@format = {}
@cache = {}

options = args.extract_options!
args.each do |name|
@format[name] = []
end
options.each do |name, paramaters|
@format[name] = paramaters
end
end

def initialize_copy(original)
@cache = {}
end

def format(key)
@cache[key] ||= @format.inject(key.to_s) do |result, args|
func, args = args
if func.is_a? Proc
func.call(result, *args)
else
result.send(func, *args)
end
end
end
end

# Yields a builder and automatically turns the result into a JSON string
def self.encode
new._tap { |jbuilder| yield jbuilder }.target!
end

@@key_format = {}
@@key_formatter = KeyFormatter.new

define_method(:__class__, find_hidden_method(:class))
define_method(:_tap, find_hidden_method(:tap))
define_method(:_is_a?, find_hidden_method(:is_a?))
reveal(:respond_to?)

def initialize(key_format = @@key_format.clone)
def initialize(key_formatter = @@key_formatter.clone)
@attributes = ActiveSupport::OrderedHash.new
@key_format = key_format
@key_formatter = key_formatter
end

# Dynamically set a key value pair.
Expand All @@ -42,7 +71,7 @@ def set!(key, value = nil)
if block_given?
_yield_nesting(key) { |jbuilder| yield jbuilder }
else
@attributes[_format_key(key)] = value
_set_value(key, value)
end
end

Expand Down Expand Up @@ -75,12 +104,12 @@ def set!(key, value = nil)
# { "_first_name": "David" }
#
def key_format!(*args)
__class__.extract_key_format(args, @key_format)
@key_formatter = KeyFormatter.new(*args)
end

# Same as the instance method key_format! except sets the default.
def self.key_format(*args)
extract_key_format(args, @@key_format)
@@key_formatter = KeyFormatter.new(*args)
end

# Turns the current element into an array and yields a builder to add a hash.
Expand Down Expand Up @@ -129,12 +158,9 @@ def child!
#
# { "people": [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ] }
def array!(collection)
@attributes = [] and return if collection.empty?

collection.each do |element|
child! do |child|
yield child, element
end
@attributes = []
collection.each do |element| #[] and return if collection.empty?
@attributes << _new_instance._tap { |jbuilder| yield jbuilder, element }.attributes!
end
end

Expand All @@ -156,22 +182,19 @@ def array!(collection)
#
# json.(@person, :name, :age)
def extract!(object, *attributes)
p = if object.is_a?(Hash)
lambda{|attribute| __send__ attribute, object.send(:fetch, attribute)}
if object.is_a?(Hash)
attributes.each {|attribute| _set_value attribute, object.send(:fetch, attribute)}
else
lambda{|attribute| __send__ attribute, object.send(attribute)}
attributes.each {|attribute| _set_value attribute, object.send(attribute)}
end

attributes.each{|attribute| p.call(attribute)}
end

if RUBY_VERSION > '1.9'
def call(*args)
case
when args.one?
array!(args.first) { |json, element| yield json, element }
when args.many?
extract!(*args)
def call(object = nil, *attributes)
if attributes.empty?
array!(object) { |json, element| yield json, element }
else
extract!(object, *attributes)
end
end
end
Expand All @@ -187,97 +210,79 @@ def target!
end


private
def method_missing(method, *args)
case
# json.age 32
# json.person another_jbuilder
# { "age": 32, "person": { ... }
when args.one? && args.first.respond_to?(:_is_a?) && args.first._is_a?(Jbuilder)
set! method, args.first.attributes!

# json.comments @post.comments { |json, comment| ... }
# { "comments": [ { ... }, { ... } ] }
when args.one? && block_given?
_yield_iteration(method, args.first) { |child, element| yield child, element }

# json.age 32
# { "age": 32 }
when args.length == 1
set! method, args.first

# json.comments { |json| ... }
# { "comments": ... }
when args.empty? && block_given?
_yield_nesting(method) { |jbuilder| yield jbuilder }
protected
def _set_value(key, value)
@attributes[@key_formatter.format(key)] = value
end

# json.comments(@post.comments, :content, :created_at)
# { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
when args.many? && args.first.respond_to?(:each)
_inline_nesting method, args.first, args.from(1)

# json.author @post.creator, :name, :email_address
# { "author": { "name": "David", "email_address": "[email protected]" } }
when args.many?
_inline_extract method, args.first, args.from(1)
private
def method_missing(method, value = nil, *args)
if block_given?
if value
# json.comments @post.comments { |json, comment| ... }
# { "comments": [ { ... }, { ... } ] }
_yield_iteration(method, value) { |child, element| yield child, element }
else
# json.comments { |json| ... }
# { "comments": ... }
_yield_nesting(method) { |jbuilder| yield jbuilder }
end
else
if args.empty?
if Jbuilder === value
# json.age 32
# json.person another_jbuilder
# { "age": 32, "person": { ... }
_set_value method, value.attributes!
else
# json.age 32
# { "age": 32 }
_set_value method, value
end
else
if value.respond_to?(:each)
# json.comments(@post.comments, :content, :created_at)
# { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
_inline_nesting method, value, args
else
# json.author @post.creator, :name, :email_address
# { "author": { "name": "David", "email_address": "[email protected]" } }
_inline_extract method, value, args
end
end
end
end

# Overwrite in subclasses if you need to add initialization values
def _new_instance
__class__.new(@key_format)
__class__.new(@key_formatter)
end

def _yield_nesting(container)
set! container, _new_instance._tap { |jbuilder| yield jbuilder }.attributes!
_set_value container, _new_instance._tap { |jbuilder| yield jbuilder }.attributes!
end

def _inline_nesting(container, collection, attributes)
__send__(container) do |parent|
parent.array!(collection) and return if collection.empty?

collection.each do |element|
parent.child! do |child|
attributes.each do |attribute|
child.__send__ attribute, element.send(attribute)
end
_yield_nesting(container) do |parent|
parent.array!(collection) do |child, element|
attributes.each do |attribute|
child._set_value attribute, element.send(attribute)
end
end
end
end

def _yield_iteration(container, collection)
__send__(container) do |parent|
_yield_nesting(container) do |parent|
parent.array!(collection) do |child, element|
yield child, element
end
end
end

def _inline_extract(container, record, attributes)
__send__(container) { |parent| parent.extract! record, *attributes }
end

# Format the key using the methods described in @key_format
def _format_key(key)
@key_format.inject(key.to_s) do |result, args|
func, args = args
if func.is_a? Proc
func.call(result, *args)
else
result.send(func, *args)
end
end
end

def self.extract_key_format(args, target)
options = args.extract_options!
args.each do |name|
target[name] = []
end
options.each do |name, paramaters|
target[name] = paramaters
end
_yield_nesting(container) { |parent| parent.extract! record, *attributes }
end
end

Expand Down
6 changes: 3 additions & 3 deletions lib/jbuilder_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ def self.encode(context)
new(context)._tap { |jbuilder| yield jbuilder }.target!
end

def initialize(context)
def initialize(context, *args)
@context = context
super()
super(*args)
end

def partial!(options, locals = {})
Expand All @@ -21,7 +21,7 @@ def partial!(options, locals = {})

private
def _new_instance
__class__.new(@context)
__class__.new(@context, @key_formatter)
end
end

Expand Down
39 changes: 39 additions & 0 deletions test/jbuilder_template_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
require 'test/unit'
require 'active_support/test_case'
require 'active_support/inflector'
require 'action_dispatch'
require 'action_view'

require 'jbuilder'
require 'jbuilder_template'

class JbuilderTemplateTest < ActiveSupport::TestCase
test "rendering" do
json = JbuilderTemplate.encode(binding) do |json|
json.content "hello"
end

assert_equal "hello", JSON.parse(json)["content"]
end

test "key_format! with parameter" do
json = JbuilderTemplate.new(binding)
json.key_format! :camelize => [:lower]
json.camel_style "for JS"

assert_equal ['camelStyle'], json.attributes!.keys
end

test "key_format! propagates to child elements" do
json = JbuilderTemplate.new(binding)
json.key_format! :upcase
json.level1 "one"
json.level2 do |json|
json.value "two"
end

result = json.attributes!
assert_equal "one", result["LEVEL1"]
assert_equal "two", result["LEVEL2"]["VALUE"]
end
end
Loading