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,14 @@
1
+ module RenderSync
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ def self.source_root
5
+ File.dirname(__FILE__) + "/templates"
6
+ end
7
+
8
+ def copy_files
9
+ template "sync.yml", "config/sync.yml"
10
+ copy_file "sync.ru", "sync.ru"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # Run with: rackup sync.ru -E production
2
+ require "bundler/setup"
3
+ require "yaml"
4
+ require "faye"
5
+ require "render_sync"
6
+
7
+ Faye::WebSocket.load_adapter 'thin'
8
+
9
+ RenderSync.load_config(
10
+ File.expand_path("../config/sync.yml", __FILE__),
11
+ ENV["RAILS_ENV"] || "development"
12
+ )
13
+
14
+ run RenderSync.pubsub_app
@@ -0,0 +1,34 @@
1
+ # Faye
2
+ development:
3
+ server: "http://localhost:9292/faye"
4
+ adapter_javascript_url: "http://localhost:9292/faye/faye.js"
5
+ auth_token: DEVELOPMENT_SECRET_TOKEN
6
+ adapter: "Faye"
7
+ async: true
8
+
9
+ # Pusher
10
+ # development:
11
+ # adapter_javascript_url: "http://js.pusher.com/2.0/pusher.min.js"
12
+ # app_id: "YOUR_PUSHER_APP_ID"
13
+ # api_key: "YOUR_PUSHER_API_KEY"
14
+ # auth_token: "YOUR_PUSHER_SECRET"
15
+ # adapter: "Pusher"
16
+ # async: true
17
+
18
+ # Disabled
19
+ # development:
20
+ # adapter: false
21
+
22
+ test:
23
+ server: "http://localhost:9292/faye"
24
+ adapter_javascript_url: "http://localhost:9292/faye/faye.js"
25
+ adapter: "Faye"
26
+ auth_token: "secret"
27
+ async: false
28
+
29
+ production:
30
+ server: "http://example.com/faye"
31
+ adapter_javascript_url: "http://localhost:9292/faye/faye.js"
32
+ adapter: "Faye"
33
+ auth_token: "<%= defined?(SecureRandom) ? SecureRandom.hex(32) : ActiveSupport::SecureRandom.hex(32) %>"
34
+ async: true
@@ -0,0 +1,174 @@
1
+ require 'eventmachine'
2
+ require 'monitor'
3
+ require 'digest/sha1'
4
+ require 'erb'
5
+ require 'net/http'
6
+ require 'net/https'
7
+ require 'logger'
8
+ require 'render_sync/renderer'
9
+ require 'render_sync/actions'
10
+ require 'render_sync/action'
11
+ require 'render_sync/controller_helpers'
12
+ require 'render_sync/view_helpers'
13
+ require 'render_sync/model_change_tracking'
14
+ require 'render_sync/model_actions'
15
+ require 'render_sync/model_syncing'
16
+ require 'render_sync/model_touching'
17
+ require 'render_sync/model'
18
+ require 'render_sync/scope'
19
+ require 'render_sync/scope_definition'
20
+ require 'render_sync/refetch_model'
21
+ require 'render_sync/faye_extension'
22
+ require 'render_sync/partial_creator'
23
+ require 'render_sync/refetch_partial_creator'
24
+ require 'render_sync/partial'
25
+ require 'render_sync/refetch_partial'
26
+ require 'render_sync/channel'
27
+ require 'render_sync/resource'
28
+ require 'render_sync/clients/faye'
29
+ require 'render_sync/clients/pusher'
30
+ require 'render_sync/clients/dummy'
31
+ require 'render_sync/reactor'
32
+ if defined? Rails
33
+ require 'render_sync/erb_tracker'
34
+ require 'render_sync/engine'
35
+ end
36
+
37
+ module RenderSync
38
+
39
+ class << self
40
+ attr_accessor :config, :client, :logger
41
+
42
+ def config
43
+ @config || {}
44
+ end
45
+
46
+ def config_json
47
+ @config_json ||= begin
48
+ {
49
+ server: server,
50
+ api_key: api_key,
51
+ pusher_ws_host: pusher_ws_host,
52
+ pusher_ws_port: pusher_ws_port,
53
+ pusher_wss_port: pusher_wss_port,
54
+ pusher_encrypted: pusher_encrypted,
55
+ adapter: adapter
56
+ }.reject { |k, v| v.nil? }.to_json
57
+ end
58
+ end
59
+
60
+ # Resets the configuration to the default (empty hash)
61
+ def reset_config
62
+ @config = {}
63
+ @config_json = nil
64
+ end
65
+
66
+ # Loads the configuration from a given YAML file and environment (such as production)
67
+ def load_config(filename, environment)
68
+ reset_config
69
+ yaml = YAML.load(ERB.new(File.read(filename)).result)[environment.to_s]
70
+ raise ArgumentError, "The #{environment} environment does not exist in #{filename}" if yaml.nil?
71
+ yaml.each{|key, value| config[key.to_sym] = value }
72
+ setup_logger
73
+
74
+ if adapter
75
+ setup_client
76
+ else
77
+ setup_dummy_client
78
+ end
79
+ end
80
+
81
+ def setup_client
82
+ raise ArgumentError, "auth_token missing" if config[:auth_token].nil?
83
+ @client = RenderSync::Clients.const_get(adapter).new
84
+ @client.setup
85
+ end
86
+
87
+ def setup_dummy_client
88
+ config[:auth_token] = 'dummy_auth_token'
89
+ @client = RenderSync::Clients::Dummy.new
90
+ end
91
+
92
+ def setup_logger
93
+ @logger = (defined?(Rails) && Rails.logger) ? Rails.logger : Logger.new(STDOUT)
94
+ end
95
+
96
+ def async?
97
+ config[:async]
98
+ end
99
+
100
+ def server
101
+ config[:server]
102
+ end
103
+
104
+ def adapter_javascript_url
105
+ config[:adapter_javascript_url]
106
+ end
107
+
108
+ def auth_token
109
+ config[:auth_token]
110
+ end
111
+
112
+ def adapter
113
+ config[:adapter]
114
+ end
115
+
116
+ def app_id
117
+ config[:app_id]
118
+ end
119
+
120
+ def api_key
121
+ config[:api_key]
122
+ end
123
+
124
+ def pusher_api_scheme
125
+ config[:pusher_api_scheme]
126
+ end
127
+
128
+ def pusher_api_host
129
+ config[:pusher_api_host]
130
+ end
131
+
132
+ def pusher_api_port
133
+ config[:pusher_api_port]
134
+ end
135
+
136
+ def pusher_ws_host
137
+ config[:pusher_ws_host]
138
+ end
139
+
140
+ def pusher_ws_port
141
+ config[:pusher_ws_port]
142
+ end
143
+
144
+ def pusher_wss_port
145
+ config[:pusher_wss_port]
146
+ end
147
+
148
+ def pusher_encrypted
149
+ if config[:pusher_encrypted].nil?
150
+ true
151
+ else
152
+ config[:pusher_encrypted]
153
+ end
154
+ end
155
+
156
+ def reactor
157
+ @reactor ||= Reactor.new
158
+ end
159
+
160
+ # Returns the Faye Rack application.
161
+ # Any options given are passed to the Faye::RackAdapter.
162
+ def pubsub_app(options = {})
163
+ Faye::RackAdapter.new({
164
+ mount: config[:mount] || "/faye",
165
+ timeout: config[:timeout] || 45,
166
+ extensions: [FayeExtension.new]
167
+ }.merge(options))
168
+ end
169
+
170
+ def views_root
171
+ Rails.root.join('app', 'views', 'sync')
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,39 @@
1
+ module RenderSync
2
+ class Action
3
+ include Actions
4
+
5
+ attr_accessor :record, :name, :scope
6
+
7
+ def initialize(record, name, *args)
8
+ options = args.extract_options!
9
+ @record = record
10
+ @name = name
11
+ @scope = get_scope_from_options(options)
12
+ end
13
+
14
+ def perform
15
+ case name
16
+ when :new
17
+ sync_new record, scope: scope
18
+ when :update
19
+ sync_update record, scope: scope
20
+ when :destroy
21
+ sync_destroy record, scope: scope
22
+ end
23
+ end
24
+
25
+ # Just for testing purposes (see test/sync/model_test.rb)
26
+ def test_path
27
+ Resource.new(record, scope).polymorphic_path.to_s
28
+ end
29
+
30
+ private
31
+
32
+ # Merge default_scope and scope from options Hash
33
+ # compact array to remove nil elements
34
+ def get_scope_from_options(options)
35
+ [options[:default_scope], options[:scope]].compact
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,114 @@
1
+ module RenderSync
2
+ module Actions
3
+
4
+ # Render all sync'd partials for resource to string and publish update action
5
+ # to pubsub server with rendered resource messages
6
+ #
7
+ # resource - The ActiveModel resource
8
+ # options - The Hash of options
9
+ # default_scope - The ActiveModel resource to scope the update channel to
10
+ # scope - Either a String, a symbol, an instance of ActiveModel or
11
+ # RenderSync::Scope or an Array containing a combination to scope
12
+ # the update channel to. Will be concatenated to an optional
13
+ # default_scope
14
+ #
15
+ def sync_update(resource, options = {})
16
+ sync resource, :update, options
17
+ end
18
+
19
+ # Render all sync'd partials for resource to string and publish destroy action
20
+ # to pubsub server with rendered resource messages
21
+ #
22
+ # resource - The ActiveModel resource
23
+ # options - The Hash of options
24
+ # default_scope - The ActiveModel resource to scope the update channel to
25
+ # scope - Either a String, a symbol, an instance of ActiveModel or
26
+ # RenderSync::Scope or an Array containing a combination to scope
27
+ # the destroy channel to. Will be concatenated to an optional
28
+ # default_scope
29
+ #
30
+ def sync_destroy(resource, options = {})
31
+ sync resource, :destroy, options
32
+ end
33
+
34
+ # Render all sync'd partials for resource to string and publish action
35
+ # to pubsub server with rendered resource messages
36
+ #
37
+ # resource - The ActiveModel resource
38
+ # action - The Symbol action to publish. One of :update, :destroy
39
+ # options - The Hash of options
40
+ # default_scope - The ActiveModel resource to scope the action channel to
41
+ # scope - Either a String, a symbol, an instance of ActiveModel or
42
+ # RenderSync::Scope or an Array containing a combination to scope
43
+ # the channel to. Will be concatenated to an optional default_scope
44
+ #
45
+ def sync(resource, action, options = {})
46
+ scope = options[:scope]
47
+ partial_name = options[:partial]
48
+ resources = [resource].flatten
49
+ messages = resources.collect do |resource|
50
+ if partial_name
51
+ specified_partials(resource, sync_render_context, partial_name).collect do |partial|
52
+ partial.message(action)
53
+ end
54
+ else
55
+ all_partials(resource, sync_render_context, scope).collect do |partial|
56
+ partial.message(action)
57
+ end
58
+ end
59
+ end
60
+
61
+ RenderSync.client.batch_publish(messages.flatten)
62
+ end
63
+
64
+ # Render all sync'd partials for resource to string and publish
65
+ # new action to pubsub server with rendered resource messages
66
+ #
67
+ # resource - The ActiveModel resource, or Array of ActiveModel resources
68
+ # action - The Symbol action to publish. One of :update, :destroy
69
+ # options - The Hash of options
70
+ # default_scope - The ActiveModel resource to scope the new channel to
71
+ # scope - Either a String, a symbol, an instance of ActiveModel or
72
+ # RenderSync::Scope or an Array containing any combination to scope
73
+ # the new channel to. Will be concatenated to an optional
74
+ # default_scope
75
+ #
76
+ def sync_new(resource, options = {})
77
+ scope = options[:scope]
78
+ partial_name = options[:partial]
79
+ resources = [resource].flatten
80
+ messages = resources.collect do |resource|
81
+ if partial_name
82
+ specified_partials(resource, sync_render_context, partial_name).collect do |partial|
83
+ partial.creator_for_scope(scope).message
84
+ end
85
+ else
86
+ all_partials(resource, sync_render_context, scope).collect do |partial|
87
+ partial.creator_for_scope(scope).message
88
+ end
89
+ end
90
+ end
91
+
92
+ RenderSync.client.batch_publish(messages.flatten)
93
+ end
94
+
95
+ private
96
+
97
+ # The Context class handling partial rendering
98
+ def sync_render_context
99
+ @sync_render_context ||= Renderer.new
100
+ end
101
+
102
+ # Returns Array of Partials for all given resource and context, including
103
+ # both Partial and RefetchPartial instances
104
+ def all_partials(resource, context, scope = nil)
105
+ Partial.all(resource, context, scope) + RefetchPartial.all(resource, context, scope)
106
+ end
107
+
108
+ # Returns an Array containing both the Partial and RefetchPartial instances
109
+ # for a given resource, context and partial name
110
+ def specified_partials(resource, context, partial_name)
111
+ [Partial.find(resource, partial_name, context), RefetchPartial.find(resource, partial_name, context)].compact
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,23 @@
1
+ module RenderSync
2
+
3
+ class Channel
4
+
5
+ attr_accessor :name
6
+
7
+ def initialize(name)
8
+ self.name = name
9
+ end
10
+
11
+ def signature
12
+ OpenSSL::HMAC.hexdigest(
13
+ OpenSSL::Digest.new('sha1'),
14
+ RenderSync.auth_token,
15
+ self.name
16
+ )
17
+ end
18
+
19
+ def to_s
20
+ RenderSync.client.normalize_channel(self.signature)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ module RenderSync
2
+ module Clients
3
+ class Dummy
4
+ def method_missing(*args, &block)
5
+ nil
6
+ end
7
+
8
+ class Message
9
+ def self.method_missing(*args, &block)
10
+ nil
11
+ end
12
+
13
+ def initialize(*)
14
+ end
15
+
16
+ def method_missing(*args, &block)
17
+ nil
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,104 @@
1
+ module RenderSync
2
+ module Clients
3
+ class Faye
4
+
5
+ def setup
6
+ require 'faye'
7
+ # nothing to set up
8
+ end
9
+
10
+ def batch_publish(*args)
11
+ Message.batch_publish(*args)
12
+ end
13
+
14
+ def build_message(*args)
15
+ Message.new(*args)
16
+ end
17
+
18
+ # Public: Normalize channel to adapter supported format
19
+ #
20
+ # channel - The string channel name
21
+ #
22
+ # Returns The normalized channel prefixed with supported format for Faye
23
+ def normalize_channel(channel)
24
+ "/#{channel}"
25
+ end
26
+
27
+
28
+ class Message
29
+
30
+ attr_accessor :channel, :data
31
+
32
+ def self.batch_publish(messages)
33
+ if RenderSync.async?
34
+ batch_publish_asynchronous(messages)
35
+ else
36
+ batch_publish_synchronous(messages)
37
+ end
38
+ end
39
+
40
+ def self.batch_publish_synchronous(messages)
41
+ Net::HTTP.post_form(
42
+ URI.parse(RenderSync.server),
43
+ message: batch_messages_query_hash(messages).to_json
44
+ )
45
+ end
46
+
47
+ def self.batch_publish_asynchronous(messages)
48
+ RenderSync.reactor.perform do
49
+ EM::HttpRequest.new(RenderSync.server).post(body: {
50
+ message: batch_messages_query_hash(messages).to_json
51
+ })
52
+ end
53
+ end
54
+
55
+ def self.batch_messages_query_hash(messages)
56
+ {
57
+ channel: "/batch_publish",
58
+ data: messages.collect(&:to_hash),
59
+ ext: { auth_token: RenderSync.auth_token }
60
+ }
61
+ end
62
+
63
+ def initialize(channel, data)
64
+ self.channel = channel
65
+ self.data = data
66
+ end
67
+
68
+ def to_hash
69
+ {
70
+ channel: channel,
71
+ data: data,
72
+ ext: {
73
+ auth_token: RenderSync.auth_token
74
+ }
75
+ }
76
+ end
77
+
78
+ def to_json
79
+ to_hash.to_json
80
+ end
81
+
82
+ def publish
83
+ if RenderSync.async?
84
+ publish_asynchronous
85
+ else
86
+ publish_synchronous
87
+ end
88
+ end
89
+
90
+ def publish_synchronous
91
+ Net::HTTP.post_form URI.parse(RenderSync.server), message: to_json
92
+ end
93
+
94
+ def publish_asynchronous
95
+ RenderSync.reactor.perform do
96
+ EM::HttpRequest.new(RenderSync.server).post(body: {
97
+ message: self.to_json
98
+ })
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end