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,21 @@
1
+ module RenderSync
2
+ class RefetchPartialCreator < PartialCreator
3
+
4
+ def initialize(name, resource, scoped_resource, context)
5
+ super
6
+ self.partial = RefetchPartial.new(name, self.resource.model, nil, context)
7
+ end
8
+
9
+ def message
10
+ RenderSync.client.build_message(channel,
11
+ refetch: true,
12
+ resourceId: resource.id,
13
+ authToken: partial.auth_token,
14
+ channelUpdate: partial.channel_for_action(:update),
15
+ channelDestroy: partial.channel_for_action(:destroy),
16
+ selectorStart: partial.selector_start,
17
+ selectorEnd: partial.selector_end
18
+ )
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module RenderSync
2
+ class Renderer
3
+
4
+ attr_accessor :context
5
+
6
+ def initialize
7
+ self.context = ApplicationController.new.view_context
8
+ self.context.instance_eval do
9
+ def url_options
10
+ ActionMailer::Base.default_url_options
11
+ end
12
+ end
13
+ end
14
+
15
+ def render_to_string(options)
16
+ context.render(options)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,115 @@
1
+ require 'pathname'
2
+
3
+ module RenderSync
4
+ class Resource
5
+ attr_accessor :model, :scopes
6
+
7
+ # Constructor
8
+ #
9
+ # model - The ActiveModel instace for this Resource
10
+ # scopes - The optional scopes to prefix polymorphic paths with.
11
+ # Can be a Symbol/String, a parent model or an RenderSync::Scope
12
+ # or an Array with any combination.
13
+ #
14
+ # Examples
15
+ #
16
+ # class User < ActiveRecord::Base
17
+ # sync :all
18
+ # sync_scope :cool, -> { where(cool: true) }
19
+ # sync_scope :in_group, ->(group) { where(group_id: group.id) }
20
+ # end
21
+ #
22
+ # user = User.find(1)
23
+ #
24
+ # resource = Resource.new(user)
25
+ # resource.polymorphic_path => "/users/1"
26
+ # resource.polymorphic_new_path => "/users/new"
27
+ #
28
+ # resource = Resource.new(user, :admin)
29
+ # resource.polymorphic_path => "/admin/users/1"
30
+ # resource.polymorphic_new_path => "/admin/users/new"
31
+ #
32
+ # resource = Resource.new(user, [:staff, :restricted])
33
+ # resource.polymorphic_path => "/staff/restricted/users/1"
34
+ # resource.polymorphic_new_path => "/staff/restricted/users/new"
35
+ #
36
+ # resource = Resource.new(user, project)
37
+ # resource.polymorphic_path => "/projects/2/users/1"
38
+ # resource.polymorphic_new_path => "/projects/2/users/new"
39
+ #
40
+ # resource = Resource.new(user, User.cool)
41
+ # resource.polymorphic_path => "/cool/users/2"
42
+ # resource.polymorphic_new_path => "/cool/users/new"
43
+ #
44
+ # resource = Resource.new(user, User.in_group(group))
45
+ # resource.polymorphic_path => "/in_group/group/3/users/2"
46
+ # resource.polymorphic_new_path => "/in_group/group/3/users/new"
47
+ #
48
+ # resource = Resource.new(user, [:admin, User.cool, User.in_group(group)])
49
+ # resource.polymorphic_path => "admin/cool/in_group/group/3/users/2"
50
+ # resource.polymorphic_new_path => "admin/cool/in_group/group/3/users/new"
51
+ #
52
+ # resource = Resource.new(user, [:admin, project])
53
+ # resource.polymorphic_path => "/admin/projects/2/users/1"
54
+ # resource.polymorphic_new_path => "/admin/projects/2/users/new"
55
+ #
56
+ def initialize(model, scopes = nil)
57
+ self.model = model
58
+ self.scopes = scopes
59
+ end
60
+
61
+ def scopes=(new_scopes)
62
+ new_scopes = [new_scopes] unless new_scopes.nil? or new_scopes.is_a? Array
63
+ @scopes = new_scopes
64
+ end
65
+
66
+ def id
67
+ model.id
68
+ end
69
+
70
+ def name
71
+ model.class.model_name.to_s.underscore
72
+ end
73
+
74
+ def base_name
75
+ name.split('/').last
76
+ end
77
+
78
+ def plural_name
79
+ name.pluralize
80
+ end
81
+
82
+ def scopes_path
83
+ path = Pathname.new('/')
84
+ unless scopes.nil?
85
+ paths = scopes.map do |scope|
86
+ if scope.is_a?(RenderSync::Scope)
87
+ scope.polymorphic_path.relative_path_from(path)
88
+ elsif scope.class.respond_to? :model_name
89
+ Resource.new(scope).polymorphic_path.relative_path_from(path)
90
+ else
91
+ scope.to_s
92
+ end
93
+ end
94
+ path = path.join(*paths)
95
+ end
96
+ path
97
+ end
98
+
99
+ # Returns an unscoped Pathname for the model (e.g. /users/1)
100
+ def model_path
101
+ Pathname.new('/').join(plural_name, id.to_s)
102
+ end
103
+
104
+ # Returns the scoped Pathname for the model (e.g. /users/1/todos/2)
105
+ def polymorphic_path
106
+ scopes_path.join(plural_name, id.to_s)
107
+ end
108
+
109
+ # Returns the scoped Pathname for a new model (e.g. /users/1/todos/new)
110
+ def polymorphic_new_path
111
+ scopes_path.join(plural_name, "new")
112
+ end
113
+ end
114
+
115
+ end
@@ -0,0 +1,113 @@
1
+ module RenderSync
2
+ class Scope
3
+ attr_accessor :scope_definition, :args, :valid
4
+
5
+ def initialize(scope_definition, args)
6
+ @scope_definition = scope_definition
7
+ @args = args
8
+ end
9
+
10
+ # Return a new sync scope by passing a scope definition (containing a lambda and parameter names)
11
+ # and a set of arguments to be handed over to the lambda
12
+ def self.new_from_args(scope_definition, args)
13
+ if args.length != scope_definition.parameters.length
14
+ raise ArgumentError, "wrong number of arguments (#{args.length} for #{scope_definition.parameters.length})"
15
+ end
16
+
17
+ # Classes currently supported as Arguments for the sync scope lambda
18
+ supported_arg_types = [Fixnum, Integer, ActiveRecord::Base]
19
+
20
+ # Check passed args for types. Raise ArgumentError if arg class is not supported
21
+ args.each_with_index do |arg, i|
22
+
23
+ unless supported_arg_types.find { |klass| break true if arg.is_a?(klass) }
24
+ param = scope_definition.parameters[i]
25
+ raise ArgumentError, "invalid argument '#{param}' (#{arg.class.name}). Currently only #{supported_arg_types.map(&:name).join(", ")} are supported"
26
+ end
27
+ end
28
+
29
+ new(scope_definition, args)
30
+ end
31
+
32
+ # Return a new sync scope by passing a scope definition (containing a lambda and parameter names)
33
+ # and an ActiveRecord model object. The args List will be filled with the model attributes
34
+ # corrensponding to the parameter names defined in the scope_definition
35
+ def self.new_from_model(scope_definition, model)
36
+ new(scope_definition, scope_definition.parameters.map { |p| model.send(p) })
37
+ end
38
+
39
+ # Return the ActiveRecord Relation by calling the lamda with the given args.
40
+ #
41
+ def relation
42
+ scope_definition.lambda.call(*args)
43
+ end
44
+
45
+ # Check if the combination of stored AR relation and args is valid by calling exists? on it.
46
+ # This may raise an exception depending on the args, so we have to rescue the block
47
+ #
48
+ def valid?
49
+ @valid ||= begin
50
+ relation.exists?
51
+ true # set valid to true, if relation.exists?(model) does not throw any exception
52
+ rescue
53
+ false
54
+ end
55
+ end
56
+
57
+ def invalid?
58
+ !valid?
59
+ end
60
+
61
+ # Check if the given record falls under the narrowing by the stored ActiveRecord Relation.
62
+ # Depending on the arguments set in args this can lead to an exception (e.g. when a nil is passed)
63
+ # Also set the value of valid to avoid another DB query.
64
+ #
65
+ def contains?(record)
66
+ begin
67
+ val = relation.exists?(record.id)
68
+ @valid = true # set valid to true, if relation.exists?(model) does not throw any exception
69
+ val
70
+ rescue
71
+ @valid = false
72
+ end
73
+ end
74
+
75
+ # Generates an Array of path elements based on the given lambda args and their
76
+ # name which is saved in scope_definition.parameters
77
+ #
78
+ def args_path
79
+ scope_definition.parameters.each_with_index.map do |parameter, i|
80
+ if args[i].is_a? ActiveRecord::Base
81
+ [parameter.to_s, args[i].send(args[i].class.primary_key).to_s]
82
+ else
83
+ [parameter.to_s, args[i].to_s]
84
+ end
85
+ end.flatten
86
+ end
87
+
88
+ # Returns the Pathname for this scope
89
+ # Example:
90
+ # class User < ActiveRecord::Base
91
+ # sync :all
92
+ # belongs_to :group
93
+ # sync_scope :in_group, ->(group) { where(group_id: group.id) }
94
+ # end
95
+ #
96
+ # group = Group.first
97
+ # User.in_group(group).polymorphic_path.to_s
98
+ # # => "/in_group/group/1"
99
+ #
100
+ def polymorphic_path
101
+ Pathname.new('/').join(*([scope_definition.name.to_s, args_path].flatten))
102
+ end
103
+
104
+ # Delegate all undefined methods to the relation, so that
105
+ # the scope behaves like an ActiveRecord::Relation, e.g. call count
106
+ # on the relation (User.in_group(group).count)
107
+ #
108
+ def method_missing(method, *args, &block)
109
+ relation.send(method, *args, &block)
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,30 @@
1
+ module RenderSync
2
+ class ScopeDefinition
3
+ attr_accessor :klass, :name, :lambda, :parameters, :args
4
+
5
+ def initialize(klass, name, lambda)
6
+ self.class.ensure_valid_params!(klass, lambda)
7
+
8
+ @klass = klass
9
+ @name = name
10
+ @lambda = lambda
11
+ @parameters = lambda.parameters.map { |p| p[1] }
12
+ end
13
+
14
+ # Checks the validity of the parameter names contained in the lambda definition.
15
+ # E.g. if the lambda looks like this:
16
+ #
17
+ # ->(user) { where(user_id: user.id) }
18
+ #
19
+ # The name of the passed argument (user) must be present as a column name or an
20
+ # instance method (e.g. an association) of the ActiveRecord object.
21
+ #
22
+ def self.ensure_valid_params!(klass, lambda)
23
+ unless (invalid = lambda.parameters.map { |p| p[1] } - klass.column_names.map(&:to_sym) - klass.instance_methods) == []
24
+ raise ArgumentError, "Invalid parameters #{invalid}. Parameter names of the sync_scope lambda definition may only contain ActiveRecord column names or instance methods of #{klass.name}."
25
+ end
26
+ true
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,106 @@
1
+ module RenderSync
2
+
3
+ module ViewHelpers
4
+
5
+ # Surround partial render in script tags, watching for
6
+ # sync_update and sync_destroy channels from pubsub server
7
+ #
8
+ # options - The Hash of options
9
+ # partial - The String partial filename without leading underscore
10
+ # resource - The ActiveModel resource
11
+ # collection - The Array of ActiveModel resources to use in place of
12
+ # single resource
13
+ #
14
+ # Examples
15
+ # <%= sync partial: 'todo', resource: todo %>
16
+ # <%= sync partial: 'todo', collection: todos %>
17
+ #
18
+ def sync(options = {})
19
+ collection = options[:collection] || [options.fetch(:resource)]
20
+ scope = options[:channel] || options[:scope] || (collection.is_a?(RenderSync::Scope) ? collection : nil)
21
+ partial_name = options.fetch(:partial, scope)
22
+ refetch = options.fetch(:refetch, false)
23
+
24
+ results = []
25
+ collection.each do |resource|
26
+ if refetch
27
+ partial = RefetchPartial.new(partial_name, resource, scope, self)
28
+ else
29
+ partial = Partial.new(partial_name, resource, scope, self)
30
+ end
31
+ results << "
32
+ <script type='text/javascript' data-sync-id='#{partial.selector_start}'>
33
+ RenderSync.onReady(function(){
34
+ var partial = new RenderSync.Partial({
35
+ name: '#{partial.name}',
36
+ resourceName: '#{partial.resource.name}',
37
+ resourceId: '#{resource.id}',
38
+ authToken: '#{partial.refetch_auth_token}',
39
+ channelUpdate: '#{partial.channel_for_action(:update)}',
40
+ channelDestroy: '#{partial.channel_for_action(:destroy)}',
41
+ selectorStart: '#{partial.selector_start}',
42
+ selectorEnd: '#{partial.selector_end}',
43
+ refetch: #{refetch}
44
+ });
45
+ partial.subscribe();
46
+ });
47
+ </script>
48
+ ".squish.html_safe
49
+ results << partial.render
50
+ results << "
51
+ <script type='text/javascript' data-sync-id='#{partial.selector_end}'>
52
+ </script>
53
+ ".squish.html_safe
54
+ end
55
+
56
+ safe_join(results)
57
+ end
58
+
59
+ # Setup listener for new resource from sync_new channel, appending
60
+ # partial in place
61
+ #
62
+ # options - The Hash of options
63
+ # partial - The String partial filename without leading underscore
64
+ # resource - The ActiveModel resource
65
+ # scope - The ActiveModel resource to scope the new channel publishes to.
66
+ # Used for restricting new resource publishes to 'owner' models.
67
+ # ie, current_user, project, group, etc. When excluded, listens
68
+ # for global resource creates.
69
+ #
70
+ # direction - The String/Symbol direction to insert rendered partials.
71
+ # One of :append, :prepend. Defaults to :append
72
+ #
73
+ # Examples
74
+ # <%= sync_new partial: 'todo', resource: Todo.new, scope: @project %>
75
+ # <%= sync_new partial: 'todo', resource: Todo.new, scope: @project, direction: :prepend %>
76
+ #
77
+ def sync_new(options = {})
78
+ partial_name = options.fetch(:partial)
79
+ scope = options[:scope]
80
+ direction = options.fetch :direction, 'append'
81
+ refetch = options.fetch(:refetch, false)
82
+ resource = scope.is_a?(RenderSync::Scope) ? scope.new : options.fetch(:resource)
83
+
84
+ if refetch
85
+ creator = RefetchPartialCreator.new(partial_name, resource, scope, self)
86
+ else
87
+ creator = PartialCreator.new(partial_name, resource, scope, self)
88
+ end
89
+ "
90
+ <script type='text/javascript' data-sync-id='#{creator.selector}'>
91
+ RenderSync.onReady(function(){
92
+ var creator = new RenderSync.PartialCreator({
93
+ name: '#{partial_name}',
94
+ resourceName: '#{creator.resource.name}',
95
+ channel: '#{creator.channel}',
96
+ selector: '#{creator.selector}',
97
+ direction: '#{direction}',
98
+ refetch: #{refetch}
99
+ });
100
+ creator.subscribe();
101
+ });
102
+ </script>
103
+ ".html_safe
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Dummy::Application.load_tasks