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,77 @@
1
+ module RenderSync
2
+ module Clients
3
+ class Pusher
4
+
5
+ def setup
6
+ require 'pusher'
7
+ ::Pusher.app_id = RenderSync.app_id
8
+ ::Pusher.key = RenderSync.api_key
9
+ ::Pusher.secret = RenderSync.auth_token
10
+
11
+ if RenderSync.pusher_api_scheme
12
+ ::Pusher.scheme = RenderSync.pusher_api_scheme
13
+ end
14
+
15
+ if RenderSync.pusher_api_host
16
+ ::Pusher.host = RenderSync.pusher_api_host
17
+ end
18
+
19
+ if RenderSync.pusher_api_port
20
+ ::Pusher.port = RenderSync.pusher_api_port
21
+ end
22
+ end
23
+
24
+ def batch_publish(*args)
25
+ Message.batch_publish(*args)
26
+ end
27
+
28
+ def build_message(*args)
29
+ Message.new(*args)
30
+ end
31
+
32
+ # Public: Normalize channel to adapter supported format
33
+ #
34
+ # channel - The string channel name
35
+ #
36
+ # Returns The normalized channel prefixed with supported format for Pusher
37
+ def normalize_channel(channel)
38
+ channel
39
+ end
40
+
41
+
42
+ class Message
43
+
44
+ attr_accessor :channel, :data
45
+
46
+ def self.batch_publish(messages)
47
+ messages.each do |message|
48
+ message.publish
49
+ end
50
+ end
51
+
52
+ def initialize(channel, data)
53
+ self.channel = channel
54
+ self.data = data
55
+ end
56
+
57
+ def publish
58
+ if RenderSync.async?
59
+ publish_asynchronous
60
+ else
61
+ publish_synchronous
62
+ end
63
+ end
64
+
65
+ def publish_synchronous
66
+ ::Pusher.trigger([channel], 'sync', data)
67
+ end
68
+
69
+ def publish_asynchronous
70
+ RenderSync.reactor.perform do
71
+ ::Pusher.trigger_async([channel], 'sync', data)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,33 @@
1
+ module RenderSync
2
+
3
+ module ControllerHelpers
4
+
5
+ include Actions
6
+
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def enable_sync(options = {})
13
+ around_filter :enable_sync, options
14
+ end
15
+ end
16
+
17
+
18
+ private
19
+
20
+ def enable_sync
21
+ RenderSync::Model.enable(sync_render_context) do
22
+ yield
23
+ end
24
+ end
25
+
26
+ # ControllerHelpers overrides Action#sync_render_context to use self as
27
+ # context to allow full access to request/response cycle
28
+ # over default abstract Renderer class
29
+ def sync_render_context
30
+ self
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ module RenderSync
2
+
3
+ class Engine < Rails::Engine
4
+ # Loads the sync.yml file if it exists.
5
+ initializer "sync.config", group: :all do
6
+ path = Rails.root.join("config/sync.yml")
7
+ RenderSync.load_config(path, Rails.env) if path.exist?
8
+ end
9
+
10
+ initializer "sync.activerecord" do
11
+ ActiveRecord::Base.send :extend, Model::ClassMethods
12
+ end
13
+
14
+ # Adds the ViewHelpers into ActionView::Base
15
+ initializer "sync.view_helpers" do
16
+ ActionView::Base.send :include, ViewHelpers
17
+ end
18
+
19
+ # Adds the ControllerHelpers into ActionConroller::Base
20
+ initializer "sync.controller_helpers" do
21
+ ActionController::Base.send :include, RenderSync::ControllerHelpers
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,49 @@
1
+ module RenderSync
2
+ tracker_class = nil
3
+ begin
4
+ require 'action_view/dependency_tracker'
5
+ tracker_class = ActionView::DependencyTracker::ERBTracker
6
+ rescue LoadError
7
+ begin
8
+ require 'cache_digests/dependency_tracker'
9
+ tracker_class = CacheDigests::DependencyTracker::ERBTracker
10
+ rescue LoadError
11
+ end
12
+ end
13
+
14
+ if tracker_class
15
+ class ERBTracker < tracker_class
16
+ # Matches:
17
+ # sync partial: "comment", collection: commentable.comments
18
+ # sync partial: "comment", resource: comment
19
+ SYNC_DEPENDENCY = /
20
+ sync(?:_new)?\s* # sync or sync_new, followed by optional whitespace
21
+ \(?\s* # start an optional parenthesis for the sync call
22
+ (?:partial:|:partial\s+=>)\s* # naming the partial, used with collection
23
+ ["']([a-z][a-z_\/]+)["']\s* # the template name itself -- 1st capture
24
+ ,\s* # comma separating parameters
25
+ :?(?:resource|collection)(?::|\s+=>)\s* # resource or collection identifier
26
+ @?(?:[a-z]+\.)*([a-z]+) # the resource or collection itself -- 2nd capture
27
+ /x
28
+
29
+ def self.call(name, template)
30
+ new(name, template).dependencies
31
+ end
32
+
33
+ def dependencies
34
+ (sync_dependencies + super).uniq
35
+ end
36
+
37
+ private
38
+
39
+ def source
40
+ template.source
41
+ end
42
+
43
+ def sync_dependencies
44
+ source.scan(SYNC_DEPENDENCY).
45
+ collect { |template, resource| "sync/#{resource.pluralize}/#{template}" }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ module RenderSync
2
+ class FayeExtension
3
+
4
+ def incoming(message, callback)
5
+ return handle_eror(message, callback) unless message_authenticated?(message)
6
+ if batch_publish?(message)
7
+ batch_incoming(message, callback)
8
+ else
9
+ single_incoming(message, callback)
10
+ end
11
+ end
12
+
13
+ def batch_incoming(message, callback)
14
+ message["data"].each do |message|
15
+ incoming(message, callback)
16
+ end
17
+ end
18
+
19
+ def single_incoming(message, callback)
20
+ callback.call(message)
21
+ end
22
+
23
+ def batch_publish?(message)
24
+ message['channel'] == "/batch_publish"
25
+ end
26
+
27
+ # IMPORTANT: clear out the auth token so it is not leaked to the client
28
+ def outgoing(message, callback)
29
+ if message['ext'] && message['ext']['auth_token']
30
+ message['ext'] = {}
31
+ end
32
+ callback.call(message)
33
+ end
34
+
35
+ def handle_eror(message, callback)
36
+ message['error'] = 'Invalid authentication token'
37
+ callback.call(message)
38
+ end
39
+
40
+ def message_authenticated?(message)
41
+ !(message['channel'] !~ %r{^/meta/} &&
42
+ message['ext']['auth_token'] != RenderSync.auth_token)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,174 @@
1
+ module RenderSync
2
+ module Model
3
+
4
+ def self.enabled?
5
+ Thread.current["model_sync_enabled"]
6
+ end
7
+
8
+ def self.context
9
+ Thread.current["model_sync_context"]
10
+ end
11
+
12
+ def self.enable!(context = nil)
13
+ Thread.current["model_sync_enabled"] = true
14
+ Thread.current["model_sync_context"] = context
15
+ end
16
+
17
+ def self.disable!
18
+ Thread.current["model_sync_enabled"] = false
19
+ Thread.current["model_sync_context"] = nil
20
+ end
21
+
22
+ def self.enable(context = nil)
23
+ enable!(context)
24
+ yield
25
+ ensure
26
+ disable!
27
+ end
28
+
29
+ module ClassMethods
30
+ attr_accessor :sync_default_scope, :sync_scope_definitions, :sync_touches
31
+
32
+ # Set up automatic syncing of partials when a record of this class is
33
+ # created, updated or deleted. Be sure to wrap your model actions inside
34
+ # a sync_enable block for sync to do its magic.
35
+ #
36
+ def sync(*actions)
37
+ include ModelActions unless include?(ModelActions)
38
+ include ModelChangeTracking unless include?(ModelChangeTracking)
39
+ include ModelRenderSyncing
40
+
41
+ if actions.last.is_a? Hash
42
+ @sync_default_scope = actions.last.fetch :default_scope
43
+ end
44
+
45
+ actions = [:create, :update, :destroy] if actions.include? :all
46
+ actions.flatten!
47
+
48
+ if actions.include? :create
49
+ after_create :prepare_sync_create, if: -> { RenderSync::Model.enabled? }
50
+ end
51
+
52
+ if actions.include? :update
53
+ after_update :prepare_sync_update, if: -> { RenderSync::Model.enabled? }
54
+ end
55
+
56
+ if actions.include? :destroy
57
+ after_destroy :prepare_sync_destroy, if: -> { RenderSync::Model.enabled? }
58
+ end
59
+
60
+ end
61
+
62
+ # Set up a sync scope for the model defining a set of records to be
63
+ # updated via sync
64
+ #
65
+ # name - The name of the scope
66
+ # lambda - A lambda defining the scope.
67
+ # Has to return an ActiveRecord::Relation.
68
+ #
69
+ # You can define the lambda with arguments (see examples).
70
+ # Note that the naming of the parameters is very important. Only use
71
+ # names of methods or ActiveRecord attributes defined on the model (e.g.
72
+ # user_id). This way sync will be able to pass changed records to the
73
+ # lambda and track changes to the scope.
74
+ #
75
+ # Example:
76
+ #
77
+ # class Todo < ActiveRecord::Base
78
+ # belongs_to :user
79
+ # belongs_to :project
80
+ # scope :incomplete, -> { where(complete: false) }
81
+ #
82
+ # sync :all
83
+ #
84
+ # sync_scope :complete, -> { where(complete: true) }
85
+ # sync_scope :by_project, ->(project_id) { where(project_id: project_id) }
86
+ # sync_scope :my_incomplete_todos, ->(user) { incomplete.where(user_id: user.id) }
87
+ # end
88
+ #
89
+ # To subscribe to these scopes you would put these lines into your views:
90
+ #
91
+ # <%= sync partial: "todo", collection: @todos, scope: Todo.complete %>
92
+ #
93
+ # If the collection you want to render is exactly defined be the given
94
+ # scope the scope can be omitted:
95
+ #
96
+ # <%= sync partial: "todo", collection: Todo.complete %>
97
+ #
98
+ # For rendering my_incomplete_todos:
99
+ #
100
+ # <%= sync partial: "todo", collection: Todo.my_incomplete_todos(current_user) %>
101
+ #
102
+ # The render_new call has to look like this:
103
+ #
104
+ # <%= sync_new partial: "todo", resource: Todo.new, scope: Todo.complete %>
105
+ #
106
+ # Now when a record changes sync will use the names of the lambda
107
+ # parameters (project_id and user), get the corresponding attributes from
108
+ # the record (project_id column or user association) and pass it to the
109
+ # lambda. This way sync can identify if a record has been added or
110
+ # removed from a scope and will then publish the changes to subscribers
111
+ # on all scoped channels.
112
+ #
113
+ # Beware that chaining of sync scopes in the view is currently not
114
+ # possible. So the following example would raise an exception:
115
+ #
116
+ # <%= sync_new partial: "todo", Todo.new, scope: Todo.mine(current_user).incomplete %>
117
+ #
118
+ # To work around this just create an explicit sync_scope for your problem:
119
+ #
120
+ # sync_scope :my_incomplete_todos, ->(user) { incomplete.mine(current_user) }
121
+ #
122
+ # And in the view:
123
+ #
124
+ # <%= sync_new partial: "todo", Todo.new, scope: Todo.my_incomplete_todos(current_user) %>
125
+ #
126
+ def sync_scope(name, lambda)
127
+ if self.respond_to?(name)
128
+ raise ArgumentError, "invalid scope name '#{name}'. Already defined on #{self.name}"
129
+ end
130
+
131
+ @sync_scope_definitions[name] = RenderSync::ScopeDefinition.new(self, name, lambda)
132
+
133
+ singleton_class.send(:define_method, name) do |*args|
134
+ RenderSync::Scope.new_from_args(@sync_scope_definitions[name], args)
135
+ end
136
+ end
137
+
138
+ # Register one or more associations to be sync'd when this record changes.
139
+ #
140
+ # Example:
141
+ #
142
+ # class Todo < ActiveRecord::Base
143
+ # belongs_to :project
144
+ # belongs_to :user
145
+ #
146
+ # sync :all
147
+ # sync_touch :project, :user
148
+ # end
149
+ #
150
+ def sync_touch(*args)
151
+ # Only load Modules and set up callbacks if sync_touch wasn't
152
+ # called before
153
+ if @sync_touches.blank?
154
+ include ModelActions unless include?(ModelActions)
155
+ include ModelChangeTracking unless include?(ModelChangeTracking)
156
+ include ModelTouching
157
+
158
+ @sync_touches ||= []
159
+
160
+ after_create :prepare_sync_touches, if: -> { RenderSync::Model.enabled? }
161
+ after_update :prepare_sync_touches, if: -> { RenderSync::Model.enabled? }
162
+ after_destroy :prepare_sync_touches, if: -> { RenderSync::Model.enabled? }
163
+ end
164
+
165
+ options = args.extract_options!
166
+ args.each do |arg|
167
+ @sync_touches.push(arg)
168
+ end
169
+ end
170
+
171
+ end
172
+
173
+ end
174
+ end
@@ -0,0 +1,60 @@
1
+ module RenderSync
2
+ module ModelActions
3
+ # Set up instance variable holding the collected sync actions
4
+ # to be published later on.
5
+ #
6
+ attr_accessor :sync_actions
7
+
8
+ # Set up ActiveRecord callbacks to prepare for collecting
9
+ # publish sync actions and publishing them after commit
10
+ #
11
+ def self.included(base)
12
+ base.class_eval do
13
+ @sync_scope_definitions ||= {}
14
+
15
+ before_create :prepare_sync_actions, if: -> { RenderSync::Model.enabled? }
16
+ before_update :prepare_sync_actions, if: -> { RenderSync::Model.enabled? }
17
+ before_destroy :prepare_sync_actions, if: -> { RenderSync::Model.enabled? }
18
+
19
+ after_commit :publish_sync_actions, if: -> { RenderSync::Model.enabled? }
20
+ end
21
+ end
22
+
23
+ def sync_default_scope
24
+ return nil unless self.class.sync_default_scope
25
+ send self.class.sync_default_scope
26
+ end
27
+
28
+ private
29
+
30
+ def sync_render_context
31
+ RenderSync::Model.context || super
32
+ end
33
+
34
+ def prepare_sync_actions
35
+ self.sync_actions = []
36
+ end
37
+
38
+ # Add a new aync action to the list of actions to be published later on
39
+ #
40
+ def add_sync_action(action_name, record, options = {})
41
+ sync_actions.push(Action.new(record, action_name, options))
42
+ end
43
+
44
+ # Run the collected actions on after_commit callback
45
+ # Triggers the syncing of the partials
46
+ #
47
+ def publish_sync_actions
48
+ sync_actions.each(&:perform)
49
+ end
50
+
51
+ def sync_scope_definitions
52
+ self.class.sync_scope_definitions.values
53
+ end
54
+
55
+ def sync_render_context
56
+ RenderSync::Model.context || super
57
+ end
58
+
59
+ end
60
+ end