Skip to content

Commit 1b3b173

Browse files
author
David Heinemeier Hansson
committed
Merge pull request #54 from rolftimmermans/performance
Substantial performance improvements
2 parents aa6efc7 + e5be60e commit 1b3b173

File tree

6 files changed

+180
-97
lines changed

6 files changed

+180
-97
lines changed

Gemfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
source "http://rubygems.org"
22

3-
gemspec
3+
gemspec
4+
5+
gem "actionpack"

Gemfile.lock

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,50 @@
11
PATH
22
remote: .
33
specs:
4-
jbuilder (0.3)
4+
jbuilder (0.4.1)
55
activesupport (>= 3.0.0)
66
blankslate (>= 2.1.2.4)
77

88
GEM
99
remote: http://rubygems.org/
1010
specs:
11-
activesupport (3.1.3)
11+
actionpack (3.2.8)
12+
activemodel (= 3.2.8)
13+
activesupport (= 3.2.8)
14+
builder (~> 3.0.0)
15+
erubis (~> 2.7.0)
16+
journey (~> 1.0.4)
17+
rack (~> 1.4.0)
18+
rack-cache (~> 1.2)
19+
rack-test (~> 0.6.1)
20+
sprockets (~> 2.1.3)
21+
activemodel (3.2.8)
22+
activesupport (= 3.2.8)
23+
builder (~> 3.0.0)
24+
activesupport (3.2.8)
25+
i18n (~> 0.6)
1226
multi_json (~> 1.0)
1327
blankslate (2.1.2.4)
14-
multi_json (1.0.4)
28+
builder (3.0.0)
29+
erubis (2.7.0)
30+
hike (1.2.1)
31+
i18n (0.6.1)
32+
journey (1.0.4)
33+
multi_json (1.3.6)
34+
rack (1.4.1)
35+
rack-cache (1.2)
36+
rack (>= 0.4)
37+
rack-test (0.6.1)
38+
rack (>= 1.0)
39+
sprockets (2.1.3)
40+
hike (~> 1.2)
41+
rack (~> 1.0)
42+
tilt (~> 1.1, != 1.3.0)
43+
tilt (1.3.3)
1544

1645
PLATFORMS
1746
ruby
1847

1948
DEPENDENCIES
49+
actionpack
2050
jbuilder!

lib/jbuilder.rb

Lines changed: 94 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,50 @@
55
require 'active_support/json'
66
require 'multi_json'
77
class Jbuilder < BlankSlate
8+
class KeyFormatter
9+
def initialize(*args)
10+
@format = {}
11+
@cache = {}
12+
13+
options = args.extract_options!
14+
args.each do |name|
15+
@format[name] = []
16+
end
17+
options.each do |name, paramaters|
18+
@format[name] = paramaters
19+
end
20+
end
21+
22+
def initialize_copy(original)
23+
@cache = {}
24+
end
25+
26+
def format(key)
27+
@cache[key] ||= @format.inject(key.to_s) do |result, args|
28+
func, args = args
29+
if func.is_a? Proc
30+
func.call(result, *args)
31+
else
32+
result.send(func, *args)
33+
end
34+
end
35+
end
36+
end
37+
838
# Yields a builder and automatically turns the result into a JSON string
939
def self.encode
1040
new._tap { |jbuilder| yield jbuilder }.target!
1141
end
1242

13-
@@key_format = {}
43+
@@key_formatter = KeyFormatter.new
1444

1545
define_method(:__class__, find_hidden_method(:class))
1646
define_method(:_tap, find_hidden_method(:tap))
17-
define_method(:_is_a?, find_hidden_method(:is_a?))
1847
reveal(:respond_to?)
1948

20-
def initialize(key_format = @@key_format.clone)
49+
def initialize(key_formatter = @@key_formatter.clone)
2150
@attributes = ActiveSupport::OrderedHash.new
22-
@key_format = key_format
51+
@key_formatter = key_formatter
2352
end
2453

2554
# Dynamically set a key value pair.
@@ -42,7 +71,7 @@ def set!(key, value = nil)
4271
if block_given?
4372
_yield_nesting(key) { |jbuilder| yield jbuilder }
4473
else
45-
@attributes[_format_key(key)] = value
74+
_set_value(key, value)
4675
end
4776
end
4877

@@ -75,12 +104,12 @@ def set!(key, value = nil)
75104
# { "_first_name": "David" }
76105
#
77106
def key_format!(*args)
78-
__class__.extract_key_format(args, @key_format)
107+
@key_formatter = KeyFormatter.new(*args)
79108
end
80109

81110
# Same as the instance method key_format! except sets the default.
82111
def self.key_format(*args)
83-
extract_key_format(args, @@key_format)
112+
@@key_formatter = KeyFormatter.new(*args)
84113
end
85114

86115
# Turns the current element into an array and yields a builder to add a hash.
@@ -129,12 +158,9 @@ def child!
129158
#
130159
# { "people": [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ] }
131160
def array!(collection)
132-
@attributes = [] and return if collection.empty?
133-
134-
collection.each do |element|
135-
child! do |child|
136-
yield child, element
137-
end
161+
@attributes = []
162+
collection.each do |element| #[] and return if collection.empty?
163+
@attributes << _new_instance._tap { |jbuilder| yield jbuilder, element }.attributes!
138164
end
139165
end
140166

@@ -156,22 +182,19 @@ def array!(collection)
156182
#
157183
# json.(@person, :name, :age)
158184
def extract!(object, *attributes)
159-
p = if object.is_a?(Hash)
160-
lambda{|attribute| __send__ attribute, object.send(:fetch, attribute)}
185+
if object.is_a?(Hash)
186+
attributes.each {|attribute| _set_value attribute, object.send(:fetch, attribute)}
161187
else
162-
lambda{|attribute| __send__ attribute, object.send(attribute)}
188+
attributes.each {|attribute| _set_value attribute, object.send(attribute)}
163189
end
164-
165-
attributes.each{|attribute| p.call(attribute)}
166190
end
167191

168192
if RUBY_VERSION > '1.9'
169-
def call(*args)
170-
case
171-
when args.one?
172-
array!(args.first) { |json, element| yield json, element }
173-
when args.many?
174-
extract!(*args)
193+
def call(object = nil, *attributes)
194+
if attributes.empty?
195+
array!(object) { |json, element| yield json, element }
196+
else
197+
extract!(object, *attributes)
175198
end
176199
end
177200
end
@@ -187,97 +210,79 @@ def target!
187210
end
188211

189212

190-
private
191-
def method_missing(method, *args)
192-
case
193-
# json.age 32
194-
# json.person another_jbuilder
195-
# { "age": 32, "person": { ... }
196-
when args.one? && args.first.respond_to?(:_is_a?) && args.first._is_a?(Jbuilder)
197-
set! method, args.first.attributes!
198-
199-
# json.comments @post.comments { |json, comment| ... }
200-
# { "comments": [ { ... }, { ... } ] }
201-
when args.one? && block_given?
202-
_yield_iteration(method, args.first) { |child, element| yield child, element }
203-
204-
# json.age 32
205-
# { "age": 32 }
206-
when args.length == 1
207-
set! method, args.first
208-
209-
# json.comments { |json| ... }
210-
# { "comments": ... }
211-
when args.empty? && block_given?
212-
_yield_nesting(method) { |jbuilder| yield jbuilder }
213+
protected
214+
def _set_value(key, value)
215+
@attributes[@key_formatter.format(key)] = value
216+
end
213217

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

219-
# json.author @post.creator, :name, :email_address
220-
# { "author": { "name": "David", "email_address": "[email protected]" } }
221-
when args.many?
222-
_inline_extract method, args.first, args.from(1)
219+
private
220+
def method_missing(method, value = nil, *args)
221+
if block_given?
222+
if value
223+
# json.comments @post.comments { |json, comment| ... }
224+
# { "comments": [ { ... }, { ... } ] }
225+
_yield_iteration(method, value) { |child, element| yield child, element }
226+
else
227+
# json.comments { |json| ... }
228+
# { "comments": ... }
229+
_yield_nesting(method) { |jbuilder| yield jbuilder }
230+
end
231+
else
232+
if args.empty?
233+
if Jbuilder === value
234+
# json.age 32
235+
# json.person another_jbuilder
236+
# { "age": 32, "person": { ... }
237+
_set_value method, value.attributes!
238+
else
239+
# json.age 32
240+
# { "age": 32 }
241+
_set_value method, value
242+
end
243+
else
244+
if value.respond_to?(:each)
245+
# json.comments(@post.comments, :content, :created_at)
246+
# { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
247+
_inline_nesting method, value, args
248+
else
249+
# json.author @post.creator, :name, :email_address
250+
# { "author": { "name": "David", "email_address": "[email protected]" } }
251+
_inline_extract method, value, args
252+
end
253+
end
223254
end
224255
end
225256

226257
# Overwrite in subclasses if you need to add initialization values
227258
def _new_instance
228-
__class__.new(@key_format)
259+
__class__.new(@key_formatter)
229260
end
230261

231262
def _yield_nesting(container)
232-
set! container, _new_instance._tap { |jbuilder| yield jbuilder }.attributes!
263+
_set_value container, _new_instance._tap { |jbuilder| yield jbuilder }.attributes!
233264
end
234265

235266
def _inline_nesting(container, collection, attributes)
236-
__send__(container) do |parent|
237-
parent.array!(collection) and return if collection.empty?
238-
239-
collection.each do |element|
240-
parent.child! do |child|
241-
attributes.each do |attribute|
242-
child.__send__ attribute, element.send(attribute)
243-
end
267+
_yield_nesting(container) do |parent|
268+
parent.array!(collection) do |child, element|
269+
attributes.each do |attribute|
270+
child._set_value attribute, element.send(attribute)
244271
end
245272
end
246273
end
247274
end
248275

249276
def _yield_iteration(container, collection)
250-
__send__(container) do |parent|
277+
_yield_nesting(container) do |parent|
251278
parent.array!(collection) do |child, element|
252279
yield child, element
253280
end
254281
end
255282
end
256283

257284
def _inline_extract(container, record, attributes)
258-
__send__(container) { |parent| parent.extract! record, *attributes }
259-
end
260-
261-
# Format the key using the methods described in @key_format
262-
def _format_key(key)
263-
@key_format.inject(key.to_s) do |result, args|
264-
func, args = args
265-
if func.is_a? Proc
266-
func.call(result, *args)
267-
else
268-
result.send(func, *args)
269-
end
270-
end
271-
end
272-
273-
def self.extract_key_format(args, target)
274-
options = args.extract_options!
275-
args.each do |name|
276-
target[name] = []
277-
end
278-
options.each do |name, paramaters|
279-
target[name] = paramaters
280-
end
285+
_yield_nesting(container) { |parent| parent.extract! record, *attributes }
281286
end
282287
end
283288

lib/jbuilder_template.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ def self.encode(context)
33
new(context)._tap { |jbuilder| yield jbuilder }.target!
44
end
55

6-
def initialize(context)
6+
def initialize(context, *args)
77
@context = context
8-
super()
8+
super(*args)
99
end
1010

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

2222
private
2323
def _new_instance
24-
__class__.new(@context)
24+
__class__.new(@context, @key_formatter)
2525
end
2626
end
2727

test/jbuilder_template_test.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
require 'test/unit'
2+
require 'active_support/test_case'
3+
require 'active_support/inflector'
4+
require 'action_dispatch'
5+
require 'action_view'
6+
7+
require 'jbuilder'
8+
require 'jbuilder_template'
9+
10+
class JbuilderTemplateTest < ActiveSupport::TestCase
11+
test "rendering" do
12+
json = JbuilderTemplate.encode(binding) do |json|
13+
json.content "hello"
14+
end
15+
16+
assert_equal "hello", JSON.parse(json)["content"]
17+
end
18+
19+
test "key_format! with parameter" do
20+
json = JbuilderTemplate.new(binding)
21+
json.key_format! :camelize => [:lower]
22+
json.camel_style "for JS"
23+
24+
assert_equal ['camelStyle'], json.attributes!.keys
25+
end
26+
27+
test "key_format! propagates to child elements" do
28+
json = JbuilderTemplate.new(binding)
29+
json.key_format! :upcase
30+
json.level1 "one"
31+
json.level2 do |json|
32+
json.value "two"
33+
end
34+
35+
result = json.attributes!
36+
assert_equal "one", result["LEVEL1"]
37+
assert_equal "two", result["LEVEL2"]["VALUE"]
38+
end
39+
end

0 commit comments

Comments
 (0)