breezy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ module Breezy
2
+ # For non-GET requests, sets a request_method cookie containing
3
+ # the request method of the current request. The Breezy script
4
+ # will not initialize if this cookie is set.
5
+ module Cookies
6
+ private
7
+ def set_request_method_cookie
8
+ if request.get?
9
+ cookies.delete(:request_method)
10
+ else
11
+ cookies[:request_method] = request.request_method
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ module Breezy
2
+ module Helpers
3
+ def breezy_tag
4
+ if defined?(@breezy) && @breezy
5
+ "<script type='text/javascript'>Breezy.replace(#{@breezy});</script>".html_safe
6
+ end
7
+ end
8
+
9
+ def breezy_snippet
10
+ if defined?(@breezy) && @breezy
11
+ snippet = @breezy.gsub(/\;$/, '')
12
+ "Breezy.replace(#{snippet});".html_safe
13
+ end
14
+ end
15
+
16
+ def use_breezy_html
17
+ @_use_breezy_html = true
18
+ end
19
+
20
+ def breezy_silient?
21
+ !!request.headers["X-SILENT"]
22
+ end
23
+
24
+ def breezy_filter
25
+ request.params[:_breezy_filter] || (session && session[:breezy_filter])
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ module Breezy
2
+ module Render
3
+ def default_render(*args)
4
+ if @_use_breezy_html
5
+ render(*args)
6
+ else
7
+ super
8
+ end
9
+ end
10
+
11
+ def render(*args, &block)
12
+ render_options = args.extract_options!
13
+ breezy = render_options.delete(:breezy)
14
+ breezy = {} if breezy == true || @_use_breezy_html
15
+
16
+ if breezy
17
+ view_parts = _prefixes.reverse.push(action_name)[1..-1]
18
+ view_name = view_parts.map(&:camelize).join
19
+
20
+ breezy[:view] ||= view_name
21
+ render_options[:locals] ||= {}
22
+ render_options[:locals][:breezy] = breezy
23
+ end
24
+
25
+ if @_use_breezy_html && request.format == :html
26
+ original_formats = self.formats
27
+
28
+ @breezy = render_to_string(*args, render_options.merge(formats: [:js]))
29
+ self.formats = original_formats
30
+ render_options.reverse_merge!(formats: original_formats, template: 'breezy/response')
31
+ end
32
+
33
+ super(*args, render_options, &block)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ module Breezy
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,22 @@
1
+ module Breezy
2
+ # Changes the response status to 403 Forbidden if all of these conditions are true:
3
+ # - The current request originated from Breezy
4
+ # - The request is being redirected to a different domain
5
+ module XDomainBlocker
6
+ private
7
+ def same_origin?(a, b)
8
+ a = URI.parse URI.escape(a)
9
+ b = URI.parse URI.escape(b)
10
+ [a.scheme, a.host, a.port] == [b.scheme, b.host, b.port]
11
+ end
12
+
13
+ def abort_xdomain_redirect
14
+ to_uri = response.headers['Location']
15
+ current = request.headers['X-XHR-Referer']
16
+ unless to_uri.blank? || current.blank? || same_origin?(current, to_uri)
17
+ self.status = 403
18
+ end
19
+ rescue URI::InvalidURIError
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,56 @@
1
+ module Breezy
2
+ # Intercepts calls to _compute_redirect_to_location (used by redirect_to) for two purposes.
3
+ #
4
+ # 1. Corrects the behavior of redirect_to with the :back option by using the X-XHR-Referer
5
+ # request header instead of the standard Referer request header.
6
+ #
7
+ # 2. Stores the return value (the redirect target url) to persist through to the redirect
8
+ # request, where it will be used to set the X-XHR-Redirected-To response header. The
9
+ # Breezy script will detect the header and use replaceState to reflect the redirected
10
+ # url.
11
+ module XHRHeaders
12
+ if Rails.version >= '5.0'
13
+ def redirect_back(fallback_location:, **args)
14
+ if referer = request.headers["X-XHR-Referer"]
15
+ rsp = redirect_to referer, **args
16
+ store_for_breezy(self.location)
17
+ rsp
18
+ else
19
+ super
20
+ end
21
+ end
22
+ end
23
+
24
+ def _compute_redirect_to_location(*args)
25
+ options, request = _normalize_redirect_params(args)
26
+
27
+ store_for_breezy begin
28
+ if options == :back && request.headers["X-XHR-Referer"]
29
+ super(*[(request if args.length == 2), request.headers["X-XHR-Referer"]].compact)
30
+ else
31
+ super(*args)
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+ def store_for_breezy(url)
38
+ session[:_breezy_redirect_to] = url if session && request.headers["X-XHR-Referer"]
39
+ url
40
+ end
41
+
42
+ def set_xhr_redirected_to
43
+ if session && session[:_breezy_redirect_to]
44
+ response.headers['X-XHR-Redirected-To'] = session.delete :_breezy_redirect_to
45
+ end
46
+ end
47
+
48
+ # Ensure backwards compatibility
49
+ # Rails < 4.2: _compute_redirect_to_location(options)
50
+ # Rails >= 4.2: _compute_redirect_to_location(request, options)
51
+ def _normalize_redirect_params(args)
52
+ options, req = args.reverse
53
+ [options, req || request]
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,13 @@
1
+ module Breezy
2
+ module XHRRedirect
3
+ def call(env)
4
+ status, headers, body = super(env)
5
+
6
+ if env['rack.session'] && env['HTTP_X_XHR_REFERER']
7
+ env['rack.session'][:_breezy_redirect_to] = headers['Location']
8
+ end
9
+
10
+ [status, headers, body]
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module Breezy
2
+ # Corrects the behavior of url_for (and link_to, which uses url_for) with the :back
3
+ # option by using the X-XHR-Referer request header instead of the standard Referer
4
+ # request header.
5
+ module XHRUrlFor
6
+ def url_for(options = {})
7
+ options = (controller.request.headers["X-XHR-Referer"] || options) if options == :back
8
+ super
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ require_relative "dummy/application"
2
+ Dummy::Application.initialize!
3
+
4
+ module Blade
5
+ module Server
6
+ private
7
+
8
+ def app
9
+ Rack::Builder.app do
10
+ use Rack::ShowExceptions
11
+
12
+ map '/app' do
13
+ run Dummy::Application
14
+ end
15
+
16
+ map '/' do
17
+ run Blade::RackAdapter.new
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,1025 @@
1
+ require "test_helper"
2
+ require "mocha/setup"
3
+ require "action_view"
4
+ require "action_view/testing/resolvers"
5
+ require "breezy_template"
6
+
7
+ BLOG_POST_PARTIAL = <<-JBUILDER
8
+ json.extract! blog_post, :id, :body
9
+ json.author do
10
+ first_name, last_name = blog_post.author_name.split(nil, 2)
11
+ json.first_name first_name
12
+ json.last_name last_name
13
+ end
14
+ JBUILDER
15
+
16
+ COLLECTION_PARTIAL = <<-JBUILDER
17
+ json.extract! collection, :id, :name
18
+ JBUILDER
19
+
20
+ PROFILE_PARTIAL = <<-JBUILDER
21
+ json.email email
22
+ JBUILDER
23
+
24
+ FOOTER_PARTIAL = <<-JBUILDER
25
+ json.terms "You agree"
26
+ JBUILDER
27
+
28
+ BlogPost = Struct.new(:id, :body, :author_name)
29
+ Collection = Struct.new(:id, :name)
30
+ blog_authors = [ "David Heinemeier Hansson", "Pavel Pravosud" ].cycle
31
+ BLOG_POST_COLLECTION = Array.new(10){ |i| BlogPost.new(i+1, "post body #{i+1}", blog_authors.next) }
32
+ COLLECTION_COLLECTION = Array.new(5){ |i| Collection.new(i+1, "collection #{i+1}") }
33
+
34
+ ActionView::Template.register_template_handler :breezy, BreezyTemplate::Handler
35
+
36
+ PARTIALS = {
37
+ "_partial.js.breezy" => "foo ||= 'hello'; json.content foo",
38
+ "_blog_post.js.breezy" => BLOG_POST_PARTIAL,
39
+ "_profile.js.breezy" => PROFILE_PARTIAL,
40
+ "_footer.js.breezy" => FOOTER_PARTIAL,
41
+ "_collection.js.breezy" => COLLECTION_PARTIAL
42
+ }
43
+
44
+ def strip_format(str)
45
+ str.strip_heredoc.gsub(/\n\s*/, "")
46
+ end
47
+
48
+ class BreezyTemplateTest < ActionView::TestCase
49
+ setup do
50
+ self.request_forgery = false
51
+ Breezy.configuration.track_assets = []
52
+
53
+ # this is a stub. Normally this would be set by the
54
+ # controller locals
55
+ self.breezy = {}
56
+
57
+ @context = self
58
+ Rails.cache.clear
59
+ end
60
+
61
+ cattr_accessor :request_forgery, :breezy
62
+ self.request_forgery = false
63
+
64
+ def breezy_filter
65
+ @breezy_filter
66
+ end
67
+
68
+ def request
69
+ @request
70
+ end
71
+
72
+ def jbuild(source, opts={})
73
+ @breezy_filter = opts[:breezy_filter]
74
+ @request = opts[:request] || action_controller_test_request
75
+ @rendered = []
76
+ partials = PARTIALS.clone
77
+ partials["test.js.breezy"] = source
78
+ resolver = ActionView::FixtureResolver.new(partials)
79
+ lookup_context.view_paths = [resolver]
80
+ lookup_context.formats = [:js]
81
+ template = ActionView::Template.new(source, "test", BreezyTemplate::Handler, virtual_path: "test")
82
+ template.render(self, {}).strip
83
+ end
84
+
85
+ def action_controller_test_request
86
+ if ::Rails.version.start_with?('5')
87
+ ::ActionController::TestRequest.create
88
+ else
89
+ ::ActionController::TestRequest.new
90
+ end
91
+ end
92
+
93
+ def cache_keys
94
+ major_v = Rails::VERSION::MAJOR
95
+ minor_v = Rails::VERSION::MINOR
96
+ rails_v = "rails#{major_v}#{minor_v}"
97
+ path = File.expand_path("../fixtures/cache_keys.yaml", __FILE__)
98
+ keys = YAML.load_file(path)
99
+ keys[method_name][rails_v]
100
+ end
101
+
102
+ def undef_context_methods(*names)
103
+ self.class_eval do
104
+ names.each do |name|
105
+ undef_method name.to_sym if method_defined?(name.to_sym)
106
+ end
107
+ end
108
+ end
109
+
110
+ def protect_against_forgery?
111
+ self.request_forgery
112
+ end
113
+
114
+ def form_authenticity_token
115
+ "secret"
116
+ end
117
+
118
+ test "rendering" do
119
+ result = jbuild(<<-JBUILDER)
120
+ json.content "hello"
121
+ JBUILDER
122
+
123
+ expected = strip_format(<<-JS)
124
+ (function(){
125
+ return ({"data":{"content":"hello"}});
126
+ })()
127
+ JS
128
+
129
+ assert_equal expected, result
130
+ end
131
+
132
+ test "when rendering with duplicate keys, the last one wins" do
133
+ result = jbuild(<<-JBUILDER)
134
+ json.content do
135
+ json.miss 123
136
+ end
137
+
138
+ json.content do
139
+ json.hit 123
140
+ end
141
+ JBUILDER
142
+
143
+
144
+ expected = strip_format(<<-JS)
145
+ (function(){
146
+ return ({"data":{"content":{"hit":123}}});
147
+ })()
148
+ JS
149
+
150
+ assert_equal expected, result
151
+ end
152
+
153
+ test "when rendering with duplicate array values, the last one wins" do
154
+ result = jbuild(<<-JBUILDER)
155
+ json.content do
156
+ json.array! [1,2]
157
+ json.array! [3,4]
158
+ end
159
+ JBUILDER
160
+
161
+ expected = strip_format(<<-JS)
162
+ (function(){
163
+ return ({\"data\":{\"content\":[3,4]}});
164
+ })()
165
+ JS
166
+
167
+ assert_equal expected, result
168
+ end
169
+
170
+ test "render with asset tracking" do
171
+ Breezy.configuration.track_assets = ['test.js', 'test.css']
172
+
173
+ result = jbuild(<<-TEMPLATE)
174
+ json.content "hello"
175
+ TEMPLATE
176
+
177
+ expected = strip_format(<<-JS)
178
+ (function(){
179
+ return ({"data":{"content":"hello"},"assets":["/test.js","/test.css"]});
180
+ })()
181
+ JS
182
+
183
+ assert_equal expected, result
184
+ end
185
+
186
+
187
+ test "render with csrf token when request forgery is on" do
188
+ self.request_forgery = true
189
+ # csrf_meta_tags also delegate authenticity tokens to the controller
190
+ # here we provide a simple mock to the context
191
+
192
+ result = jbuild(<<-TEMPLATE)
193
+ json.content "hello"
194
+ TEMPLATE
195
+
196
+ expected = strip_format(<<-JS)
197
+ (function(){
198
+ return ({"data":{"content":"hello"},"csrf_token":"secret"});
199
+ })()
200
+ JS
201
+
202
+ assert_equal expected, result
203
+ end
204
+
205
+ test "wrapping jbuilder contents inside Breezy with additional options" do
206
+ Breezy.configuration.track_assets = ['test.js', 'test.css']
207
+ self.breezy = { title: 'this is fun' }
208
+
209
+ result = jbuild(<<-TEMPLATE)
210
+ json.content "hello"
211
+ TEMPLATE
212
+
213
+ expected = strip_format(<<-JS)
214
+ (function(){
215
+ return ({"data":{"content":"hello"},"title":"this is fun","assets":["/test.js","/test.css"]});
216
+ })()
217
+ JS
218
+
219
+ assert_equal expected, result
220
+ end
221
+
222
+ test "key_format! with parameter" do
223
+ result = jbuild(<<-JBUILDER)
224
+ json.key_format! camelize: [:lower]
225
+ json.camel_style "for JS"
226
+ JBUILDER
227
+
228
+ expected = strip_format(<<-JS)
229
+ (function(){
230
+ return ({"data":{"camelStyle":"for JS"}});
231
+ })()
232
+ JS
233
+
234
+ assert_equal expected, result
235
+ end
236
+
237
+ test "key_format! propagates to child elements" do
238
+ result = jbuild(<<-JBUILDER)
239
+ json.key_format! :upcase
240
+ json.level1 "one"
241
+ json.level2 do
242
+ json.value "two"
243
+ end
244
+ JBUILDER
245
+
246
+ expected = strip_format(<<-JS)
247
+ (function(){
248
+ return ({"data":{
249
+ "LEVEL1":"one",
250
+ "LEVEL2":{"VALUE":"two"}
251
+ }});
252
+ })()
253
+ JS
254
+
255
+ assert_equal expected, result
256
+ end
257
+
258
+ test "renders partial via the option through set!" do
259
+ @post = BLOG_POST_COLLECTION.first
260
+ Rails.cache.clear
261
+
262
+ result = jbuild(<<-JBUILDER)
263
+ json.post @post, partial: "blog_post", as: :blog_post
264
+ JBUILDER
265
+
266
+ expected = strip_format(<<-JS)
267
+ (function(){
268
+ return ({"data":{"post":{
269
+ "id":1,
270
+ "body":"post body 1",
271
+ "author":{"first_name":"David","last_name":"Heinemeier Hansson"}
272
+ }}});
273
+ })()
274
+ JS
275
+
276
+ assert_equal expected, result
277
+ end
278
+
279
+ test "renders a partial with no locals" do
280
+ result = jbuild(<<-JBUILDER)
281
+ json.footer partial: "footer"
282
+ JBUILDER
283
+
284
+ expected = strip_format(<<-JS)
285
+ (function(){
286
+ return ({"data":{"footer":{"terms":"You agree"}}});
287
+ })()
288
+ JS
289
+ assert_equal expected, result
290
+ end
291
+
292
+ test "renders a partial with locals" do
293
+ result = jbuild(<<-JBUILDER)
294
+ json.profile partial: "profile", locals: {email: "test@test.com"}
295
+ JBUILDER
296
+
297
+ expected = strip_format(<<-JS)
298
+ (function(){
299
+ return ({"data":{"profile":{"email":"test@test.com"}}});
300
+ })()
301
+ JS
302
+ assert_equal expected, result
303
+ end
304
+
305
+ test "renders a partial with locals and caches" do
306
+ result = jbuild(<<-JBUILDER)
307
+ json.profile 32, cache: "cachekey", partial: "profile", locals: {email: "test@test.com"}
308
+ JBUILDER
309
+
310
+ expected = strip_format(<<-JS)
311
+ (function(){
312
+ Breezy.cache("#{cache_keys[0]}", {"email":"test@test.com"});
313
+ return ({"data":{"profile":Breezy.cache("#{cache_keys[0]}")}});
314
+ })()
315
+ JS
316
+
317
+ assert_equal expected, result
318
+ end
319
+
320
+ test "renders a partial even without a :as to the value, this usage is rare" do
321
+ result = jbuild(<<-JBUILDER)
322
+ json.profile 32, partial: "profile", locals: {email: "test@test.com"}
323
+ JBUILDER
324
+
325
+ expected = strip_format(<<-JS)
326
+ (function(){
327
+ return ({"data":{"profile":{"email":"test@test.com"}}});
328
+ })()
329
+ JS
330
+
331
+ assert_equal expected, result
332
+ end
333
+
334
+ test "render array of partials without an :as to a member, this usage is very rare" do
335
+ result = jbuild(<<-JBUILDER)
336
+ json.array! [1,2], partial: "footer"
337
+ JBUILDER
338
+
339
+ expected = strip_format(<<-JS)
340
+ (function(){
341
+ return ({"data":[{"terms":"You agree"},{"terms":"You agree"}]});
342
+ })()
343
+ JS
344
+
345
+ assert_equal expected, result
346
+ end
347
+
348
+ test "render array of partials without an :as to a member and cache" do
349
+ result = jbuild(<<-JBUILDER)
350
+ json.array! [1,2], partial: "footer", cache: ->(i){ ['a', i] }
351
+ JBUILDER
352
+
353
+ expected = strip_format(<<-JS)
354
+ (function(){
355
+ Breezy.cache("#{cache_keys[0]}", {"terms":"You agree"});
356
+ Breezy.cache("#{cache_keys[1]}", {"terms":"You agree"});
357
+ return ({"data":[Breezy.cache("#{cache_keys[0]}"),Breezy.cache("#{cache_keys[1]}")]});
358
+ })()
359
+ JS
360
+
361
+ assert_equal expected, result
362
+ end
363
+
364
+ test "render array of partials" do
365
+ result = jbuild(<<-JBUILDER)
366
+ json.array! BLOG_POST_COLLECTION, partial: "blog_post", as: :blog_post
367
+ JBUILDER
368
+
369
+ expected = strip_format(<<-JS)
370
+ (function(){
371
+ return ({"data":[
372
+ {"id":1,"body":"post body 1","author":{"first_name":"David","last_name":"Heinemeier Hansson"}},
373
+ {"id":2,"body":"post body 2","author":{"first_name":"Pavel","last_name":"Pravosud"}},
374
+ {"id":3,"body":"post body 3","author":{"first_name":"David","last_name":"Heinemeier Hansson"}},
375
+ {"id":4,"body":"post body 4","author":{"first_name":"Pavel","last_name":"Pravosud"}},
376
+ {"id":5,"body":"post body 5","author":{"first_name":"David","last_name":"Heinemeier Hansson"}},
377
+ {"id":6,"body":"post body 6","author":{"first_name":"Pavel","last_name":"Pravosud"}},
378
+ {"id":7,"body":"post body 7","author":{"first_name":"David","last_name":"Heinemeier Hansson"}},
379
+ {"id":8,"body":"post body 8","author":{"first_name":"Pavel","last_name":"Pravosud"}},
380
+ {"id":9,"body":"post body 9","author":{"first_name":"David","last_name":"Heinemeier Hansson"}},
381
+ {"id":10,"body":"post body 10","author":{"first_name":"Pavel","last_name":"Pravosud"}}
382
+ ]});
383
+ })()
384
+ JS
385
+
386
+ assert_equal expected, result
387
+ end
388
+
389
+ test "renders array of partials as empty array with nil-collection" do
390
+ result = jbuild(<<-JBUILDER)
391
+ json.array! nil, partial: "blog_post", as: :blog_post
392
+ JBUILDER
393
+
394
+ expected = strip_format(<<-JS)
395
+ (function(){
396
+ return ({"data":[]});
397
+ })()
398
+ JS
399
+
400
+ assert_equal expected, result
401
+ end
402
+
403
+ test "renders array of partials via set!" do
404
+ result = jbuild(<<-JBUILDER)
405
+ json.posts BLOG_POST_COLLECTION, partial: "blog_post", as: :blog_post
406
+ JBUILDER
407
+
408
+ expected = strip_format(<<-JS)
409
+ (function(){
410
+ return ({"data":{"posts":[
411
+ {"id":1,"body":"post body 1","author":{"first_name":"David","last_name":"Heinemeier Hansson"}},
412
+ {"id":2,"body":"post body 2","author":{"first_name":"Pavel","last_name":"Pravosud"}},
413
+ {"id":3,"body":"post body 3","author":{"first_name":"David","last_name":"Heinemeier Hansson"}},
414
+ {"id":4,"body":"post body 4","author":{"first_name":"Pavel","last_name":"Pravosud"}},
415
+ {"id":5,"body":"post body 5","author":{"first_name":"David","last_name":"Heinemeier Hansson"}},
416
+ {"id":6,"body":"post body 6","author":{"first_name":"Pavel","last_name":"Pravosud"}},
417
+ {"id":7,"body":"post body 7","author":{"first_name":"David","last_name":"Heinemeier Hansson"}},
418
+ {"id":8,"body":"post body 8","author":{"first_name":"Pavel","last_name":"Pravosud"}},
419
+ {"id":9,"body":"post body 9","author":{"first_name":"David","last_name":"Heinemeier Hansson"}},
420
+ {"id":10,"body":"post body 10","author":{"first_name":"Pavel","last_name":"Pravosud"}}
421
+ ]}});
422
+ })()
423
+ JS
424
+
425
+ assert_equal expected, result
426
+ end
427
+
428
+ test "render as empty array if partials as a nil value" do
429
+ result = jbuild <<-JBUILDER
430
+ json.posts nil, partial: "blog_post", as: :blog_post
431
+ JBUILDER
432
+
433
+ expected = strip_format(<<-JS)
434
+ (function(){
435
+ return ({"data":{"posts":[]}});
436
+ })()
437
+ JS
438
+ assert_equal expected, result
439
+ end
440
+
441
+ test "caching a value at a node" do
442
+ undef_context_methods :fragment_name_with_digest, :cache_fragment_name
443
+
444
+ result = jbuild(<<-JBUILDER)
445
+ json.hello(32, cache: ['b', 'c'])
446
+ JBUILDER
447
+
448
+ expected = strip_format(<<-JS)
449
+ (function(){
450
+ Breezy.cache("#{cache_keys[0]}", 32);
451
+ return ({"data":{"hello":Breezy.cache("#{cache_keys[0]}")}});
452
+ })()
453
+ JS
454
+
455
+ assert_equal expected, result
456
+ end
457
+
458
+ test "caching elements in a list" do
459
+ undef_context_methods :fragment_name_with_digest, :cache_fragment_name
460
+
461
+ result = jbuild(<<-JBUILDER)
462
+ json.hello do
463
+ json.array! [4,5], cache: ->(i){ ['a', i] } do |x|
464
+ json.top "hello" + x.to_s
465
+ end
466
+ end
467
+ JBUILDER
468
+
469
+ expected = strip_format(<<-JS)
470
+ (function(){
471
+ Breezy.cache("#{cache_keys[0]}", {"top":"hello4"});
472
+ Breezy.cache("#{cache_keys[1]}", {"top":"hello5"});
473
+ return ({"data":{"hello":[Breezy.cache("#{cache_keys[0]}"),Breezy.cache("#{cache_keys[1]}")]}});
474
+ })()
475
+ JS
476
+
477
+ assert_equal expected, result
478
+ end
479
+
480
+ test "nested caching generates a depth-first list of cache nodes" do
481
+ undef_context_methods :fragment_name_with_digest, :cache_fragment_name
482
+
483
+ result = jbuild(<<-JBUILDER)
484
+ json.hello(cache: ['a', 'b']) do
485
+ json.content(cache: ['d', 'z']) do
486
+ json.subcontent 'inner'
487
+ end
488
+ json.other(cache: ['e', 'z']) do
489
+ json.subcontent 'other'
490
+ end
491
+ end
492
+ JBUILDER
493
+
494
+ expected = strip_format(<<-JS)
495
+ (function(){
496
+ Breezy.cache("#{cache_keys[0]}", {"subcontent":"inner"});
497
+ Breezy.cache("#{cache_keys[1]}", {"subcontent":"other"});
498
+ Breezy.cache("#{cache_keys[2]}", {"content":Breezy.cache("#{cache_keys[0]}"),"other":Breezy.cache("#{cache_keys[1]}")});
499
+ return ({"data":{"hello":Breezy.cache("#{cache_keys[2]}")}});
500
+ })()
501
+ JS
502
+
503
+ assert_equal expected, result
504
+ end
505
+
506
+ test "caching an empty block generates no cache and no errors" do
507
+ undef_context_methods :fragment_name_with_digest, :cache_fragment_name
508
+
509
+ result = nil
510
+
511
+ assert_nothing_raised do
512
+ result = jbuild(<<-JBUILDER)
513
+ json.hello do
514
+ json.array! [4,5], cache: ->(i){['a', i]} do |x|
515
+ end
516
+ end
517
+ JBUILDER
518
+ end
519
+
520
+ expected = strip_format(<<-JS)
521
+ (function(){
522
+ return ({\"data\":{\"hello\":[]}});
523
+ })()
524
+ JS
525
+
526
+ assert_equal expected, result
527
+ end
528
+
529
+ test "child! accepts cache options" do
530
+ undef_context_methods :fragment_name_with_digest, :cache_fragment_name
531
+
532
+ result = jbuild(<<-JBUILDER)
533
+ json.comments do
534
+ json.child!(cache: ['e', 'z']) { json.content "hello" }
535
+ json.child! { json.content "world" }
536
+ end
537
+ JBUILDER
538
+
539
+ expected = strip_format(<<-JS)
540
+ (function(){
541
+ Breezy.cache("#{cache_keys[0]}", {"content":"hello"});
542
+ return ({"data":{"comments":[Breezy.cache("#{cache_keys[0]}"),{"content":"world"}]}});
543
+ })()
544
+ JS
545
+
546
+ assert_equal expected, result
547
+ end
548
+
549
+ test "fragment caching" do
550
+ undef_context_methods :fragment_name_with_digest, :cache_fragment_name
551
+
552
+ jbuild(<<-JBUILDER)
553
+ json.post(cache: 'cachekey') do
554
+ json.name "Cache"
555
+ end
556
+ JBUILDER
557
+
558
+ result = jbuild(<<-JBUILDER)
559
+ json.post(cache: 'cachekey') do
560
+ json.name "Miss"
561
+ end
562
+ JBUILDER
563
+
564
+ expected = strip_format(<<-JS)
565
+ (function(){
566
+ Breezy.cache("#{cache_keys[0]}", {"name":"Cache"});
567
+ return ({"data":{"post":Breezy.cache("#{cache_keys[0]}")}});
568
+ })()
569
+ JS
570
+
571
+ assert_equal expected, result
572
+ end
573
+
574
+ test "fragment caching deserializes an array" do
575
+ undef_context_methods :fragment_name_with_digest, :cache_fragment_name
576
+
577
+ result = jbuild <<-JBUILDER
578
+ json.content(cache: "cachekey") do
579
+ json.array! %w[a b c]
580
+ end
581
+ JBUILDER
582
+
583
+ expected = strip_format(<<-JS)
584
+ (function(){
585
+ Breezy.cache("#{cache_keys[0]}", ["a","b","c"]);
586
+ return ({"data":{"content":Breezy.cache("#{cache_keys[0]}")}});
587
+ })()
588
+ JS
589
+
590
+ assert_equal expected, result
591
+ end
592
+
593
+ test "fragment caching works with previous version of cache digests" do
594
+ undef_context_methods :cache_fragment_name
595
+
596
+ if !@context.class.method_defined? :fragment_name_with_digest
597
+ @context.class_eval do
598
+ def fragment_name_with_digest
599
+ end
600
+ end
601
+ end
602
+
603
+ @context.expects :fragment_name_with_digest
604
+
605
+ jbuild <<-JBUILDER
606
+ json.content(cache: "cachekey") do
607
+ json.name "Cache"
608
+ end
609
+ JBUILDER
610
+ end
611
+
612
+ test "fragment caching works with current cache digests" do
613
+ undef_context_methods :fragment_name_with_digest
614
+
615
+ @context.expects :cache_fragment_name
616
+ ActiveSupport::Cache.expects :expand_cache_key
617
+
618
+ jbuild <<-JBUILDER
619
+ json.content(cache: "cachekey") do
620
+ json.name "Cache"
621
+ end
622
+ JBUILDER
623
+ end
624
+
625
+ test "current cache digest option accepts options through the last element hash" do
626
+ undef_context_methods :fragment_name_with_digest
627
+
628
+ @context.expects(:cache_fragment_name)
629
+ .with("cachekey", skip_digest: true)
630
+ .returns("cachekey")
631
+
632
+ ActiveSupport::Cache.expects :expand_cache_key
633
+
634
+ jbuild <<-JBUILDER
635
+ json.content(cache: ["cachekey", skip_digest: true]) do
636
+ json.name "Cache"
637
+ end
638
+ JBUILDER
639
+ end
640
+
641
+ test "does not perform caching when controller.perform_caching is false" do
642
+ controller.perform_caching = false
643
+
644
+ result = jbuild <<-JBUILDER
645
+ json.content(cache: "cachekey") do
646
+ json.name "Cache"
647
+ end
648
+ JBUILDER
649
+
650
+ expected = strip_format(<<-JS)
651
+ (function(){
652
+ return ({"data":{"content":{"name":"Cache"}}});
653
+ })()
654
+ JS
655
+
656
+ assert_equal expected, result
657
+ end
658
+
659
+ test "invokes templates via params via set! and caches" do
660
+ @post = BLOG_POST_COLLECTION.first
661
+
662
+ result = jbuild(<<-JBUILDER)
663
+ json.post @post, partial: "blog_post", as: :blog_post, cache: ['a', 'b']
664
+ JBUILDER
665
+
666
+ expected = strip_format(<<-JS)
667
+ (function(){
668
+ Breezy.cache("#{cache_keys[0]}", {"id":1,"body":"post body 1","author":{"first_name":"David","last_name":"Heinemeier Hansson"}});
669
+ return ({"data":{"post":Breezy.cache("#{cache_keys[0]}")}});
670
+ })()
671
+ JS
672
+
673
+ assert_equal expected, result
674
+ end
675
+
676
+ test "shares partial caches (via the partial's digest) across multiple templates" do
677
+ @hit = BlogPost.new(1, "hit", "John Smith")
678
+ @miss = BlogPost.new(2, "miss", "John Smith")
679
+
680
+ jbuild(<<-JBUILDER)
681
+ json.post @hit, partial: "blog_post", as: :blog_post, cache: ['a', 'b']
682
+ JBUILDER
683
+
684
+ result = jbuild(<<-JBUILDER)
685
+ json.post @miss, partial: "blog_post", as: :blog_post, cache: ['a', 'b']
686
+ JBUILDER
687
+
688
+ expected = strip_format(<<-JS)
689
+ (function(){
690
+ Breezy.cache("#{cache_keys[0]}", {"id":1,"body":"hit","author":{"first_name":"John","last_name":"Smith"}});
691
+ return ({"data":{"post":Breezy.cache("#{cache_keys[0]}")}});
692
+ })()
693
+ JS
694
+
695
+ assert_equal expected, result
696
+ end
697
+
698
+
699
+ test "render array of partials and caches" do
700
+ result = jbuild(<<-JBUILDER)
701
+ json.array! BLOG_POST_COLLECTION, partial: "blog_post", as: :blog_post, cache: ->(d){ ['a', d.id] }
702
+ JBUILDER
703
+ Rails.cache.clear
704
+
705
+ expected = strip_format(<<-JS)
706
+ (function(){
707
+ Breezy.cache("#{cache_keys[0]}", {"id":1,"body":"post body 1","author":{"first_name":"David","last_name":"Heinemeier Hansson"}});
708
+ Breezy.cache("#{cache_keys[1]}", {"id":2,"body":"post body 2","author":{"first_name":"Pavel","last_name":"Pravosud"}});
709
+ Breezy.cache("#{cache_keys[2]}", {"id":3,"body":"post body 3","author":{"first_name":"David","last_name":"Heinemeier Hansson"}});
710
+ Breezy.cache("#{cache_keys[3]}", {"id":4,"body":"post body 4","author":{"first_name":"Pavel","last_name":"Pravosud"}});
711
+ Breezy.cache("#{cache_keys[4]}", {"id":5,"body":"post body 5","author":{"first_name":"David","last_name":"Heinemeier Hansson"}});
712
+ Breezy.cache("#{cache_keys[5]}", {"id":6,"body":"post body 6","author":{"first_name":"Pavel","last_name":"Pravosud"}});
713
+ Breezy.cache("#{cache_keys[6]}", {"id":7,"body":"post body 7","author":{"first_name":"David","last_name":"Heinemeier Hansson"}});
714
+ Breezy.cache("#{cache_keys[7]}", {"id":8,"body":"post body 8","author":{"first_name":"Pavel","last_name":"Pravosud"}});
715
+ Breezy.cache("#{cache_keys[8]}", {"id":9,"body":"post body 9","author":{"first_name":"David","last_name":"Heinemeier Hansson"}});
716
+ Breezy.cache("#{cache_keys[9]}", {"id":10,"body":"post body 10","author":{"first_name":"Pavel","last_name":"Pravosud"}});
717
+ return ({"data":[Breezy.cache("#{cache_keys[0]}"),Breezy.cache("#{cache_keys[1]}"),Breezy.cache("#{cache_keys[2]}"),Breezy.cache("#{cache_keys[3]}"),Breezy.cache("#{cache_keys[4]}"),Breezy.cache("#{cache_keys[5]}"),Breezy.cache("#{cache_keys[6]}"),Breezy.cache("#{cache_keys[7]}"),Breezy.cache("#{cache_keys[8]}"),Breezy.cache("#{cache_keys[9]}")]});
718
+ })()
719
+ JS
720
+
721
+ assert_equal expected, result
722
+ end
723
+
724
+ test "filtering for a node in the tree" do
725
+ result = jbuild(<<-JBUILDER)
726
+ json._filter_by_path('hit.hit2')
727
+ json.hit do
728
+ json.hit2 do
729
+ json.greeting 'hello world'
730
+ end
731
+ end
732
+
733
+ json.miss do
734
+ json.miss2 do
735
+ raise 'this should not be called'
736
+ json.greeting 'missed call'
737
+ end
738
+ end
739
+ JBUILDER
740
+ Rails.cache.clear
741
+
742
+ expected = strip_format(<<-JS)
743
+ (function(){
744
+ return (
745
+ {"data":{"greeting":"hello world"}}
746
+ );
747
+ })()
748
+ JS
749
+
750
+ assert_equal expected, result
751
+ end
752
+
753
+ test "filtering for a node in the tree via breezy_filter helper" do
754
+ result = jbuild(<<-JBUILDER, breezy_filter: 'hit.hit2')
755
+ json.hit do
756
+ json.hit2 do
757
+ json.greeting 'hello world'
758
+ end
759
+ end
760
+
761
+ json.miss do
762
+ json.miss2 do
763
+ raise 'this should not be called'
764
+ json.greeting 'missed call'
765
+ end
766
+ end
767
+ JBUILDER
768
+ Rails.cache.clear
769
+
770
+ expected = strip_format(<<-JS)
771
+ (function(){
772
+ return (
773
+ {"data":{"greeting":"hello world"},"action":"graft","path":"hit.hit2"}
774
+ );
775
+ })()
776
+ JS
777
+
778
+ assert_equal expected, result
779
+ end
780
+
781
+ test "filtering a cached node returns just that" do
782
+ undef_context_methods :fragment_name_with_digest, :cache_fragment_name
783
+ result = jbuild(<<-JBUILDER, breezy_filter: 'hit.hit2')
784
+ json.hit do
785
+ json.hit2(cache: 'a') do
786
+ json.greeting 'hello world'
787
+ end
788
+ end
789
+ JBUILDER
790
+ Rails.cache.clear
791
+
792
+ expected = strip_format(<<-JS)
793
+ (function(){
794
+ Breezy.cache("#{cache_keys[0]}", {"greeting":"hello world"});
795
+ return ({"data":Breezy.cache("219dfba9f552f91402a22cf67c633582"),"action":"graft","path":"hit.hit2"});
796
+ })()
797
+
798
+
799
+ JS
800
+
801
+ assert_equal expected, result
802
+ end
803
+
804
+ test "filtering for a node of a AR relation in a tree by id via an appended where clause" do
805
+ result = jbuild(<<-JBUILDER, breezy_filter: 'hit.hit2.id=1')
806
+ post = Post.create
807
+ post.comments.create title: 'first'
808
+ post.comments.create title: 'second'
809
+
810
+ post.comments.expects(:where).once().with('id'=>1).returns([{id: 1, title: 'first'}])
811
+
812
+ json.hit do
813
+ json.hit2 do
814
+ json.array! post.comments do |x|
815
+ raise 'this should be be called' if x[:title] == 'second'
816
+ json.title x[:title]
817
+ end
818
+ end
819
+ end
820
+ JBUILDER
821
+
822
+ Rails.cache.clear
823
+
824
+ expected = strip_format(<<-JS)
825
+ (function(){
826
+ return (
827
+ {"data":[{"title":"first"}],"action":"graft","path":"hit.hit2.id=1"}
828
+ );
829
+ })()
830
+ JS
831
+ assert_equal expected, result
832
+ end
833
+
834
+
835
+ test "filtering for a node of a AR relation in a tree by index via an appended where clause" do
836
+ result = jbuild(<<-JBUILDER, breezy_filter: 'hit.hit2.0')
837
+ post = Post.create
838
+ post.comments.create title: 'first'
839
+ post.comments.create title: 'second'
840
+
841
+ offset = post.comments.offset(0)
842
+ post.comments.expects(:offset).once().with(0).returns(offset)
843
+
844
+ json.hit do
845
+ json.hit2 do
846
+ json.array! post.comments do |x|
847
+ raise 'this should be be called' if x[:title] == 'second'
848
+ json.title x[:title]
849
+ end
850
+ end
851
+ end
852
+ JBUILDER
853
+
854
+ Rails.cache.clear
855
+
856
+ expected = strip_format(<<-JS)
857
+ (function(){
858
+ return (
859
+ {"data":[{"title":"first"}],"action":"graft","path":"hit.hit2.0"}
860
+ );
861
+ })()
862
+ JS
863
+ assert_equal expected, result
864
+ end
865
+
866
+ test "filtering for a node in an array of a tree by id" do
867
+ result = jbuild(<<-JBUILDER, breezy_filter: 'hit.hit2.id=1')
868
+ json.hit do
869
+ json.hit2 do
870
+ json.array! [{id: 1, name: 'hit' }, {id:2, name: 'miss'}] do |x|
871
+ raise 'this should be be called' if x[:name] == 'miss'
872
+ json.name x[:name]
873
+ end
874
+ end
875
+ end
876
+ JBUILDER
877
+ Rails.cache.clear
878
+
879
+ expected = strip_format(<<-JS)
880
+ (function(){
881
+ return (
882
+ {"data":[{"name":"hit"}],"action":"graft","path":"hit.hit2.id=1"}
883
+ );
884
+ })()
885
+ JS
886
+
887
+ assert_equal expected, result
888
+ end
889
+
890
+ test "filtering for a node in an array of a tree by index" do
891
+ result = jbuild(<<-JBUILDER, breezy_filter: 'hit.hit2.0')
892
+ json.hit do
893
+ json.hit2 do
894
+ json.array! [{id: 1, name: 'hit' }, {id:2, name: 'miss'}] do |x|
895
+ raise 'this should be be called' if x[:name] == 'miss'
896
+ json.name x[:name]
897
+ end
898
+ end
899
+ end
900
+ JBUILDER
901
+ Rails.cache.clear
902
+
903
+ expected = strip_format(<<-JS)
904
+ (function(){
905
+ return (
906
+ {"data":[{"name":"hit"}],"action":"graft","path":"hit.hit2.0"}
907
+ );
908
+ })()
909
+ JS
910
+
911
+ assert_equal expected, result
912
+ end
913
+
914
+ test "rendering with node deferement" do
915
+ req = action_controller_test_request
916
+ req.path = '/some_url'
917
+
918
+ result = jbuild(<<-JBUILDER, request: req)
919
+ json.hit do
920
+ json.hit2 defer: true do
921
+ json.hit3 do
922
+ json.greeting 'hello world'
923
+ end
924
+ end
925
+ end
926
+ JBUILDER
927
+ Rails.cache.clear
928
+
929
+ expected = strip_format(<<-JS)
930
+ (function(){
931
+ Breezy.visit('/some_url?_breezy_filter=hit.hit2', {async: true, pushState: false});
932
+ return (
933
+ {"data":{"hit":{"hit2":null}}}
934
+ );
935
+ })()
936
+ JS
937
+
938
+ assert_equal expected, result
939
+ end
940
+
941
+ test "rendering with node array deferment" do
942
+ req = action_controller_test_request
943
+ req.path = '/some_url'
944
+
945
+ result = jbuild(<<-JBUILDER, request: req)
946
+ json.hit do
947
+ json.hit2 do
948
+ data = [{id: 1, name: 'foo'}, {id: 2, name: 'bar'}]
949
+ json.array! data, key: :id do
950
+ json.greeting defer: true do
951
+ json.gree 'hi'
952
+ end
953
+ end
954
+ end
955
+ end
956
+ JBUILDER
957
+ Rails.cache.clear
958
+
959
+ expected = strip_format(<<-JS)
960
+ (function(){
961
+ Breezy.visit('/some_url?_breezy_filter=hit.hit2.id%3D1.greeting', {async: true, pushState: false});
962
+ Breezy.visit('/some_url?_breezy_filter=hit.hit2.id%3D2.greeting', {async: true, pushState: false});
963
+ return (
964
+ {"data":{"hit":{"hit2":[{"greeting":null},{"greeting":null}]}}}
965
+ );
966
+ })()
967
+ JS
968
+
969
+ assert_equal expected, result
970
+ end
971
+
972
+ test 'deferment does not work on values' do
973
+ undef_context_methods :fragment_name_with_digest, :cache_fragment_name
974
+
975
+ result = jbuild(<<-JBUILDER)
976
+ json.hello(32, defer: true)
977
+ JBUILDER
978
+
979
+ expected = strip_format(<<-JS)
980
+ (function(){
981
+ return ({"data":{"hello":32}});
982
+ })()
983
+ JS
984
+
985
+ assert_equal expected, result
986
+ end
987
+
988
+ test 'deferment is disabled when filtering by keypath' do
989
+ undef_context_methods :fragment_name_with_digest, :cache_fragment_name
990
+ result = jbuild(<<-JBUILDER, breezy_filter: 'hello.world')
991
+ json.hello defer: true do
992
+ json.world 32
993
+ end
994
+ JBUILDER
995
+
996
+ expected = strip_format(<<-JS)
997
+ (function(){
998
+ return ({"data":{"world":32},"action":"graft","path":"hello.world"});
999
+ })()
1000
+ JS
1001
+
1002
+ assert_equal expected, result
1003
+
1004
+ end
1005
+
1006
+ test 'deferment is enabled at the end of a keypath when filtering' do
1007
+ undef_context_methods :fragment_name_with_digest, :cache_fragment_name
1008
+ result = jbuild(<<-JBUILDER, breezy_filter: 'hello')
1009
+ json.hello defer: true do
1010
+ json.content defer: true do
1011
+ json.world 32
1012
+ end
1013
+ end
1014
+ JBUILDER
1015
+
1016
+ expected = strip_format(<<-JS)
1017
+ (function(){
1018
+ Breezy.visit('?_breezy_filter=hello.content', {async: true, pushState: false});
1019
+ return ({"data":{"content":null},"action":"graft","path":"hello"});
1020
+ })()
1021
+ JS
1022
+
1023
+ assert_equal expected, result
1024
+ end
1025
+ end