lifesaver 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.coveralls.yml +0 -0
- data/.gitignore +17 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +93 -0
- data/Rakefile +5 -0
- data/lib/lifesaver.rb +24 -0
- data/lib/lifesaver/index_graph.rb +53 -0
- data/lib/lifesaver/index_worker.rb +13 -0
- data/lib/lifesaver/marshal.rb +43 -0
- data/lib/lifesaver/model_additions.rb +133 -0
- data/lib/lifesaver/railtie.rb +9 -0
- data/lib/lifesaver/version.rb +3 -0
- data/lib/lifesaver/visitor_worker.rb +9 -0
- data/lifesaver.gemspec +31 -0
- data/spec/lifesaver/index_graph_spec.rb +64 -0
- data/spec/lifesaver/marshal_spec.rb +80 -0
- data/spec/lifesaver/model_additions_spec.rb +91 -0
- data/spec/lifesaver_spec.rb +111 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/active_record.rb +53 -0
- data/spec/support/test_models.rb +90 -0
- metadata +223 -0
data/.coveralls.yml
ADDED
File without changes
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Paul Sorensen
|
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,93 @@
|
|
1
|
+
# Lifesaver
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/paulnsorensen/lifesaver.png?branch=master)](https://travis-ci.org/paulnsorensen/lifesaver)
|
4
|
+
[![Dependency Status](https://gemnasium.com/paulnsorensen/lifesaver.png)](https://gemnasium.com/paulnsorensen/lifesaver)
|
5
|
+
[![Coverage Status](https://coveralls.io/repos/paulnsorensen/lifesaver/badge.png)](https://coveralls.io/r/paulnsorensen/lifesaver)
|
6
|
+
[![Code Climate](https://codeclimate.com/github/paulnsorensen/lifesaver.png)](https://codeclimate.com/github/paulnsorensen/lifesaver)
|
7
|
+
|
8
|
+
Indexes your ActiveRecord models in [elasticsearch](https://github.com/elasticsearch/elasticsearch) asynchronously by making use of [tire](https://github.com/karmi/tire) and [resque](https://github.com/resque/resque) (hence the name: resque + tire = lifesaver). Using lifesaver, you can easily control when or if to reindex your model depending on your context. Lifesaver also provides the ability to traverse ActiveRecord associations to trigger the index updates of related models.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
gem 'lifesaver', git: "git://github.com/paulnsorensen/lifesaver.git"
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install lifesaver
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
Replaces the tire callbacks in your models
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class Article < ActiveRecord::Base
|
30
|
+
include Tire::Model::Search
|
31
|
+
# Replace the following include with Lifesaver
|
32
|
+
# include Tire::Model::Callbacks
|
33
|
+
enqueues_indexing
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
#### Configurable Behavior
|
38
|
+
You can decided when or if the index gets updated at all based on your current situation. Lifesaver exposes two methods (`supress_indexing`, `unsuppress_indexing`) that set a model's indexing behavior until that model is saved.
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class ArticlesController < ApplicationController
|
42
|
+
def suppressed_update
|
43
|
+
@article = Article.find(params[:id])
|
44
|
+
@article.attributes = params[:article]
|
45
|
+
|
46
|
+
# No reindexing will occur at all
|
47
|
+
@article.suppress_indexing
|
48
|
+
|
49
|
+
@article.save!
|
50
|
+
|
51
|
+
# Not neccessary but if saved
|
52
|
+
# after this following call,
|
53
|
+
# this article would reindex
|
54
|
+
@article.unsuppress_indexing
|
55
|
+
end
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
#### ActiveRecord Association Traversal
|
60
|
+
Lifesaver can trigger other models to reindex if you have nested models in your indexes that you would like to update. Use the `notifies_for_indexing` method to indicate which related models should be marked for indexing. Any associations passed will be both updated when a model is changed (`save` or `destroy`) and when another model notifies it. Any associations passed in the options will only notify when the model is changed or notified when specified in the `only_on_change` or `only_on_notify` keys, respectively.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class Article < ActiveRecord::Base
|
64
|
+
belongs_to :author
|
65
|
+
belongs_to :category
|
66
|
+
has_many :watchers
|
67
|
+
has_one :moderator
|
68
|
+
|
69
|
+
notifies_for_indexing :author,
|
70
|
+
only_on_change: :category,
|
71
|
+
only_on_notify: [:watchers, :moderator]
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
## Integration with Resque
|
76
|
+
You will see two new queues: `lifesaver_indexing` and `lifesaver_notification`
|
77
|
+
|
78
|
+
## Contributing
|
79
|
+
|
80
|
+
1. Fork it
|
81
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
82
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
83
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
84
|
+
5. Create new Pull Request
|
85
|
+
|
86
|
+
## TODO
|
87
|
+
+ specify which fields will trigger indexing changes
|
88
|
+
+ configuration options
|
89
|
+
+ bulk indexing
|
90
|
+
+ resque-scheduler to provide `delay_indexing` and `enqueues_indexing after: 30.minutes, on: :save`
|
91
|
+
+ unsuppress indexing after save
|
92
|
+
+ sidekiq support
|
93
|
+
+ prepare for new elasticsearch library
|
data/Rakefile
ADDED
data/lib/lifesaver.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'resque-loner'
|
2
|
+
require "lifesaver/version"
|
3
|
+
require "lifesaver/marshal"
|
4
|
+
require "lifesaver/index_graph"
|
5
|
+
require "lifesaver/model_additions"
|
6
|
+
require "lifesaver/index_worker"
|
7
|
+
require "lifesaver/visitor_worker"
|
8
|
+
require "lifesaver/railtie" if defined? Rails
|
9
|
+
|
10
|
+
module Lifesaver
|
11
|
+
@@suppress_indexing = false
|
12
|
+
|
13
|
+
def self.suppress_indexing
|
14
|
+
@@suppress_indexing = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.unsuppress_indexing
|
18
|
+
@@suppress_indexing = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.indexing_suppressed?
|
22
|
+
@@suppress_indexing
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Lifesaver
|
2
|
+
class IndexGraph
|
3
|
+
def self.generate(marshalled_models)
|
4
|
+
models_to_index = []
|
5
|
+
visited_models = {}
|
6
|
+
graph = []
|
7
|
+
marshalled_models.each do |m|
|
8
|
+
mdl, opts = Lifesaver::Marshal.load(m)
|
9
|
+
if mdl
|
10
|
+
if opts[:status] == :notified
|
11
|
+
graph << mdl
|
12
|
+
elsif opts[:status] == :changed
|
13
|
+
visited_models[self.visited_model_key(mdl)] = true
|
14
|
+
graph |= self.notified_models(mdl, true)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
graph.each {|m| visited_models[self.visited_model_key(m)] = true }
|
19
|
+
while !graph.empty?
|
20
|
+
mdl = graph.shift
|
21
|
+
models_to_index << mdl if mdl.has_index?
|
22
|
+
self.notified_models(mdl).each do |m|
|
23
|
+
unless visited_models[self.visited_model_key(m)]
|
24
|
+
visited_models[self.visited_model_key(m)] = true
|
25
|
+
graph << m
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
models_to_index
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.visited_model_key(mdl)
|
33
|
+
if mdl.is_a?(Hash)
|
34
|
+
klass = mdl[:class].to_s.classify
|
35
|
+
"#{klass}_#{mdl[:id]}"
|
36
|
+
elsif mdl.try(:id)
|
37
|
+
"#{mdl.class.name}_#{mdl.id}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.notified_models(mdl, on_change = false)
|
42
|
+
if Lifesaver::Marshal.is_serialized?(mdl)
|
43
|
+
mdl, opts = Lifesaver::Marshal.load(mdl)
|
44
|
+
end
|
45
|
+
models = []
|
46
|
+
key = on_change ? :on_change : :on_notify
|
47
|
+
mdl.class.notifiable_associations[key].each do |assoc|
|
48
|
+
models |= mdl.association_models(assoc)
|
49
|
+
end
|
50
|
+
models
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Lifesaver::IndexWorker
|
2
|
+
include ::Resque::Plugins::UniqueJob
|
3
|
+
@queue = :lifesaver_indexing
|
4
|
+
def self.perform(class_name, id, action)
|
5
|
+
klass = class_name.to_s.classify.constantize
|
6
|
+
case action.to_sym
|
7
|
+
when :update
|
8
|
+
klass.find(id).update_index if klass.exists?(id)
|
9
|
+
when :destroy
|
10
|
+
klass.index.remove({type: klass.document_type, id: id})
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Lifesaver
|
2
|
+
class Marshal
|
3
|
+
def self.dump(obj, opts={})
|
4
|
+
raise unless opts.is_a?(Hash)
|
5
|
+
opts[:class] = obj.class.name.underscore.to_sym
|
6
|
+
opts[:id] = obj.id
|
7
|
+
opts
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.load(obj)
|
11
|
+
raise unless self.is_serialized?(obj)
|
12
|
+
obj = self.sanitize(obj)
|
13
|
+
klass = obj[:class].to_s.classify.constantize
|
14
|
+
if klass.exists?(obj[:id])
|
15
|
+
mdl = klass.find(obj[:id])
|
16
|
+
obj.delete(:id)
|
17
|
+
obj.delete(:class)
|
18
|
+
return mdl, obj
|
19
|
+
else
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.sanitize(obj)
|
25
|
+
raise unless obj.is_a?(Hash)
|
26
|
+
obj = obj.symbolize_keys
|
27
|
+
obj[:id] = obj[:id].to_i if obj[:id]
|
28
|
+
obj[:class] = obj[:class].to_sym if obj[:class]
|
29
|
+
obj[:status] = obj[:status].to_sym if obj[:status]
|
30
|
+
obj
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.is_serialized?(obj)
|
34
|
+
if obj.is_a?(Hash)
|
35
|
+
obj = self.sanitize(obj)
|
36
|
+
if obj.key?(:class) && obj.key?(:id)
|
37
|
+
return true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module Lifesaver
|
2
|
+
module ModelAdditions
|
3
|
+
module ClassMethods
|
4
|
+
|
5
|
+
def notifies_for_indexing(*args)
|
6
|
+
self.notifiable_associations = { on_change: [], on_notify: [] }
|
7
|
+
opts = args.pop if args.last.is_a?(Hash)
|
8
|
+
args.each do |a|
|
9
|
+
self.notifiable_associations[:on_change] << a
|
10
|
+
self.notifiable_associations[:on_notify] << a
|
11
|
+
end
|
12
|
+
%w(on_change on_notify).each do |k|
|
13
|
+
opt = opts[("only_" + k).to_sym] if opts
|
14
|
+
key = k.to_sym
|
15
|
+
if opt
|
16
|
+
if opt.is_a?(Array)
|
17
|
+
self.notifiable_associations[key] |= opt
|
18
|
+
else
|
19
|
+
self.notifiable_associations[key] << opt
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
notification_callbacks
|
24
|
+
end
|
25
|
+
|
26
|
+
def enqueues_indexing(options = {})
|
27
|
+
indexing_callbacks
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def notification_callbacks(options={})
|
33
|
+
after_save do
|
34
|
+
send :update_associations, options.merge(operation: :update)
|
35
|
+
end
|
36
|
+
before_destroy do
|
37
|
+
send :update_associations, options.merge(operation: :destroy)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def indexing_callbacks(options={})
|
42
|
+
after_save do
|
43
|
+
send :enqueue_indexing, options.merge(operation: :update)
|
44
|
+
end
|
45
|
+
after_destroy do
|
46
|
+
send :enqueue_indexing, options.merge(operation: :destroy)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.included(base)
|
52
|
+
base.class_attribute :notifiable_associations
|
53
|
+
base.notifiable_associations = { on_change: [], on_notify: [] }
|
54
|
+
base.extend(ClassMethods)
|
55
|
+
end
|
56
|
+
|
57
|
+
def association_models(assoc)
|
58
|
+
models = []
|
59
|
+
association = send(assoc.to_sym)
|
60
|
+
unless association.nil?
|
61
|
+
if association.respond_to?(:each)
|
62
|
+
association.each do |m|
|
63
|
+
models << m
|
64
|
+
end
|
65
|
+
else
|
66
|
+
models << association
|
67
|
+
end
|
68
|
+
end
|
69
|
+
models
|
70
|
+
end
|
71
|
+
|
72
|
+
def has_index?
|
73
|
+
self.respond_to?(:tire)
|
74
|
+
end
|
75
|
+
|
76
|
+
def suppress_indexing
|
77
|
+
@indexing_suppressed = true
|
78
|
+
end
|
79
|
+
|
80
|
+
def unsuppress_indexing
|
81
|
+
@indexing_suppressed = false
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def enqueue_indexing(opts)
|
87
|
+
if has_index? && !suppress_indexing?
|
88
|
+
::Resque.enqueue(
|
89
|
+
Lifesaver::IndexWorker,
|
90
|
+
self.class.name.underscore.to_sym,
|
91
|
+
self.id,
|
92
|
+
opts[:operation]
|
93
|
+
)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def dependent_association_map
|
98
|
+
dependent = {}
|
99
|
+
self.class.reflect_on_all_associations.each do |assoc|
|
100
|
+
dependent[assoc.name.to_sym] = true if assoc.options[:dependent].present?
|
101
|
+
end
|
102
|
+
dependent
|
103
|
+
end
|
104
|
+
|
105
|
+
def update_associations(opts)
|
106
|
+
models = []
|
107
|
+
if opts[:operation] == :destroy
|
108
|
+
dependent = dependent_association_map
|
109
|
+
assoc_models = []
|
110
|
+
self.class.notifiable_associations[:on_change].each do |assoc|
|
111
|
+
assoc_models |= association_models(assoc) unless dependent[assoc]
|
112
|
+
end
|
113
|
+
assoc_models.each do |m|
|
114
|
+
models << Lifesaver::Marshal.dump(m, {status: :notified})
|
115
|
+
end
|
116
|
+
elsif opts[:operation] == :update
|
117
|
+
models << Lifesaver::Marshal.dump(self, {status: :changed})
|
118
|
+
end
|
119
|
+
|
120
|
+
::Resque.enqueue(Lifesaver::VisitorWorker, models) unless models.empty?
|
121
|
+
end
|
122
|
+
|
123
|
+
def validate_options(options)
|
124
|
+
# on: should only have active model callback verbs (create, update, destroy?)
|
125
|
+
# after: (next versions after you use resque scheduler) time to schedule
|
126
|
+
# only: specifies fields that trigger changes
|
127
|
+
end
|
128
|
+
|
129
|
+
def suppress_indexing?
|
130
|
+
Lifesaver.indexing_suppressed? || @indexing_suppressed || false
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class Lifesaver::VisitorWorker
|
2
|
+
include Resque::Plugins::UniqueJob
|
3
|
+
@queue = :lifesaver_notification
|
4
|
+
def self.perform(models)
|
5
|
+
Lifesaver::IndexGraph.generate(models).each do |m|
|
6
|
+
Resque.enqueue(Lifesaver::IndexWorker, m.class.name.underscore.to_sym, m.id, :update) if m.has_index?
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
data/lifesaver.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'lifesaver/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "lifesaver"
|
8
|
+
spec.version = Lifesaver::VERSION
|
9
|
+
spec.authors = ["Paul Sorensen"]
|
10
|
+
spec.email = ["paulnsorensen@gmail.com"]
|
11
|
+
spec.description = %q{Indexes your ActiveRecord models in elasticsearch asynchronously by making use of tire and resque}
|
12
|
+
spec.summary = %q{Indexes your ActiveRecord models in elasticsearch asynchronously by making use of tire and resque}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
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.required_ruby_version = '>= 1.9'
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
spec.add_development_dependency "rspec"
|
25
|
+
spec.add_development_dependency "activerecord"
|
26
|
+
spec.add_development_dependency "sqlite3"
|
27
|
+
spec.add_runtime_dependency 'activerecord', '>= 3'
|
28
|
+
spec.add_runtime_dependency 'tire', '~> 0.6.0'
|
29
|
+
spec.add_runtime_dependency 'resque', '~> 1.25.0'
|
30
|
+
spec.add_runtime_dependency 'resque-loner', '~> 1.2.1'
|
31
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lifesaver::IndexGraph do
|
4
|
+
Lifesaver.suppress_indexing
|
5
|
+
|
6
|
+
describe ".visited_model_key" do
|
7
|
+
it "should return a key when passed an ActiveRecord model" do
|
8
|
+
post = Post.create(title: "Some post")
|
9
|
+
expect(Lifesaver::IndexGraph.visited_model_key(post)).to eql("Post_1")
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should return a key when passed a Hash" do
|
13
|
+
post = {class: "post", id: "1", status: "notified"}
|
14
|
+
expect(Lifesaver::IndexGraph.visited_model_key(post)).to eql("Post_1")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe ".notified_models" do
|
19
|
+
after(:all) do
|
20
|
+
Post.destroy_all
|
21
|
+
Author.destroy_all
|
22
|
+
Authorship.destroy_all
|
23
|
+
end
|
24
|
+
context "when passed model has changed" do
|
25
|
+
before(:each) do
|
26
|
+
@post = Post.create(title: "Some post")
|
27
|
+
@author = Author.create(name: "Some guy")
|
28
|
+
Authorship.create(post: @post, author: @author)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should return notified models when passed an ActiveRecord model" do
|
32
|
+
models = Lifesaver::IndexGraph.notified_models(@post, true)
|
33
|
+
expect(models.size).to eql(1)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should return notified models when passed a Hash" do
|
37
|
+
post = {class: "post", id: "1", status: "changed"}
|
38
|
+
models = Lifesaver::IndexGraph.notified_models(post, true)
|
39
|
+
expect(models.size).to eql(1)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "when passed model has not changed" do
|
44
|
+
before(:each) do
|
45
|
+
@post = Post.create(title: "Some post")
|
46
|
+
@author = Author.create(name: "Some guy")
|
47
|
+
Authorship.create(post: @post, author: @author)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should return notified models when passed an ActiveRecord model" do
|
51
|
+
models = Lifesaver::IndexGraph.notified_models(@author)
|
52
|
+
expect(models.size).to eql(1)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should return notified models when passed a Hash" do
|
56
|
+
author = {class: "author", id: "1", status: "notified"}
|
57
|
+
models = Lifesaver::IndexGraph.notified_models(author)
|
58
|
+
expect(models.size).to eql(1)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
Lifesaver.unsuppress_indexing
|
64
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lifesaver::Marshal do
|
4
|
+
describe ".is_serialized?" do
|
5
|
+
it "returns true if Hash has :id and :class" do
|
6
|
+
obj = { class: :post, id: 1 }
|
7
|
+
expect(Lifesaver::Marshal.is_serialized?(obj)).to eql(true)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "returns true if Hash has 'id' and 'class'" do
|
11
|
+
obj = { "class" => "post", "id" => "1" }
|
12
|
+
expect(Lifesaver::Marshal.is_serialized?(obj)).to eql(true)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "returns false if Hash does not have id and class keys" do
|
16
|
+
obj = { some_key: 1, id: 4 }
|
17
|
+
expect(Lifesaver::Marshal.is_serialized?(obj)).to eql(false)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "returns false if passed an non-Hash object" do
|
21
|
+
obj = Post.new
|
22
|
+
expect(Lifesaver::Marshal.is_serialized?(obj)).to eql(false)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe ".sanitize" do
|
27
|
+
it "returns symbolized version of the Hash'" do
|
28
|
+
obj = { "class" => "post", "id" => "1", "status" => "updated" }
|
29
|
+
out = { class: :post, id: 1, status: :updated }
|
30
|
+
expect(Lifesaver::Marshal.sanitize(obj)).to eql(out)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "rejects non-Hashes" do
|
34
|
+
obj = Post.new
|
35
|
+
expect { Lifesaver::Marshal.sanitize(obj) }.to raise_error
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe ".load" do
|
40
|
+
before(:all) do
|
41
|
+
Post.create(title: "Test Post")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "loads a serialized object from ActiveRecord" do
|
45
|
+
obj = { class: :post, id: 1 }
|
46
|
+
expect(Lifesaver::Marshal.load(obj)).to eql([Post.find(1), {}])
|
47
|
+
end
|
48
|
+
|
49
|
+
it "returns nil if model not found" do
|
50
|
+
obj = { class: :post, id: 12 }
|
51
|
+
expect(Lifesaver::Marshal.load(obj)).to eql(nil)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "returns options if they were passed" do
|
55
|
+
obj = { class: :post, id: 1, status: :notified }
|
56
|
+
m = Lifesaver::Marshal.load(obj)
|
57
|
+
expect(m).to eql([Post.find(1), {status: :notified}])
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
it "rejects bad input" do
|
62
|
+
obj = Post.new
|
63
|
+
expect { Lifesaver::Marshal.load(obj) }.to raise_error
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe ".dump" do
|
68
|
+
it "decomposes an object to a Hash" do
|
69
|
+
obj = Post.create(title: "Test Post")
|
70
|
+
out = { class: :post, id: 2 }
|
71
|
+
expect(Lifesaver::Marshal.dump(obj)).to eql(out)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "adds additional key, value pairs if passed" do
|
75
|
+
obj = Post.create(title: "Test Post")
|
76
|
+
out = { status: :updated, class: :post, id: 2 }
|
77
|
+
expect(Lifesaver::Marshal.dump(obj, {status: :updated})).to eql(out)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lifesaver::ModelAdditions do
|
4
|
+
Lifesaver.suppress_indexing
|
5
|
+
|
6
|
+
describe ".notifies_for_indexing" do
|
7
|
+
after(:each) do
|
8
|
+
Post.send(:notifies_for_indexing, only_on_change: :authorships)
|
9
|
+
end
|
10
|
+
it "should return a Hash of models for notification on save and notify" do
|
11
|
+
Post.send(:notifies_for_indexing, :comments,
|
12
|
+
only_on_change: :authorships,
|
13
|
+
only_on_notify: :authors
|
14
|
+
)
|
15
|
+
exp_hash = { on_change: [], on_notify: [] }
|
16
|
+
exp_hash[:on_change] << :comments
|
17
|
+
exp_hash[:on_change] << :authorships
|
18
|
+
exp_hash[:on_notify] << :comments
|
19
|
+
exp_hash[:on_notify] << :authors
|
20
|
+
expect(Post.notifiable_associations).to eql(exp_hash)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#association_models" do
|
25
|
+
before(:each) do
|
26
|
+
@post = Post.create(title: "Some post")
|
27
|
+
affiliate = Affiliate.create(name: "Some place")
|
28
|
+
@author = Author.create(name: "Some guy", affiliate_id: affiliate.id)
|
29
|
+
Authorship.create(post: @post, author: @author)
|
30
|
+
end
|
31
|
+
after(:all) do
|
32
|
+
Post.destroy_all
|
33
|
+
Author.destroy_all
|
34
|
+
Authorship.destroy_all
|
35
|
+
Affiliate.destroy_all
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should return an array of models for multiple association" do
|
39
|
+
association = @post.association_models(:authorships)
|
40
|
+
expect(association[0]).to be_a_kind_of(Authorship)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should return an array of one model for a singular association" do
|
44
|
+
association = @author.association_models(:affiliate)
|
45
|
+
expect(association[0]).to be_a_kind_of(Affiliate)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
Lifesaver.unsuppress_indexing
|
50
|
+
|
51
|
+
describe "#indexing_suppressed?" do
|
52
|
+
let(:post) { Post.new(title: "Test Post") }
|
53
|
+
|
54
|
+
it "should be false by default" do
|
55
|
+
expect(post.send(:suppress_indexing?)).to eql(false)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should be true if overridden locally" do
|
59
|
+
Lifesaver.suppress_indexing
|
60
|
+
expect(post.send(:suppress_indexing?)).to eql(true)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should be false if override is cancelled" do
|
64
|
+
Lifesaver.unsuppress_indexing
|
65
|
+
expect(post.send(:suppress_indexing?)).to eql(false)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should be true if set individually" do
|
69
|
+
post.suppress_indexing
|
70
|
+
expect(post.send(:suppress_indexing?)).to eql(true)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should be false if unset individually" do
|
74
|
+
post.unsuppress_indexing
|
75
|
+
expect(post.send(:suppress_indexing?)).to eql(false)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#dependent_association_map" do
|
80
|
+
it "returns an empty Hash when there are no dependent associations" do
|
81
|
+
expect(Comment.new.send(:dependent_association_map)).to eql({})
|
82
|
+
end
|
83
|
+
|
84
|
+
it "returns a Hash with the keys of dependent_association" do
|
85
|
+
dependent_map = Author.new.send(:dependent_association_map)
|
86
|
+
expect(dependent_map).to eql({authorships: true})
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lifesaver do
|
4
|
+
before(:all) do
|
5
|
+
Lifesaver.suppress_indexing
|
6
|
+
Post.destroy_all
|
7
|
+
Author.destroy_all
|
8
|
+
Authorship.destroy_all
|
9
|
+
Affiliate.destroy_all
|
10
|
+
Comment.destroy_all
|
11
|
+
Lifesaver.unsuppress_indexing
|
12
|
+
end
|
13
|
+
before(:each) do
|
14
|
+
[Author, Post].each do |klass|
|
15
|
+
klass.tire.index.delete
|
16
|
+
klass.tire.create_elasticsearch_index
|
17
|
+
end
|
18
|
+
|
19
|
+
Lifesaver.suppress_indexing
|
20
|
+
|
21
|
+
@posts = []
|
22
|
+
@posts << Post.create(
|
23
|
+
title: "Lifesavers are my favorite candy",
|
24
|
+
content: "Lorem ipsum",
|
25
|
+
tags: %w(candy stuff opinions)
|
26
|
+
)
|
27
|
+
@posts << Post.create(
|
28
|
+
title: "Birds are the best animal",
|
29
|
+
content: "Lorem ipsum",
|
30
|
+
tags: %w(animals stuff facts)
|
31
|
+
)
|
32
|
+
@posts << Post.create(
|
33
|
+
title: "Chicago Cubs have a winning season",
|
34
|
+
content: "Lorem ipsum",
|
35
|
+
tags: %w(sports stuff jokes)
|
36
|
+
)
|
37
|
+
@comments = []
|
38
|
+
@comments << Comment.create(
|
39
|
+
post: @posts.last,
|
40
|
+
text: "We love this!"
|
41
|
+
)
|
42
|
+
@comments << Comment.create(
|
43
|
+
post: @posts.last,
|
44
|
+
text: "We lied. Didn't realize it was a joke."
|
45
|
+
)
|
46
|
+
@authors = []
|
47
|
+
@authors << Author.create(name: "Paul Sorensen")
|
48
|
+
@authors << Author.create(name: "Paul Sorensen's Ghost Writer")
|
49
|
+
@authors << Author.create(name: "Theo Epstein")
|
50
|
+
@affiliates = []
|
51
|
+
@affiliates << Affiliate.create(name: "Prosper Forebearer")
|
52
|
+
@affiliates << Affiliate.create(name: "Chicago Cubs")
|
53
|
+
@authors[0].affiliate_id = @affiliates.first.id
|
54
|
+
@authors[1].affiliate_id = @affiliates.first.id
|
55
|
+
@authors[2].affiliate_id = @affiliates.last.id
|
56
|
+
@authors.each { |a| a.save! }
|
57
|
+
Authorship.create(post: @posts[0], author: @authors[0])
|
58
|
+
Authorship.create(post: @posts[1], author: @authors[0])
|
59
|
+
Authorship.create(post: @posts[1], author: @authors[1])
|
60
|
+
Authorship.create(post: @posts[2], author: @authors[2])
|
61
|
+
|
62
|
+
Lifesaver.unsuppress_indexing
|
63
|
+
|
64
|
+
[Author, Post].each do |klass|
|
65
|
+
klass.all.each { |k| k.tire.update_index }
|
66
|
+
klass.tire.index.refresh
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
after(:each) do
|
71
|
+
Post.destroy_all
|
72
|
+
Author.destroy_all
|
73
|
+
Authorship.destroy_all
|
74
|
+
Affiliate.destroy_all
|
75
|
+
Comment.destroy_all
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should traverse the provided graph" do
|
79
|
+
models = Lifesaver::IndexGraph.generate([{"class"=>"author", "id"=>1, "status"=>"changed"}])
|
80
|
+
expect(models.size).to eql(2)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should reindex on destroy" do
|
84
|
+
@authors[2].destroy
|
85
|
+
sleep(1.seconds)
|
86
|
+
expect(Author.search(query: "Theo Epstein").to_a.size).to eql(0)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should reindex on update" do
|
90
|
+
@authors[2].name = "Harry Carry"
|
91
|
+
@authors[2].save!
|
92
|
+
sleep(1.seconds) # need to wait for elasticsearch to update
|
93
|
+
expect(Author.search(query: "Harry Carry").to_a.size).to eql(1)
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should update distant related indexes" do
|
97
|
+
@posts[0].tags << 'werd'
|
98
|
+
@posts[0].save!
|
99
|
+
sleep(1.seconds)
|
100
|
+
expect(Author.search(query: "werd").to_a.size).to eql(1)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should update related indexes if saved model doesn't have index" do
|
104
|
+
@comments[0].text = "We hate this!"
|
105
|
+
@comments[0].save!
|
106
|
+
sleep(1.seconds)
|
107
|
+
result = Post.search(query: "Chicago").to_a.first
|
108
|
+
comment_text = result.comments.first.text
|
109
|
+
expect(comment_text).to eql("We hate this!")
|
110
|
+
end
|
111
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'coveralls'
|
2
|
+
Coveralls.wear!
|
3
|
+
|
4
|
+
require 'lifesaver'
|
5
|
+
|
6
|
+
require 'support/active_record'
|
7
|
+
require 'support/test_models'
|
8
|
+
ActiveSupport.on_load :active_record do
|
9
|
+
include Lifesaver::ModelAdditions
|
10
|
+
end
|
11
|
+
|
12
|
+
Resque.inline = true
|
13
|
+
Tire::Model::Search.index_prefix "lifesaver_test"
|
14
|
+
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
# config.expect_with :rspec do |c|
|
18
|
+
# c.syntax = :expect
|
19
|
+
# end
|
20
|
+
|
21
|
+
config.around do |example|
|
22
|
+
ActiveRecord::Base.transaction do
|
23
|
+
example.run
|
24
|
+
raise ActiveRecord::Rollback
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
3
|
+
|
4
|
+
module ActiveModel::Validations
|
5
|
+
# Extension to enhance `should have` on AR Model instances. Calls
|
6
|
+
# model.valid? in order to prepare the object's errors object.
|
7
|
+
#
|
8
|
+
# You can also use this to specify the content of the error messages.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# model.should have(:no).errors_on(:attribute)
|
13
|
+
# model.should have(1).error_on(:attribute)
|
14
|
+
# model.should have(n).errors_on(:attribute)
|
15
|
+
#
|
16
|
+
# model.errors_on(:attribute).should include("can't be blank")
|
17
|
+
def errors_on(attribute)
|
18
|
+
self.valid?
|
19
|
+
[self.errors[attribute]].flatten.compact
|
20
|
+
end
|
21
|
+
alias :error_on :errors_on
|
22
|
+
end
|
23
|
+
|
24
|
+
# test models we'll use in each spec
|
25
|
+
ActiveRecord::Migration.create_table :comments do |t|
|
26
|
+
t.string :text
|
27
|
+
t.integer :post_id
|
28
|
+
t.timestamps
|
29
|
+
end
|
30
|
+
|
31
|
+
ActiveRecord::Migration.create_table :posts do |t|
|
32
|
+
t.string :title
|
33
|
+
t.text :content
|
34
|
+
t.text :tags
|
35
|
+
t.timestamps
|
36
|
+
end
|
37
|
+
|
38
|
+
ActiveRecord::Migration.create_table :authors do |t|
|
39
|
+
t.string :name
|
40
|
+
t.integer :affiliate_id
|
41
|
+
t.timestamps
|
42
|
+
end
|
43
|
+
|
44
|
+
ActiveRecord::Migration.create_table :authorships do |t|
|
45
|
+
t.integer :post_id
|
46
|
+
t.integer :author_id
|
47
|
+
t.timestamps
|
48
|
+
end
|
49
|
+
|
50
|
+
ActiveRecord::Migration.create_table :affiliates do |t|
|
51
|
+
t.string :name
|
52
|
+
t.timestamps
|
53
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'tire'
|
2
|
+
|
3
|
+
ActiveSupport.on_load :active_record do
|
4
|
+
include Lifesaver::ModelAdditions
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
class Author < ActiveRecord::Base
|
9
|
+
has_many :authorships, dependent: :destroy
|
10
|
+
has_many :posts, through: :authorships
|
11
|
+
belongs_to :affiliate
|
12
|
+
enqueues_indexing
|
13
|
+
include ::Tire::Model::Search
|
14
|
+
notifies_for_indexing :authorships
|
15
|
+
def post_tags
|
16
|
+
tags = Set.new
|
17
|
+
posts.select(:tags).each do |p|
|
18
|
+
tags |= Set.new(p.tags)
|
19
|
+
end
|
20
|
+
tags.to_a
|
21
|
+
end
|
22
|
+
mapping do
|
23
|
+
indexes :id, type: 'integer', index: 'not_analyzed'
|
24
|
+
indexes :name, type: 'multi_field', fields: {
|
25
|
+
name: {type: 'string', analyzer: 'snowball'},
|
26
|
+
untouched: {type: 'string', index: 'not_analyzed'}
|
27
|
+
}
|
28
|
+
indexes :post_tags, analyzer: 'keyword'
|
29
|
+
end
|
30
|
+
def self.search(params)
|
31
|
+
tire.search do
|
32
|
+
size 100
|
33
|
+
query { string params[:query] } if params[:query].present?
|
34
|
+
sort do
|
35
|
+
by 'name.untouched', :asc
|
36
|
+
end
|
37
|
+
filter :term, affiliate_id: params[:afilliate_id] if params[:affiliate_id].present?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
def to_indexed_json
|
41
|
+
to_json(include: :affiliate, methods: :post_tags)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Authorship < ActiveRecord::Base
|
46
|
+
belongs_to :author
|
47
|
+
belongs_to :post
|
48
|
+
notifies_for_indexing only_on_notify: [:author, :post]
|
49
|
+
end
|
50
|
+
|
51
|
+
class Comment < ActiveRecord::Base
|
52
|
+
belongs_to :post
|
53
|
+
notifies_for_indexing :post
|
54
|
+
end
|
55
|
+
|
56
|
+
class Post < ActiveRecord::Base
|
57
|
+
has_many :comments
|
58
|
+
has_many :authorships, dependent: :destroy
|
59
|
+
has_many :authors, through: :authorships
|
60
|
+
serialize :tags, Array
|
61
|
+
enqueues_indexing
|
62
|
+
notifies_for_indexing only_on_change: :authorships
|
63
|
+
include ::Tire::Model::Search
|
64
|
+
mapping do
|
65
|
+
indexes :id, type: 'integer', index: 'not_analyzed'
|
66
|
+
indexes :title, type: 'multi_field', fields: {
|
67
|
+
title: {type: 'string', analyzer: 'snowball'},
|
68
|
+
untouched: {type: 'string', index: 'not_analyzed'}
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.search(params)
|
73
|
+
tire.search do
|
74
|
+
size 100
|
75
|
+
query { string params[:query] } if params[:query].present?
|
76
|
+
sort do
|
77
|
+
by 'title.untouched', :asc
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_indexed_json
|
83
|
+
to_json(include: :comments, authors: { include: :affiliate })
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class Affiliate < ActiveRecord::Base
|
88
|
+
has_many :authors
|
89
|
+
notifies_for_indexing :authors
|
90
|
+
end
|
metadata
ADDED
@@ -0,0 +1,223 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lifesaver
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Paul Sorensen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-10-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: activerecord
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: sqlite3
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: activerecord
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '3'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '3'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: tire
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.6.0
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ~>
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 0.6.0
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: resque
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ~>
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: 1.25.0
|
134
|
+
type: :runtime
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ~>
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: 1.25.0
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: resque-loner
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ~>
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: 1.2.1
|
150
|
+
type: :runtime
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ~>
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: 1.2.1
|
158
|
+
description: Indexes your ActiveRecord models in elasticsearch asynchronously by making
|
159
|
+
use of tire and resque
|
160
|
+
email:
|
161
|
+
- paulnsorensen@gmail.com
|
162
|
+
executables: []
|
163
|
+
extensions: []
|
164
|
+
extra_rdoc_files: []
|
165
|
+
files:
|
166
|
+
- .coveralls.yml
|
167
|
+
- .gitignore
|
168
|
+
- .travis.yml
|
169
|
+
- CHANGELOG.md
|
170
|
+
- Gemfile
|
171
|
+
- LICENSE.txt
|
172
|
+
- README.md
|
173
|
+
- Rakefile
|
174
|
+
- lib/lifesaver.rb
|
175
|
+
- lib/lifesaver/index_graph.rb
|
176
|
+
- lib/lifesaver/index_worker.rb
|
177
|
+
- lib/lifesaver/marshal.rb
|
178
|
+
- lib/lifesaver/model_additions.rb
|
179
|
+
- lib/lifesaver/railtie.rb
|
180
|
+
- lib/lifesaver/version.rb
|
181
|
+
- lib/lifesaver/visitor_worker.rb
|
182
|
+
- lifesaver.gemspec
|
183
|
+
- spec/lifesaver/index_graph_spec.rb
|
184
|
+
- spec/lifesaver/marshal_spec.rb
|
185
|
+
- spec/lifesaver/model_additions_spec.rb
|
186
|
+
- spec/lifesaver_spec.rb
|
187
|
+
- spec/spec_helper.rb
|
188
|
+
- spec/support/active_record.rb
|
189
|
+
- spec/support/test_models.rb
|
190
|
+
homepage: ''
|
191
|
+
licenses:
|
192
|
+
- MIT
|
193
|
+
post_install_message:
|
194
|
+
rdoc_options: []
|
195
|
+
require_paths:
|
196
|
+
- lib
|
197
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
198
|
+
none: false
|
199
|
+
requirements:
|
200
|
+
- - ! '>='
|
201
|
+
- !ruby/object:Gem::Version
|
202
|
+
version: '1.9'
|
203
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
204
|
+
none: false
|
205
|
+
requirements:
|
206
|
+
- - ! '>='
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
209
|
+
requirements: []
|
210
|
+
rubyforge_project:
|
211
|
+
rubygems_version: 1.8.23
|
212
|
+
signing_key:
|
213
|
+
specification_version: 3
|
214
|
+
summary: Indexes your ActiveRecord models in elasticsearch asynchronously by making
|
215
|
+
use of tire and resque
|
216
|
+
test_files:
|
217
|
+
- spec/lifesaver/index_graph_spec.rb
|
218
|
+
- spec/lifesaver/marshal_spec.rb
|
219
|
+
- spec/lifesaver/model_additions_spec.rb
|
220
|
+
- spec/lifesaver_spec.rb
|
221
|
+
- spec/spec_helper.rb
|
222
|
+
- spec/support/active_record.rb
|
223
|
+
- spec/support/test_models.rb
|