changeling 0.0.7 → 0.1.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.
- data/README.md +32 -22
- data/Rakefile +47 -0
- data/benchmarks/benchmark_setup.rake +16 -0
- data/benchmarks/changeling_benchmark.rake +34 -0
- data/benchmarks/tire_benchmark.rake +29 -0
- data/changeling.gemspec +3 -1
- data/lib/changeling.rb +10 -0
- data/lib/changeling/async/resque_worker.rb +11 -0
- data/lib/changeling/async/sidekiq_worker.rb +16 -0
- data/lib/changeling/async/trackling.rb +27 -0
- data/lib/changeling/exceptions/async_gem_required.rb +9 -0
- data/lib/changeling/probeling.rb +6 -3
- data/lib/changeling/trackling.rb +2 -0
- data/lib/changeling/version.rb +1 -1
- data/spec/controllers/blameling_integration_spec.rb +79 -0
- data/spec/fixtures/app/application.rb +0 -5
- data/spec/fixtures/app/models/async_blog_post.rb +10 -0
- data/spec/fixtures/app/models/async_blog_post_active_record.rb +5 -0
- data/spec/fixtures/app/models/async_blog_post_no_timestamp.rb +9 -0
- data/spec/lib/changeling/async/trackling_spec.rb +126 -0
- data/spec/lib/changeling/models/logling_spec.rb +213 -213
- data/spec/lib/changeling/probeling_spec.rb +25 -25
- data/spec/lib/changeling/trackling_spec.rb +22 -20
- data/spec/spec_helper.rb +21 -5
- metadata +139 -38
- data/spec/controllers/blameling_controller_spec.rb +0 -20
- data/spec/controllers/blog_posts_controller_spec.rb +0 -20
- data/spec/controllers/current_account_controller_spec.rb +0 -20
- data/spec/controllers/no_current_user_controller_spec.rb +0 -20
data/README.md
CHANGED
@@ -5,6 +5,8 @@
|
|
5
5
|
[travis-home]: http://travis-ci.org/
|
6
6
|
[brew-home]: http://mxcl.github.com/homebrew/
|
7
7
|
[elasticsearch-home]: http://www.elasticsearch.org
|
8
|
+
[sidekiq-home]: https://github.com/mperham/sidekiq
|
9
|
+
[resque-home]: https://github.com/defunkt/resque
|
8
10
|
|
9
11
|
A flexible and lightweight object change tracking system.
|
10
12
|
|
@@ -38,7 +40,7 @@ $ brew install elasticsearch
|
|
38
40
|
```
|
39
41
|
|
40
42
|
## Usage
|
41
|
-
|
43
|
+
### Models
|
42
44
|
Include the Trackling module for any class you want to keep track of:
|
43
45
|
|
44
46
|
```ruby
|
@@ -49,35 +51,40 @@ class Post
|
|
49
51
|
end
|
50
52
|
```
|
51
53
|
|
52
|
-
|
54
|
+
### Controllers
|
55
|
+
If you are using a Rails app, and have the notion of a "user"", you may want to track of which user made which changes.
|
56
|
+
Unfortunately models don't understand the concept of the currently signed in user. We'll have to leverage the controller to tack on this information.
|
53
57
|
|
54
58
|
```ruby
|
55
59
|
# Doesn't have to be ApplicationController, perhaps you only want it in controllers for certain resources.
|
56
60
|
class ApplicationController < ActionController::Base
|
57
61
|
include Changeling::Blameling
|
58
|
-
|
62
|
+
|
59
63
|
# Changeling assumes your user is current_user, but if not, override the changeling_blame_user method like so:
|
60
64
|
def changeling_blame_user
|
61
65
|
current_account
|
62
66
|
end
|
63
|
-
|
67
|
+
|
64
68
|
# Controller logic here...
|
65
69
|
end
|
66
70
|
```
|
67
71
|
|
68
|
-
|
69
|
-
|
72
|
+
### Asynchronous Tracking
|
73
|
+
Sometimes in high production load, you don't want another gem clogging up your resource pipeline, slowing down performance and potentially causing downtime.
|
74
|
+
Changeling has built-in Asynchronous support so you don't have to go and write your own callbacks and queues!
|
75
|
+
Changeling is compatible with [Sidekiq][sidekiq-home] and [Resque][resque-home].
|
76
|
+
When your object is saved, a job is placed in the 'changeling' queue.
|
70
77
|
|
71
78
|
```ruby
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
79
|
+
class Post
|
80
|
+
# include Changeling::Trackling
|
81
|
+
include Changeling::Async::Trackling
|
82
|
+
|
83
|
+
# Model logic here...
|
84
|
+
end
|
77
85
|
```
|
78
86
|
|
79
|
-
|
80
|
-
|
87
|
+
### Accessing Changeling's history
|
81
88
|
If you wish to see what has been logged, include the Probeling module:
|
82
89
|
|
83
90
|
```ruby
|
@@ -93,37 +100,40 @@ With Probeling, you can check out the changes that have been made! They're store
|
|
93
100
|
You can access the up to the last 10 changes simply by calling
|
94
101
|
|
95
102
|
```ruby
|
96
|
-
@post.history
|
103
|
+
# Alias for this method: @post.history
|
104
|
+
@post.loglings
|
97
105
|
```
|
98
106
|
|
99
|
-
You can access a different number of records by passing in a number to the .
|
107
|
+
You can access a different number of records by passing in a number to the .loglings method:
|
100
108
|
|
101
109
|
```ruby
|
102
110
|
# Will automatically handle if there are less than the number of histories requested.
|
103
|
-
@post.
|
111
|
+
@post.loglings(50)
|
104
112
|
```
|
105
113
|
|
106
114
|
Access all of an objects history:
|
107
115
|
|
108
116
|
```ruby
|
109
|
-
@post.all_history
|
117
|
+
# Alias for this method: @post.all_history
|
118
|
+
@post.all_loglings
|
110
119
|
```
|
111
120
|
|
112
121
|
Access all of an objects history where a specific field was changed:
|
113
122
|
|
114
123
|
```ruby
|
115
|
-
@post.history_for_field(
|
124
|
+
# Alias for this method: @post.history_for_field(field)
|
125
|
+
@post.loglings_for_field(:title)
|
116
126
|
# Or if you prefer stringified fields:
|
117
|
-
@post.
|
127
|
+
@post.loglings_for_field('title')
|
118
128
|
|
119
129
|
# You can also pass in a number to limit your results
|
120
|
-
@post.
|
130
|
+
@post.loglings_for_field(:title, 10)
|
121
131
|
```
|
122
132
|
|
123
|
-
|
133
|
+
### Logling Properties (history objects):
|
124
134
|
|
125
135
|
```ruby
|
126
|
-
log = @post.
|
136
|
+
log = @post.loglings.first
|
127
137
|
|
128
138
|
log.klass # class of the object that the Logling is tracking.
|
129
139
|
=> Post
|
data/Rakefile
CHANGED
@@ -2,6 +2,20 @@
|
|
2
2
|
require "bundler/gem_tasks"
|
3
3
|
require "rspec/core/rake_task"
|
4
4
|
|
5
|
+
Dir.glob('benchmarks/*.rake').each { |r| import r }
|
6
|
+
|
7
|
+
desc "Run all benchmarks."
|
8
|
+
task :benchmark, :count, :fields do |t, args|
|
9
|
+
args.with_defaults(:count => 1000, :fields => 20)
|
10
|
+
|
11
|
+
Rake::Task["benchmark_setup"].invoke
|
12
|
+
Rake::Task["benchmark_tire"].reenable
|
13
|
+
Rake::Task["benchmark_tire"].invoke
|
14
|
+
puts "====================================================================\n\n"
|
15
|
+
Rake::Task["benchmark_changeling"].reenable
|
16
|
+
Rake::Task["benchmark_changeling"].invoke
|
17
|
+
end
|
18
|
+
|
5
19
|
RSpec::Core::RakeTask.new("spec") do |spec|
|
6
20
|
spec.pattern = "spec/**/*_spec.rb"
|
7
21
|
end
|
@@ -12,3 +26,36 @@ RSpec::Core::RakeTask.new('spec:progress') do |spec|
|
|
12
26
|
end
|
13
27
|
|
14
28
|
task :default => :spec
|
29
|
+
|
30
|
+
|
31
|
+
# Helper methods
|
32
|
+
def generate_loglings(count, fields)
|
33
|
+
hashes = generate_logling_hashes(count, fields)
|
34
|
+
|
35
|
+
loglings = []
|
36
|
+
|
37
|
+
hashes.each { |hash| loglings << Changeling::Models::Logling.new(hash) }
|
38
|
+
|
39
|
+
loglings
|
40
|
+
end
|
41
|
+
|
42
|
+
def generate_logling_hashes(count, fields)
|
43
|
+
hashes = []
|
44
|
+
count.times do
|
45
|
+
hash = {}
|
46
|
+
|
47
|
+
hash['klass'] = "Object"
|
48
|
+
hash['oid'] = BSON::ObjectId.new.to_s
|
49
|
+
hash['modified_by'] = BSON::ObjectId.new.to_s
|
50
|
+
hash['modified_at'] = Time.now.to_s
|
51
|
+
hash['modifications'] = {}
|
52
|
+
fields.times do |time|
|
53
|
+
hash['modifications']["name_of_field_#{time}"] = ["value_of_field_before_#{time}", "value_of_field_after_#{time}"]
|
54
|
+
end
|
55
|
+
hash['modifications'] = hash['modifications'].to_json
|
56
|
+
|
57
|
+
hashes << hash
|
58
|
+
end
|
59
|
+
|
60
|
+
hashes
|
61
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
task :benchmark_setup do
|
2
|
+
Rake::Task["install"].invoke
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'changeling'
|
6
|
+
require 'bson'
|
7
|
+
rescue LoadError
|
8
|
+
require 'rubygems'
|
9
|
+
require 'changeling'
|
10
|
+
require 'bson'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Setup Tire stuff for benchmarks.
|
14
|
+
Tire::Model::Search.index_prefix "changeling_benchmark"
|
15
|
+
Changeling::Models::Logling.tire.index.delete
|
16
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
desc "Benchmark Changeling processing times, from after_save hook through the final Tire index update."
|
2
|
+
task :benchmark_changeling, :count, :fields do |t, args|
|
3
|
+
args.with_defaults(:count => 1000, :fields => 20)
|
4
|
+
count = args[:count].to_i
|
5
|
+
fields = args[:fields].to_i
|
6
|
+
|
7
|
+
Rake::Task["benchmark_setup"].invoke
|
8
|
+
|
9
|
+
puts "Benchmarking Changeling: #{count} object(s) with #{fields} fields each.".upcase
|
10
|
+
puts "Generating objects. This may take a minute..."
|
11
|
+
|
12
|
+
hashes = generate_logling_hashes(count, fields)
|
13
|
+
loglings = []
|
14
|
+
|
15
|
+
puts "Done generating objects. Proceeding to benchmark..."
|
16
|
+
|
17
|
+
Benchmark.bmbm do |bm|
|
18
|
+
bm.report('Creation of Logling Index') do
|
19
|
+
Changeling::Models::Logling.tire.create_elasticsearch_index
|
20
|
+
end
|
21
|
+
|
22
|
+
bm.report("Initializing Loglings") do
|
23
|
+
hashes.each { |hash| Changeling::Models::Logling.new(hash) }
|
24
|
+
end
|
25
|
+
|
26
|
+
bm.report('Creating Loglings') do
|
27
|
+
hashes.each { |hash| Changeling::Models::Logling.create(hash) }
|
28
|
+
end
|
29
|
+
|
30
|
+
bm.report('Deletion of Logling Index') do
|
31
|
+
Changeling::Models::Logling.tire.index.delete
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
desc "Benchmark Tire indexing times alone when objects are already created."
|
2
|
+
task :benchmark_tire, :count, :fields do |t, args|
|
3
|
+
args.with_defaults(:count => 1000, :fields => 20)
|
4
|
+
count = args[:count].to_i
|
5
|
+
fields = args[:fields].to_i
|
6
|
+
|
7
|
+
Rake::Task["benchmark_setup"].invoke
|
8
|
+
|
9
|
+
puts "Benchmarking Tire: #{count} Logling(s) with #{fields} fields each.".upcase
|
10
|
+
puts "Generating loglings. This may take a minute..."
|
11
|
+
|
12
|
+
loglings = generate_loglings(count, fields)
|
13
|
+
|
14
|
+
puts "Done generating loglings. Proceeding to benchmark..."
|
15
|
+
|
16
|
+
Benchmark.bmbm do |bm|
|
17
|
+
bm.report('Creation of Logling Index') do
|
18
|
+
Changeling::Models::Logling.tire.create_elasticsearch_index
|
19
|
+
end
|
20
|
+
|
21
|
+
bm.report('Inserting Loglings into Index') do
|
22
|
+
loglings.each { |logling| logling.update_index }
|
23
|
+
end
|
24
|
+
|
25
|
+
bm.report('Deletion of Logling Index') do
|
26
|
+
Changeling::Models::Logling.tire.index.delete
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/changeling.gemspec
CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.version = Changeling::VERSION
|
17
17
|
|
18
18
|
# Dependencies
|
19
|
-
gem.add_dependency "tire", "~> 0.5.
|
19
|
+
gem.add_dependency "tire", "~> 0.5.3"
|
20
20
|
gem.add_dependency "activemodel"
|
21
21
|
|
22
22
|
# Development Dependencies
|
@@ -37,4 +37,6 @@ Gem::Specification.new do |gem|
|
|
37
37
|
gem.add_development_dependency "database_cleaner"
|
38
38
|
gem.add_development_dependency "sqlite3"
|
39
39
|
gem.add_development_dependency "rails"
|
40
|
+
gem.add_development_dependency "sidekiq"
|
41
|
+
gem.add_development_dependency "resque"
|
40
42
|
end
|
data/lib/changeling.rb
CHANGED
@@ -17,6 +17,16 @@ module Changeling
|
|
17
17
|
autoload :Search, 'changeling/support/search'
|
18
18
|
end
|
19
19
|
|
20
|
+
module Async
|
21
|
+
autoload :Trackling, 'changeling/async/trackling'
|
22
|
+
autoload :SidekiqWorker, 'changeling/async/sidekiq_worker'
|
23
|
+
autoload :ResqueWorker, 'changeling/async/resque_worker'
|
24
|
+
end
|
25
|
+
|
26
|
+
module Exceptions
|
27
|
+
autoload :AsyncGemRequired, 'changeling/exceptions/async_gem_required'
|
28
|
+
end
|
29
|
+
|
20
30
|
def self.blame_user
|
21
31
|
self.changeling_store[:blame_user]
|
22
32
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Changeling
|
2
|
+
module Async
|
3
|
+
class SidekiqWorker
|
4
|
+
# If Sidekiq is not installed, we'll throw an error when trying to access Changeling::Async::Trackling
|
5
|
+
begin
|
6
|
+
include Sidekiq::Worker
|
7
|
+
sidekiq_options :queue => :changeling
|
8
|
+
rescue
|
9
|
+
end
|
10
|
+
|
11
|
+
def perform(json)
|
12
|
+
Changeling::Models::Logling.create(JSON.parse(json))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Changeling
|
2
|
+
module Async
|
3
|
+
module Trackling
|
4
|
+
def self.included(base)
|
5
|
+
base.after_update :async_save_logling
|
6
|
+
end
|
7
|
+
|
8
|
+
def async_save_logling
|
9
|
+
unless defined?(Sidekiq) || defined?(Resque)
|
10
|
+
raise Changeling::Exceptions::AsyncGemRequired
|
11
|
+
end
|
12
|
+
|
13
|
+
if self.changes && !self.changes.empty?
|
14
|
+
logling = Changeling::Models::Logling.new(self)
|
15
|
+
|
16
|
+
if defined?(Sidekiq)
|
17
|
+
Changeling::Async::SidekiqWorker.perform_async(logling.to_indexed_json)
|
18
|
+
elsif defined?(Resque)
|
19
|
+
Resque.enqueue(Changeling::Async::ResqueWorker, logling.to_indexed_json)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Changeling
|
2
|
+
module Exceptions
|
3
|
+
class AsyncGemRequired < StandardError
|
4
|
+
def to_s
|
5
|
+
"Changeling's asynchronous features require either the Sidekiq (https://github.com/mperham/sidekiq) or Resque (https://github.com/defunkt/resque) background job processing gems. Please install either one and try again."
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
data/lib/changeling/probeling.rb
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
module Changeling
|
2
2
|
module Probeling
|
3
|
-
def
|
3
|
+
def all_loglings
|
4
4
|
Changeling::Models::Logling.records_for(self)
|
5
5
|
end
|
6
|
+
alias_method :all_history, :all_loglings
|
6
7
|
|
7
|
-
def
|
8
|
+
def loglings(records = 10)
|
8
9
|
Changeling::Models::Logling.records_for(self, records.to_i)
|
9
10
|
end
|
11
|
+
alias_method :history, :loglings
|
10
12
|
|
11
|
-
def
|
13
|
+
def loglings_for_field(field_name, records = nil)
|
12
14
|
Changeling::Models::Logling.records_for(self, records ? records.to_i : nil, field_name.to_s)
|
13
15
|
end
|
16
|
+
alias_method :history_for_field, :loglings_for_field
|
14
17
|
end
|
15
18
|
end
|
data/lib/changeling/trackling.rb
CHANGED
data/lib/changeling/version.rb
CHANGED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RailsApp, "Testing Blameling Integration" do
|
4
|
+
context "Controller without Blameling" do
|
5
|
+
controller(RailsApp::BlogPostsController) do
|
6
|
+
extend(RSpec::Rails::ControllerExampleGroup::BypassRescue)
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
# Request needs to be setup to avoid path setting error
|
11
|
+
@request = ActionController::TestRequest.new
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should not set current_user" do
|
15
|
+
Thread.new {
|
16
|
+
post :create
|
17
|
+
# Look in application.rb for the User class and it's id method.
|
18
|
+
BlogPost.last.all_loglings.last.modified_by.should == nil
|
19
|
+
}.join
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "Controller with Blameling" do
|
24
|
+
controller(RailsApp::BlamelingController) do
|
25
|
+
extend(RSpec::Rails::ControllerExampleGroup::BypassRescue)
|
26
|
+
end
|
27
|
+
|
28
|
+
before(:each) do
|
29
|
+
# Request needs to be setup to avoid path setting error
|
30
|
+
@request = ActionController::TestRequest.new
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should set current_user" do
|
34
|
+
Thread.new {
|
35
|
+
post :create
|
36
|
+
# Look in application.rb for the User class and it's id method.
|
37
|
+
BlogPost.last.all_loglings.last.modified_by.should == 33
|
38
|
+
}.join
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "Controller with undefined current_user method" do
|
43
|
+
controller(RailsApp::NoCurrentUserController) do
|
44
|
+
extend(RSpec::Rails::ControllerExampleGroup::BypassRescue)
|
45
|
+
end
|
46
|
+
|
47
|
+
before(:each) do
|
48
|
+
# Request needs to be setup to avoid path setting error
|
49
|
+
@request = ActionController::TestRequest.new
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should not set current_user, nor should it error out" do
|
53
|
+
Thread.new {
|
54
|
+
post :create
|
55
|
+
# Look in application.rb for the User class and it's id method.
|
56
|
+
BlogPost.last.all_loglings.last.modified_by.should == nil
|
57
|
+
}.join
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "Controller with a different overridden 'changeling_blame_user' method" do
|
62
|
+
controller(RailsApp::CurrentAccountController) do
|
63
|
+
extend(RSpec::Rails::ControllerExampleGroup::BypassRescue)
|
64
|
+
end
|
65
|
+
|
66
|
+
before(:each) do
|
67
|
+
# Request needs to be setup to avoid path setting error
|
68
|
+
@request = ActionController::TestRequest.new
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should not set current_user if current_user is not defined" do
|
72
|
+
Thread.new {
|
73
|
+
post :create
|
74
|
+
# Look in application.rb for the User class and it's id method.
|
75
|
+
BlogPost.last.all_loglings.last.modified_by.should == 88
|
76
|
+
}.join
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|