activerecord-publishable 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +22 -0
- data/README.md +87 -0
- data/Rakefile +43 -0
- data/activerecord-publishable.gemspec +29 -0
- data/examples/sse/app.rb +28 -0
- data/examples/sse/change_data.rb +44 -0
- data/examples/sse/post.rb +22 -0
- data/examples/sse/public/app.js +70 -0
- data/examples/sse/public/streaming.gif +0 -0
- data/examples/sse/views/index.html.erb +32 -0
- data/lib/active_record/publishable.rb +68 -0
- data/lib/active_record/publishable/version.rb +5 -0
- data/lib/activerecord-publishable.rb +1 -0
- data/spec/active_record/publishable_spec.rb +146 -0
- data/spec/spec_helper.rb +44 -0
- metadata +161 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7996fc0dec2bf180ae15316bbd309cc3d769d26e
|
4
|
+
data.tar.gz: ba8319dbb4a8b5d18f1258db438eae2ef9fd3264
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7b9d8a175f343875fe6ee22096cdb2aa764ca3d94ae818958b48b4876c2458013876d6b6e0f8f1b16094b5d8ef1532d69fb1043965a45027be5c2ef621d9ec2d
|
7
|
+
data.tar.gz: cbf93e800ba381ab97238478e58c983bb60bb53dc521f0342fdb1d396307811f1f2383aed73d89878540778715a2527201b934b9f9656ec6c18edcaff6afc09f
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Ray Zane
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# ActiveRecord::Publishable
|
2
|
+
|
3
|
+
Publish changes to your ActiveRecord models to Redis for use with PubSub. This allows you to easily implement Server-Sent events.
|
4
|
+
|
5
|
+

|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'activerecord-publishable'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install activerecord-publishable
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
To start publishing changes to your models, include the module and call `publishable`.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class Post < ActiveRecord::Base
|
29
|
+
include ActiveRecord::Publishable
|
30
|
+
|
31
|
+
publishable
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
The `publishable` method creates an `after_commit` hook on create, update, and destroy. You can pass the `:on` option to only push certain events. It accepts all of the options you can pass to an `after_commit` hook, like `:if` and `:unless`.
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
publishable on: [:update, :destroy]
|
39
|
+
publishable on: :create, if: :some_condition?
|
40
|
+
publishable on: :create, unless: ->{ dont_push? }
|
41
|
+
```
|
42
|
+
|
43
|
+
When pushing to Redis, your model will be serialized as JSON. If you have `ActiveModel::Serializers` loaded, your serializer will be looked up. Otherwise, `as_json` will be used.
|
44
|
+
|
45
|
+
You can pass options for serialization by using the `:serialize` option. The `:with` option allows you to override the serializer that is used.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
publishable serialize: { with: OtherPostSerializer }
|
49
|
+
publishable serialize: { only: [:title, :content] }
|
50
|
+
```
|
51
|
+
|
52
|
+
By default, events will be published to `<collection>:create`, `<collection>:update`, and `<collection>:destroy`, where collection is the plural name of your model. To override this behavior, you can pass the `:channel` option:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
publishable on: :create, channel: "custom:create"
|
56
|
+
```
|
57
|
+
|
58
|
+
Alternatively, you can override `channel_for_publishing`:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
class Post < ActiveRecord::Base
|
62
|
+
include ActiveRecord::Publishable
|
63
|
+
|
64
|
+
publishable
|
65
|
+
|
66
|
+
def channel_for_publishing(action)
|
67
|
+
"posts:#{user.id}:#{action}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
## Running the demo app
|
73
|
+
|
74
|
+
The demo app provides an example of streaming changes to your models using Server-Sent events.
|
75
|
+
|
76
|
+
1. Clone the repository
|
77
|
+
2. Run `bundle install`
|
78
|
+
3. Run `rake example`
|
79
|
+
4. Open your browser to localhost:4567
|
80
|
+
|
81
|
+
## Contributing
|
82
|
+
|
83
|
+
1. Fork it ( https://github.com/rzane/activerecord-publishable/fork )
|
84
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
85
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
86
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
87
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'PublicActivityAggregate'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
require "bundler/gem_tasks"
|
18
|
+
|
19
|
+
Bundler::GemHelper.install_tasks
|
20
|
+
|
21
|
+
require "rake/testtask"
|
22
|
+
|
23
|
+
Rake::TestTask.new :spec do |t|
|
24
|
+
t.libs << 'lib' << 'spec'
|
25
|
+
t.test_files = FileList['spec/**/*_spec.rb']
|
26
|
+
t.verbose = false
|
27
|
+
end
|
28
|
+
|
29
|
+
task :default => :spec
|
30
|
+
|
31
|
+
namespace :examples do
|
32
|
+
namespace :sse do
|
33
|
+
task :app do
|
34
|
+
ruby 'examples/sse/app.rb'
|
35
|
+
end
|
36
|
+
|
37
|
+
task :data do
|
38
|
+
ruby 'examples/sse/change_data.rb'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
multitask :sse => ['sse:app', 'sse:data']
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'active_record/publishable/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "activerecord-publishable"
|
8
|
+
spec.version = ActiveRecord::Publishable::VERSION
|
9
|
+
spec.authors = ["Ray Zane"]
|
10
|
+
spec.email = ["raymondzane@gmail.com"]
|
11
|
+
spec.summary = %q{Redis PubSub for ActiveRecord models.}
|
12
|
+
spec.description = %q{Publish events to Redis when your models change.}
|
13
|
+
spec.homepage = "https://github.com/rzane/activerecord-publishable"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "redis"
|
22
|
+
spec.add_dependency "activerecord"
|
23
|
+
spec.add_dependency "activesupport"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "minitest"
|
28
|
+
spec.add_development_dependency "mocha"
|
29
|
+
end
|
data/examples/sse/app.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.require(:default, :examples)
|
3
|
+
|
4
|
+
ActiveRecord::Publishable.redis = Redis.new(driver: :celluloid)
|
5
|
+
|
6
|
+
class Angelo::Server
|
7
|
+
def report_errors?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class App < Angelo::Base
|
13
|
+
report_errors!
|
14
|
+
|
15
|
+
get '/' do
|
16
|
+
erb :index
|
17
|
+
end
|
18
|
+
|
19
|
+
eventsource '/events/posts' do |sse|
|
20
|
+
ActiveRecord::Publishable.redis.psubscribe('posts:*') do |on|
|
21
|
+
on.pmessage do |_, channel, message|
|
22
|
+
sse.event channel.split(':').last, message
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
App.run!
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.require :default
|
3
|
+
|
4
|
+
require_relative './post'
|
5
|
+
|
6
|
+
# Running this file will create/update/delete records for demonstration purposess.
|
7
|
+
module Changer
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def change action = nil
|
11
|
+
@index = index + 1
|
12
|
+
send action || [:create, :update, :destroy].sample
|
13
|
+
end
|
14
|
+
|
15
|
+
def create
|
16
|
+
post = Post.create content: "Post ##{index}"
|
17
|
+
puts "[Model] Created ##{post.id}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def update
|
21
|
+
with_random_post 'Updated' do |post|
|
22
|
+
post.increment! :update_count
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def destroy
|
27
|
+
with_random_post('Deleted') { |post| post.destroy }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def index; @index || 0 end
|
33
|
+
|
34
|
+
def with_random_post action
|
35
|
+
if post = Post.offset(rand(Post.count)).first
|
36
|
+
yield post
|
37
|
+
puts "[Model] #{action} ##{post.id}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
Post.delete_all
|
43
|
+
5.times { Changer.change :create }
|
44
|
+
loop { Changer.change; sleep 1 }
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.require :default
|
3
|
+
|
4
|
+
# Connect to an inmemory database
|
5
|
+
ActiveRecord::Base.establish_connection adapter: 'sqlite3',
|
6
|
+
database: 'db.sqlite3'
|
7
|
+
|
8
|
+
ActiveRecord::Base.raise_in_transactional_callbacks = true
|
9
|
+
|
10
|
+
conn = ActiveRecord::Base.connection
|
11
|
+
conn.create_table :posts do |t|
|
12
|
+
t.string :content
|
13
|
+
t.integer :update_count, default: 0
|
14
|
+
t.timestamps null: false
|
15
|
+
end unless conn.table_exists?(:posts)
|
16
|
+
|
17
|
+
# Here's the important part. Define our model and enable streaming.
|
18
|
+
class Post < ActiveRecord::Base
|
19
|
+
include ActiveRecord::Publishable
|
20
|
+
|
21
|
+
publishable
|
22
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
window.App = function(url, verbs) {
|
4
|
+
this.verbs = verbs;
|
5
|
+
this.stream = new EventSource(url);
|
6
|
+
this.el = $('#posts');
|
7
|
+
};
|
8
|
+
|
9
|
+
$.extend(App.prototype, {
|
10
|
+
start: function() {
|
11
|
+
var post,
|
12
|
+
that = this;
|
13
|
+
|
14
|
+
this.stream.addEventListener('open', function() {
|
15
|
+
console.log('Listening to ' + this.url);
|
16
|
+
});
|
17
|
+
|
18
|
+
// Bind create, update, and destroy
|
19
|
+
$.each(this.verbs, function(index, verb) {
|
20
|
+
that.stream.addEventListener(verb, function(event) {
|
21
|
+
post = JSON.parse(event.data);
|
22
|
+
console.log(verb, post);
|
23
|
+
that[verb](post);
|
24
|
+
});
|
25
|
+
});
|
26
|
+
},
|
27
|
+
|
28
|
+
attributes: ['id', 'content', 'update_count', 'lastUpdated'],
|
29
|
+
|
30
|
+
render: function(post) {
|
31
|
+
post.lastUpdated = new Date(post.updated_at).toLocaleString();
|
32
|
+
|
33
|
+
var cols = $.map(this.attributes, function(attr) {
|
34
|
+
return $('<td>').text(post[attr]);
|
35
|
+
});
|
36
|
+
|
37
|
+
return $('<tr>').attr('id', 'post' + post.id).append(cols);
|
38
|
+
},
|
39
|
+
|
40
|
+
find: function(post) {
|
41
|
+
return this.el.find('#post' + post.id);
|
42
|
+
},
|
43
|
+
|
44
|
+
create: function(post) {
|
45
|
+
this.el.prepend(this.animate(this.render(post), 'bounceInLeft'));
|
46
|
+
},
|
47
|
+
|
48
|
+
update: function(post) {
|
49
|
+
var tmpl = this.render(post), row = this.find(post);
|
50
|
+
if (!row.length) { return this.create(post); }
|
51
|
+
|
52
|
+
this.animate(row, 'shake', function() {
|
53
|
+
row.replaceWith(tmpl);
|
54
|
+
});
|
55
|
+
},
|
56
|
+
|
57
|
+
destroy: function(post) {
|
58
|
+
var row = this.find(post);
|
59
|
+
this.animate(row, 'bounceOutRight', function(){ row.remove(); });
|
60
|
+
},
|
61
|
+
|
62
|
+
animate: function(el, name, callback) {
|
63
|
+
el.removeClass().addClass('animated ' + name);
|
64
|
+
return callback ? setTimeout(callback, 600) : el;
|
65
|
+
}
|
66
|
+
});
|
67
|
+
|
68
|
+
$(document).ready(function() {
|
69
|
+
new App('/events/posts', ['create', 'update', 'destroy']).start();
|
70
|
+
});
|
Binary file
|
@@ -0,0 +1,32 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<title>Streaming Demo Page</title>
|
4
|
+
<link href="https://fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
|
5
|
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.2.6/animate.min.css" rel="stylesheet" type="text/css">
|
6
|
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" rel="stylesheet" type="text/css">
|
7
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
|
8
|
+
<script src="./app.js"></script>
|
9
|
+
<style>
|
10
|
+
h1 { margin-top: 5rem; }
|
11
|
+
</style>
|
12
|
+
</head>
|
13
|
+
|
14
|
+
<body>
|
15
|
+
<div class="container">
|
16
|
+
<h1 class="title">Streaming Demo Page</h1>
|
17
|
+
|
18
|
+
<table class="u-full-width">
|
19
|
+
<thead>
|
20
|
+
<tr>
|
21
|
+
<th>#</th>
|
22
|
+
<th>Content</th>
|
23
|
+
<th>Number of updates</th>
|
24
|
+
<th>Last updated</th>
|
25
|
+
</tr>
|
26
|
+
</thead>
|
27
|
+
|
28
|
+
<tbody id="posts"></tbody>
|
29
|
+
</table>
|
30
|
+
</div>
|
31
|
+
</body>
|
32
|
+
</html>
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'active_record/publishable/version'
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
module Publishable
|
6
|
+
class << self
|
7
|
+
attr_writer :redis
|
8
|
+
|
9
|
+
def included(base)
|
10
|
+
base.extend ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
def disable!
|
14
|
+
@disabled = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def enable!
|
18
|
+
@disabled = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def disabled?
|
22
|
+
!!@disabled
|
23
|
+
end
|
24
|
+
|
25
|
+
def redis
|
26
|
+
@redis ||= Redis.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def publish(channel, message)
|
30
|
+
redis.publish(channel, message) unless disabled?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
def publishable(options = {})
|
36
|
+
Array(options.fetch(:on, [:create, :update, :destroy])).each do |verb|
|
37
|
+
after_commit options.merge(on: verb) do
|
38
|
+
unless ActiveRecord::Publishable.disabled?
|
39
|
+
publish_action(verb, options)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def publish_action(action, options = {})
|
47
|
+
channel = options[:channel] || channel_for_publishing(action)
|
48
|
+
data = serialize_for_publishing(options.fetch(:serialize, {}))
|
49
|
+
|
50
|
+
ActiveRecord::Publishable.publish(channel, data.to_json)
|
51
|
+
end
|
52
|
+
|
53
|
+
def serialize_for_publishing(options = {})
|
54
|
+
opts = options.reverse_merge(root: false).except(:with)
|
55
|
+
|
56
|
+
serializer = options.fetch :with do
|
57
|
+
next unless defined? ActiveModel::Serializer
|
58
|
+
ActiveModel::Serializer.serializer_for self
|
59
|
+
end
|
60
|
+
|
61
|
+
serializer ? serializer.new(self, opts) : as_json(opts)
|
62
|
+
end
|
63
|
+
|
64
|
+
def channel_for_publishing(action)
|
65
|
+
"#{self.class.model_name.collection}:#{action}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'active_record/publishable'
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
describe Publishable do
|
5
|
+
describe '.disable!' do
|
6
|
+
after { Publishable.enable! }
|
7
|
+
|
8
|
+
it 'can be disabled' do
|
9
|
+
Publishable.enable!
|
10
|
+
Publishable.disable!
|
11
|
+
Publishable.disabled?.must_equal true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '.enable!' do
|
16
|
+
it 'can be re-enabled' do
|
17
|
+
Publishable.disable!
|
18
|
+
Publishable.enable!
|
19
|
+
Publishable.disabled?.must_equal false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '.redis=' do
|
24
|
+
it 'can be assigned' do
|
25
|
+
Publishable.redis = 'fake'
|
26
|
+
Publishable.redis.must_equal 'fake'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '.publish' do
|
31
|
+
let(:redis) { mock('Redis') }
|
32
|
+
|
33
|
+
before { Publishable.redis = redis }
|
34
|
+
after { Publishable.enable! }
|
35
|
+
|
36
|
+
it 'provides a convience publish method' do
|
37
|
+
redis.expects(:publish).at_least_once
|
38
|
+
Publishable.publish('channel', msg: 'yo')
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'does nothing when Publishable.disabled?' do
|
42
|
+
Publishable.disable!
|
43
|
+
|
44
|
+
redis.expects(:publish).times(0)
|
45
|
+
Publishable.publish('channel', msg: 'yo')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'ClassMethods' do
|
50
|
+
describe '.publishable' do
|
51
|
+
before do
|
52
|
+
Foo._commit_callbacks.clear
|
53
|
+
Foo.must_have_commit_callbacks 0
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should define default after_commit callbacks' do
|
57
|
+
Foo.publishable
|
58
|
+
Foo.must_have_commit_callbacks 3
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should allow specifying :on action' do
|
62
|
+
Foo.publishable on: [:create, :update]
|
63
|
+
Foo.must_have_commit_callbacks 2
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should allow :on to be a symbol' do
|
67
|
+
Foo.publishable on: :create
|
68
|
+
Foo.must_have_commit_callbacks 1
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should accept :if option' do
|
72
|
+
Foo.expects(:after_commit).with(on: :create, if: :something?)
|
73
|
+
Foo.publishable on: :create, if: :something?
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should accept :unless option' do
|
77
|
+
Foo.expects(:after_commit).with(on: :create, unless: :something?)
|
78
|
+
Foo.publishable on: :create, unless: :something?
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
let(:foo) { Foo.new name: 'foo' }
|
84
|
+
let(:bar) { Bar.new name: 'bar' }
|
85
|
+
|
86
|
+
describe '#publish_action' do
|
87
|
+
let(:channel) { foo.channel_for_publishing :create }
|
88
|
+
let(:data) { foo.serialize_for_publishing }
|
89
|
+
|
90
|
+
it 'should publish the event and data' do
|
91
|
+
Publishable.expects(:publish).with(channel, data.to_json)
|
92
|
+
foo.publish_action(:create)
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'should pass :serialize option to #serialize_for_publishing' do
|
96
|
+
Publishable.stubs :publish
|
97
|
+
foo.expects(:serialize_for_publishing).with only: [:name]
|
98
|
+
foo.publish_action :create, serialize: { only: [:name] }
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should not do anything when disabled' do
|
102
|
+
Publishable.stubs(:disabled?).returns(true)
|
103
|
+
Publishable.expects(:redis).never
|
104
|
+
foo.publish_action :create
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe '#serialize_for_publishing' do
|
109
|
+
it 'should infer an active model serializer' do
|
110
|
+
foo.serialize_for_publishing.must_be_instance_of FooSerializer
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should use serializer from :with option' do
|
114
|
+
serialized = foo.serialize_for_publishing with: OtherFooSerializer
|
115
|
+
serialized.must_be_instance_of OtherFooSerializer
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'should default to root: false element' do
|
119
|
+
FooSerializer.expects(:new).with foo, root: false
|
120
|
+
foo.serialize_for_publishing
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'should pass :serialize option to the serializer' do
|
124
|
+
FooSerializer.expects(:new).with foo, root: 'foo', only: [:name]
|
125
|
+
foo.serialize_for_publishing root: 'foo', only: [:name]
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'should serialize with as_json when serializer not found' do
|
129
|
+
bar.expects(:as_json).with root: false, only: [:name]
|
130
|
+
bar.serialize_for_publishing root: false, only: [:name]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe '#channel_for_publishing' do
|
135
|
+
it 'should infer a reasonable default' do
|
136
|
+
foo.channel_for_publishing(:create).must_equal 'foos:create'
|
137
|
+
foo.channel_for_publishing(:update).must_equal 'foos:update'
|
138
|
+
foo.channel_for_publishing(:destroy).must_equal 'foos:destroy'
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'should allow non-standard events' do
|
142
|
+
foo.channel_for_publishing(:custom).must_equal 'foos:custom'
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.require :default
|
3
|
+
|
4
|
+
require 'active_record'
|
5
|
+
require 'minitest/autorun'
|
6
|
+
require 'mocha/mini_test'
|
7
|
+
|
8
|
+
FileUtils.rm_rf File.expand_path('../test.sqlite3', __FILE__)
|
9
|
+
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: 'test.sqlite3'
|
10
|
+
ActiveRecord::Base.raise_in_transactional_callbacks = true
|
11
|
+
|
12
|
+
conn = ActiveRecord::Base.connection
|
13
|
+
|
14
|
+
[:foos, :bars].each do |table|
|
15
|
+
conn.create_table table, force: true do |t|
|
16
|
+
t.string :name
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Foo < ActiveRecord::Base
|
21
|
+
include ActiveRecord::Publishable
|
22
|
+
end
|
23
|
+
|
24
|
+
class Bar < ActiveRecord::Base
|
25
|
+
include ActiveRecord::Publishable
|
26
|
+
end
|
27
|
+
|
28
|
+
# Create some ActiveModel::Serializers (for integration testing)
|
29
|
+
class FooSerializer < ActiveModel::Serializer
|
30
|
+
attributes :id, :name
|
31
|
+
end
|
32
|
+
|
33
|
+
class OtherFooSerializer < FooSerializer; end
|
34
|
+
|
35
|
+
|
36
|
+
module MiniTest::Assertions
|
37
|
+
def assert_commit_callbacks(expected, klass)
|
38
|
+
assert_equal expected, klass._commit_callbacks.select { |cb|
|
39
|
+
cb.kind == :after && cb.name == :commit
|
40
|
+
}.size
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Class.infect_an_assertion :assert_commit_callbacks, :must_have_commit_callbacks
|
metadata
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-publishable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ray Zane
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-10-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activesupport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.7'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.7'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '10.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: mocha
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: Publish events to Redis when your models change.
|
112
|
+
email:
|
113
|
+
- raymondzane@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- Gemfile
|
120
|
+
- LICENSE.txt
|
121
|
+
- README.md
|
122
|
+
- Rakefile
|
123
|
+
- activerecord-publishable.gemspec
|
124
|
+
- examples/sse/app.rb
|
125
|
+
- examples/sse/change_data.rb
|
126
|
+
- examples/sse/post.rb
|
127
|
+
- examples/sse/public/app.js
|
128
|
+
- examples/sse/public/streaming.gif
|
129
|
+
- examples/sse/views/index.html.erb
|
130
|
+
- lib/active_record/publishable.rb
|
131
|
+
- lib/active_record/publishable/version.rb
|
132
|
+
- lib/activerecord-publishable.rb
|
133
|
+
- spec/active_record/publishable_spec.rb
|
134
|
+
- spec/spec_helper.rb
|
135
|
+
homepage: https://github.com/rzane/activerecord-publishable
|
136
|
+
licenses:
|
137
|
+
- MIT
|
138
|
+
metadata: {}
|
139
|
+
post_install_message:
|
140
|
+
rdoc_options: []
|
141
|
+
require_paths:
|
142
|
+
- lib
|
143
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
148
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
requirements: []
|
154
|
+
rubyforge_project:
|
155
|
+
rubygems_version: 2.4.8
|
156
|
+
signing_key:
|
157
|
+
specification_version: 4
|
158
|
+
summary: Redis PubSub for ActiveRecord models.
|
159
|
+
test_files:
|
160
|
+
- spec/active_record/publishable_spec.rb
|
161
|
+
- spec/spec_helper.rb
|