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