render_sync 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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