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,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