breezy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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