render_sync 0.5.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.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +153 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +22 -0
  5. data/README.md +521 -0
  6. data/Rakefile +9 -0
  7. data/app/assets/javascripts/sync.coffee +355 -0
  8. data/app/controllers/sync/refetches_controller.rb +56 -0
  9. data/app/helpers/render_sync/config_helper.rb +15 -0
  10. data/config/routes.rb +3 -0
  11. data/config/sync.yml +21 -0
  12. data/lib/generators/render_sync/install_generator.rb +14 -0
  13. data/lib/generators/render_sync/templates/sync.ru +14 -0
  14. data/lib/generators/render_sync/templates/sync.yml +34 -0
  15. data/lib/render_sync.rb +174 -0
  16. data/lib/render_sync/action.rb +39 -0
  17. data/lib/render_sync/actions.rb +114 -0
  18. data/lib/render_sync/channel.rb +23 -0
  19. data/lib/render_sync/clients/dummy.rb +22 -0
  20. data/lib/render_sync/clients/faye.rb +104 -0
  21. data/lib/render_sync/clients/pusher.rb +77 -0
  22. data/lib/render_sync/controller_helpers.rb +33 -0
  23. data/lib/render_sync/engine.rb +24 -0
  24. data/lib/render_sync/erb_tracker.rb +49 -0
  25. data/lib/render_sync/faye_extension.rb +45 -0
  26. data/lib/render_sync/model.rb +174 -0
  27. data/lib/render_sync/model_actions.rb +60 -0
  28. data/lib/render_sync/model_change_tracking.rb +97 -0
  29. data/lib/render_sync/model_syncing.rb +65 -0
  30. data/lib/render_sync/model_touching.rb +35 -0
  31. data/lib/render_sync/partial.rb +112 -0
  32. data/lib/render_sync/partial_creator.rb +47 -0
  33. data/lib/render_sync/reactor.rb +48 -0
  34. data/lib/render_sync/refetch_model.rb +21 -0
  35. data/lib/render_sync/refetch_partial.rb +43 -0
  36. data/lib/render_sync/refetch_partial_creator.rb +21 -0
  37. data/lib/render_sync/renderer.rb +19 -0
  38. data/lib/render_sync/resource.rb +115 -0
  39. data/lib/render_sync/scope.rb +113 -0
  40. data/lib/render_sync/scope_definition.rb +30 -0
  41. data/lib/render_sync/view_helpers.rb +106 -0
  42. data/test/dummy/README.rdoc +28 -0
  43. data/test/dummy/Rakefile +6 -0
  44. data/test/dummy/app/assets/javascripts/application.js +13 -0
  45. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  46. data/test/dummy/app/controllers/application_controller.rb +5 -0
  47. data/test/dummy/app/helpers/application_helper.rb +2 -0
  48. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  49. data/test/dummy/app/views/sync/users/_show.html.erb +1 -0
  50. data/test/dummy/app/views/sync/users/refetch/_show.html.erb +1 -0
  51. data/test/dummy/bin/bundle +3 -0
  52. data/test/dummy/bin/rails +4 -0
  53. data/test/dummy/bin/rake +4 -0
  54. data/test/dummy/config.ru +4 -0
  55. data/test/dummy/config/application.rb +22 -0
  56. data/test/dummy/config/boot.rb +5 -0
  57. data/test/dummy/config/database.yml +8 -0
  58. data/test/dummy/config/environment.rb +5 -0
  59. data/test/dummy/config/environments/development.rb +29 -0
  60. data/test/dummy/config/environments/production.rb +80 -0
  61. data/test/dummy/config/environments/test.rb +36 -0
  62. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  63. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  64. data/test/dummy/config/initializers/inflections.rb +16 -0
  65. data/test/dummy/config/initializers/mime_types.rb +5 -0
  66. data/test/dummy/config/initializers/secret_token.rb +12 -0
  67. data/test/dummy/config/initializers/session_store.rb +3 -0
  68. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  69. data/test/dummy/config/locales/en.yml +23 -0
  70. data/test/dummy/config/routes.rb +56 -0
  71. data/test/dummy/log/test.log +626 -0
  72. data/test/dummy/public/404.html +58 -0
  73. data/test/dummy/public/422.html +58 -0
  74. data/test/dummy/public/500.html +57 -0
  75. data/test/dummy/public/favicon.ico +0 -0
  76. data/test/em_minitest_spec.rb +100 -0
  77. data/test/fixtures/sync_auth_token_missing.yml +6 -0
  78. data/test/fixtures/sync_erb.yml +7 -0
  79. data/test/fixtures/sync_faye.yml +7 -0
  80. data/test/fixtures/sync_pusher.yml +8 -0
  81. data/test/models/group.rb +3 -0
  82. data/test/models/project.rb +2 -0
  83. data/test/models/todo.rb +8 -0
  84. data/test/models/user.rb +82 -0
  85. data/test/sync/abstract_controller.rb +3 -0
  86. data/test/sync/action_test.rb +82 -0
  87. data/test/sync/channel_test.rb +15 -0
  88. data/test/sync/config_test.rb +25 -0
  89. data/test/sync/erb_tracker_test.rb +72 -0
  90. data/test/sync/faye_extension_test.rb +87 -0
  91. data/test/sync/message_test.rb +159 -0
  92. data/test/sync/model_test.rb +315 -0
  93. data/test/sync/partial_creator_test.rb +35 -0
  94. data/test/sync/partial_test.rb +107 -0
  95. data/test/sync/protected_attributes_test.rb +39 -0
  96. data/test/sync/reactor_test.rb +18 -0
  97. data/test/sync/refetch_model_test.rb +26 -0
  98. data/test/sync/refetch_partial_creator_test.rb +16 -0
  99. data/test/sync/refetch_partial_test.rb +74 -0
  100. data/test/sync/renderer_test.rb +19 -0
  101. data/test/sync/resource_test.rb +181 -0
  102. data/test/sync/scope_definition_test.rb +39 -0
  103. data/test/sync/scope_test.rb +113 -0
  104. data/test/test_helper.rb +66 -0
  105. data/test/travis/sync.ru +14 -0
  106. data/test/travis/sync.yml +21 -0
  107. metadata +317 -0
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.pattern = 'test/sync/*_test*'
6
+ end
7
+
8
+ desc "Run tests"
9
+ task :default => :test
@@ -0,0 +1,355 @@
1
+ $ = jQuery
2
+
3
+ @RenderSync =
4
+
5
+ ready: false
6
+ readyQueue: []
7
+
8
+ init: ->
9
+ $ =>
10
+ return unless RenderSyncConfig? && RenderSync[RenderSyncConfig.adapter]
11
+ @adapter ||= new RenderSync[RenderSyncConfig.adapter]
12
+ return if @isReady() || !@adapter.available()
13
+ @ready = true
14
+ @connect()
15
+ @flushReadyQueue()
16
+ @bindUnsubscribe()
17
+
18
+
19
+ # Handle Turbolinks teardown, unsubscribe from all channels before transition
20
+ bindUnsubscribe: ->
21
+ $(document).bind "page:before-change", => @adapter.unsubscribeAll()
22
+ $(document).bind "page:restore", => @reexecuteScripts()
23
+
24
+
25
+ # Handle Turbolinks cache restore, re-eval all sync script tags
26
+ reexecuteScripts: ->
27
+ for script in $("script[data-sync-id]")
28
+ eval($(script).html())
29
+
30
+
31
+ onConnectFailure: (error) -> #noop
32
+
33
+ connect: -> @adapter.connect()
34
+
35
+ isConnected: -> @adapter.isConnected()
36
+
37
+ onReady: (callback) ->
38
+ if @isReady()
39
+ callback()
40
+ else
41
+ @readyQueue.push callback
42
+
43
+
44
+ flushReadyQueue: ->
45
+ @onReady(callback) for callback in @readyQueue
46
+ @readyQueue = []
47
+
48
+
49
+ isReady: -> @ready
50
+
51
+ camelize: (str) ->
52
+ str.replace /(?:^|[-_])(\w)/g, (match, camel) -> camel?.toUpperCase() ? ''
53
+
54
+
55
+ # Find View class to render based on partial and resource names
56
+ # The class name is looked up based on
57
+ # 1. The camelized version of the concatenated snake case resource
58
+ # and partial names.
59
+ # 2. The camelized version of the snake cased partialName.
60
+ #
61
+ # Examples
62
+ # partialName 'list_row', resourceName 'todo', order of lookup:
63
+ # RenderSync.TodoListRow
64
+ # RenderSync.ListRow
65
+ # RenderSync.View
66
+ #
67
+ # Defaults to RenderSync.View if no custom view class has been defined
68
+ viewClassFromPartialName: (partialName, resourceName) ->
69
+ RenderSync[@camelize("#{resourceName}_#{partialName}")] ?
70
+ RenderSync[@camelize(partialName)] ?
71
+ RenderSync.View
72
+
73
+
74
+ class RenderSync.Adapter
75
+
76
+ subscriptions: []
77
+
78
+ unsubscribeAll: ->
79
+ subscription.cancel() for subscription in @subscriptions
80
+ @subscriptions = []
81
+
82
+ # If the channel is already subscribed, we do two things:
83
+ # 1. cancel() the subscription
84
+ # 2. remove the channel from our @subscriptions array
85
+ unsubscribeChannel: (channel) ->
86
+ for sub, index in @subscriptions when sub.channel is channel
87
+ sub.cancel()
88
+ @subscriptions.splice(index, 1)
89
+ return
90
+
91
+ subscribe: (channel, callback) ->
92
+ @unsubscribeChannel(channel)
93
+ subscription = new RenderSync[RenderSyncConfig.adapter].Subscription(@client, channel, callback)
94
+ @subscriptions.push(subscription)
95
+ subscription
96
+
97
+
98
+ class RenderSync.Faye extends RenderSync.Adapter
99
+
100
+ subscriptions: []
101
+
102
+ available: ->
103
+ !!window.Faye
104
+
105
+ connect: ->
106
+ @client = new window.Faye.Client(RenderSyncConfig.server)
107
+
108
+ isConnected: -> @client?.getState() is "CONNECTED"
109
+
110
+
111
+ class RenderSync.Faye.Subscription
112
+
113
+ constructor: (@client, channel, callback) ->
114
+ @channel = channel
115
+ @fayeSub = @client.subscribe channel, callback
116
+
117
+ cancel: ->
118
+ @fayeSub.cancel()
119
+
120
+
121
+ class RenderSync.Pusher extends RenderSync.Adapter
122
+
123
+ subscriptions: []
124
+
125
+ available: ->
126
+ !!window.Pusher
127
+
128
+ connect: ->
129
+ opts =
130
+ encrypted: RenderSyncConfig.pusher_encrypted
131
+
132
+ opts.wsHost = RenderSyncConfig.pusher_ws_host if RenderSyncConfig.pusher_ws_host
133
+ opts.wsPort = RenderSyncConfig.pusher_ws_port if RenderSyncConfig.pusher_ws_port
134
+ opts.wssPort = RenderSyncConfig.pusher_wss_port if RenderSyncConfig.pusher_wss_port
135
+
136
+ @client = new window.Pusher(RenderSyncConfig.api_key, opts)
137
+
138
+ isConnected: -> @client?.connection.state is "connected"
139
+
140
+ subscribe: (channel, callback) ->
141
+ @unsubscribeChannel(channel)
142
+ subscription = new RenderSync.Pusher.Subscription(@client, channel, callback)
143
+ @subscriptions.push(subscription)
144
+ subscription
145
+
146
+
147
+ class RenderSync.Pusher.Subscription
148
+ constructor: (@client, channel, callback) ->
149
+ @channel = channel
150
+
151
+ pusherSub = @client.subscribe(channel)
152
+ pusherSub.bind 'sync', callback
153
+
154
+ cancel: ->
155
+ @client.unsubscribe(@channel) if @client.channel(@channel)?
156
+
157
+
158
+ class RenderSync.View
159
+
160
+ removed: false
161
+
162
+ constructor: (@$el, @name) ->
163
+
164
+ beforeUpdate: (html, data) -> @update(html)
165
+
166
+ afterUpdate: -> #noop
167
+
168
+ beforeInsert: ($el, data) -> @insert($el)
169
+
170
+ afterInsert: -> #noop
171
+
172
+ beforeRemove: -> @remove()
173
+
174
+ afterRemove: -> #noop
175
+
176
+ isRemoved: -> @removed
177
+
178
+ remove: ->
179
+ @$el.remove()
180
+ @$el = $()
181
+ @removed = true
182
+ @afterRemove()
183
+
184
+
185
+ bind: -> #noop
186
+
187
+ show: -> @$el.show()
188
+
189
+ update: (html) ->
190
+ $new = $($.trim(html))
191
+ @$el.replaceWith($new)
192
+ @$el = $new
193
+ @afterUpdate()
194
+ @bind()
195
+
196
+
197
+ insert: ($el) ->
198
+ @$el.replaceWith($el)
199
+ @$el = $el
200
+ @afterInsert()
201
+ @bind()
202
+
203
+
204
+
205
+ class RenderSync.Partial
206
+
207
+ attributes:
208
+ name: null
209
+ resourceName: null
210
+ resourceId: null
211
+ authToken: null
212
+ channelUpdate: null
213
+ channelDestroy: null
214
+ selectorStart: null
215
+ selectorEnd: null
216
+ refetch: false
217
+
218
+ subscriptionUpdate: null
219
+ subscriptionDestroy: null
220
+
221
+ # attributes
222
+ #
223
+ # name - The String name of the partial without leading underscore
224
+ # resourceName - The String undercored class name of the resource
225
+ # resourceId
226
+ # authToken - The String auth token for the partial
227
+ # channelUpdate - The String channel to listen for update publishes on
228
+ # channelDestroy - The String channel to listen for destroy publishes on
229
+ # selectorStart - The String selector to mark beginning in the DOM
230
+ # selectorEnd - The String selector to mark ending in the DOM
231
+ # refetch - The Boolean to refetch markup from server or receive markup
232
+ # from pubsub update. Default false.
233
+ #
234
+ constructor: (attributes = {}) ->
235
+ @[key] = attributes[key] ? defaultValue for key, defaultValue of @attributes
236
+ @$start = $("[data-sync-id='#{@selectorStart}']")
237
+ @$end = $("[data-sync-id='#{@selectorEnd}']")
238
+ @$el = @$start.nextUntil(@$end)
239
+ @view = new (RenderSync.viewClassFromPartialName(@name, @resourceName))(@$el, @name)
240
+ @adapter = RenderSync.adapter
241
+
242
+
243
+ subscribe: ->
244
+ @subscriptionUpdate = @adapter.subscribe @channelUpdate, (data) =>
245
+ if @refetch
246
+ @refetchFromServer (html) => @update(html)
247
+ else
248
+ @update(data.html)
249
+
250
+ @subscriptionDestroy = @adapter.subscribe @channelDestroy, => @remove()
251
+
252
+
253
+ update: (html) -> @view.beforeUpdate(html, {})
254
+
255
+ remove: ->
256
+ @view.beforeRemove()
257
+ @destroy() if @view.isRemoved()
258
+
259
+
260
+ insert: (html) ->
261
+ if @refetch
262
+ @refetchFromServer (html) => @view.beforeInsert($($.trim(html)), {})
263
+ else
264
+ @view.beforeInsert($($.trim(html)), {})
265
+
266
+
267
+ destroy: ->
268
+ @subscriptionUpdate.cancel()
269
+ @subscriptionDestroy.cancel()
270
+ @$start.remove()
271
+ @$end.remove()
272
+ @$el?.remove()
273
+ delete @$start
274
+ delete @$end
275
+ delete @$el
276
+
277
+
278
+ refetchFromServer: (callback) ->
279
+ $.ajax
280
+ type: "GET"
281
+ url: "/sync/refetch.json"
282
+ data:
283
+ auth_token: @authToken
284
+ partial_name: @name
285
+ resource_name: @resourceName
286
+ resource_id: @resourceId
287
+ success: (data) -> callback(data.html)
288
+
289
+
290
+ class RenderSync.PartialCreator
291
+
292
+ attributes:
293
+ name: null
294
+ resourceName: null
295
+ authToken: null
296
+ channel: null
297
+ selector: null
298
+ direction: 'append'
299
+ refetch: false
300
+
301
+ # attributes
302
+ #
303
+ # name - The String name of the partial without leading underscore
304
+ # resourceName - The String undercored class name of the resource
305
+ # channel - The String channel to listen for new publishes on
306
+ # selector - The String selector to find the element in the DOM
307
+ # direction - The String direction to insert. One of "append" or "prepend"
308
+ # refetch - The Boolean to refetch markup from server or receive markup
309
+ # from pubsub update. Default false.
310
+ #
311
+ constructor: (attributes = {}) ->
312
+ @[key] = attributes[key] ? defaultValue for key, defaultValue of @attributes
313
+ @$el = $("[data-sync-id='#{@selector}']")
314
+ @adapter = RenderSync.adapter
315
+
316
+
317
+ subscribe: ->
318
+ @adapter.subscribe @channel, (data) =>
319
+ @insert data.html,
320
+ data.resourceId,
321
+ data.authToken,
322
+ data.channelUpdate,
323
+ data.channelDestroy,
324
+ data.selectorStart,
325
+ data.selectorEnd
326
+
327
+
328
+ insertPlaceholder: (html) ->
329
+ switch @direction
330
+ when "append" then @$el.before(html)
331
+ when "prepend" then @$el.after(html)
332
+
333
+
334
+
335
+ insert: (html, resourceId, authToken, channelUpdate, channelDestroy, selectorStart, selectorEnd) ->
336
+ @insertPlaceholder """
337
+ <script type='text/javascript' data-sync-id='#{selectorStart}'></script>
338
+ <script type='text/javascript' data-sync-el-placeholder></script>
339
+ <script type='text/javascript' data-sync-id='#{selectorEnd}'></script>
340
+ """
341
+ partial = new RenderSync.Partial(
342
+ name: @name
343
+ resourceName: @resourceName
344
+ resourceId: resourceId
345
+ authToken: authToken
346
+ channelUpdate: channelUpdate
347
+ channelDestroy: channelDestroy
348
+ selectorStart: selectorStart
349
+ selectorEnd: selectorEnd
350
+ refetch: @refetch
351
+ )
352
+ partial.subscribe()
353
+ partial.insert(html)
354
+
355
+ RenderSync.init()
@@ -0,0 +1,56 @@
1
+ class RenderSync::RefetchesController < ApplicationController
2
+
3
+ before_filter :require_valid_request
4
+ before_filter :find_resource
5
+ before_filter :find_authorized_partial
6
+
7
+ def show
8
+ render json: {
9
+ html: with_format(:html){ @partial.render_to_string }
10
+ }
11
+ end
12
+
13
+
14
+ private
15
+
16
+ def with_format(format, &block)
17
+ old_formats = formats
18
+ self.formats = [format]
19
+ block_value = block.call
20
+ self.formats = old_formats
21
+
22
+ block_value
23
+ end
24
+
25
+ def require_valid_request
26
+ render_bad_request unless request_valid?
27
+ end
28
+
29
+ def request_valid?
30
+ [
31
+ params[:resource_name],
32
+ params[:partial_name],
33
+ params[:auth_token]
34
+ ].all?(&:present?)
35
+ end
36
+
37
+ def find_resource
38
+ @resource = RenderSync::RefetchModel.find_by_class_name_and_id(
39
+ params[:resource_name],
40
+ params[:resource_id]
41
+ ) || render_bad_request
42
+ end
43
+
44
+ def find_authorized_partial
45
+ @partial = RenderSync::RefetchPartial.find_by_authorized_resource(
46
+ @resource,
47
+ params[:partial_name],
48
+ self,
49
+ params[:auth_token]
50
+ ) || render_bad_request
51
+ end
52
+
53
+ def render_bad_request
54
+ head :bad_request
55
+ end
56
+ end
@@ -0,0 +1,15 @@
1
+ module RenderSync::ConfigHelper
2
+ def include_sync_config(opts = {})
3
+ return unless RenderSync.adapter
4
+
5
+ str = ''
6
+
7
+ unless opts[:skip_adapter]
8
+ str << %{<script src="#{RenderSync.adapter_javascript_url}" data-turbolinks-eval=false></script>}
9
+ end
10
+
11
+ str << %{<script data-turbolinks-eval=false>var RenderSyncConfig = #{RenderSync.config_json};</script>}
12
+
13
+ str.html_safe
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ Rails.application.routes.draw do
2
+ get 'sync/refetch', controller: 'sync/refetches', action: 'show'
3
+ end
@@ -0,0 +1,21 @@
1
+ # Faye
2
+ development:
3
+ server: "http://localhost:9292/faye"
4
+ adapter_javascript_url: "http://localhost:9292/faye/faye.js"
5
+ auth_token: "secret"
6
+ adapter: "Faye"
7
+ async: true
8
+
9
+ test:
10
+ server: "http://localhost:9292/faye"
11
+ adapter_javascript_url: "http://localhost:9292/faye/faye.js"
12
+ adapter: "Faye"
13
+ auth_token: "secret"
14
+ async: false
15
+
16
+ production:
17
+ server: "http://localhost:9292/faye"
18
+ adapter_javascript_url: "http://localhost:9292/faye/faye.js"
19
+ adapter: "Faye"
20
+ auth_token: "secret"
21
+ async: true