HasRemote 0.1.4 → 0.1.5
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/HasRemote.gemspec +2 -2
- data/README.rdoc +5 -2
- data/VERSION +1 -1
- data/generators/has_remote_migration/templates/create_has_remote_synchronizations.erb +2 -1
- data/lib/has_remote/synchronizable.rb +25 -26
- data/lib/has_remote/synchronization.rb +6 -10
- data/lib/has_remote/tasks.rb +1 -1
- data/spec/caching_spec.rb +68 -64
- data/spec/schema.rb +5 -4
- data/spec/synchronization_spec.rb +4 -3
- metadata +2 -2
data/HasRemote.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{HasRemote}
|
8
|
-
s.version = "0.1.
|
8
|
+
s.version = "0.1.5"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Sjoerd Andringa"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-02-18}
|
13
13
|
s.description = %q{Bind a remote ActiveResource object to your local ActiveRecord objects, delegate attributes and optionally cache remote attributes locally.}
|
14
14
|
s.email = %q{sjoerd.andringa@innovationfactory.eu}
|
15
15
|
s.extra_rdoc_files = [
|
data/README.rdoc
CHANGED
@@ -106,7 +106,7 @@ Synchronize all records of one specific model:
|
|
106
106
|
User.synchronize!
|
107
107
|
|
108
108
|
The latter automatically requests all remote resources that have been changed (including new and deleted records) since the last successful synchronization for this particular model.
|
109
|
-
You may need to override the <tt>
|
109
|
+
You may need to override the <tt>updated_remotes</tt> class method in your model to match your host's REST API.
|
110
110
|
|
111
111
|
See <tt>HasRemote::Synchronizable</tt> for more information.
|
112
112
|
|
@@ -124,6 +124,9 @@ To specify additional parameters to send with the request that fetches updated r
|
|
124
124
|
|
125
125
|
rake hr:sync PARAMS="since=01-01-2010&limit=25"
|
126
126
|
|
127
|
+
(If you've overridden the <tt>updated_remotes</tt> class method on one of your synchronizable models, then note that these parameters are
|
128
|
+
passed in as a hash to <tt>updated_remotes</tt> internally.)
|
129
|
+
|
127
130
|
=== Documentation
|
128
131
|
|
129
132
|
To generate RDocs for this plugin, from the has_remote directory run:
|
@@ -146,4 +149,4 @@ To run the specs of the plugin, from the has_remote directory run:
|
|
146
149
|
Questions, requests and patches can be directed to sjoerd.andringa[AT]innovationfactory[DOT]nl.
|
147
150
|
|
148
151
|
|
149
|
-
Copyright (c) 2009 Innovation Factory.
|
152
|
+
Copyright (c) 2009-2010 Innovation Factory.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.5
|
@@ -2,7 +2,8 @@ class CreateHasRemoteSynchronizations < ActiveRecord::Migration
|
|
2
2
|
def self.up
|
3
3
|
create_table :has_remote_synchronizations do |t|
|
4
4
|
t.string :model_name, :null => false
|
5
|
-
t.datetime :
|
5
|
+
t.datetime :last_record_updated_at, :null => false
|
6
|
+
t.integer :last_record_id, :null => false
|
6
7
|
t.datetime :created_at
|
7
8
|
end
|
8
9
|
end
|
@@ -36,25 +36,25 @@ module HasRemote
|
|
36
36
|
# time is given. This may include new and optionally deleted (tagged by a 'deleted_at' attribute) resources.
|
37
37
|
#
|
38
38
|
# This is used by the <tt>synchronize!</tt> class method. By default
|
39
|
-
# it queries '/updated?since=<time>' on your resources URL, where 'time' is
|
40
|
-
# the
|
39
|
+
# it queries '/updated?since=<time>&last_record_id=<id>' on your resources URL, where 'time' is
|
40
|
+
# the updated_at value of the last processed remote object and 'last_record_id' is its id.
|
41
|
+
#
|
42
|
+
# *Options*
|
43
|
+
#
|
44
|
+
# The options passed in to <tt>synchronize!</tt> will be passed through to <tt>updated_remotes</tt>.
|
45
|
+
# By default they are used as request parameters for remotely requesting the updated records.
|
41
46
|
#
|
42
47
|
# You may need to override this method in your model to match your host's REST API or to change
|
43
48
|
# the default time, e.g.:
|
44
49
|
#
|
45
|
-
# def self.
|
46
|
-
# time
|
47
|
-
# User::Remote.find :all, :from => :search, :params => {:updated_since => time.
|
50
|
+
# def self.updated_remotes(options = nil)
|
51
|
+
# time = last_synchronization.try(:last_record_updated_at) || 12.hours.ago
|
52
|
+
# User::Remote.find :all, :from => :search, :params => { :updated_since => time.to_s(:db) }
|
48
53
|
# end
|
49
54
|
#
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
# [<tt>params</tt>:] Optional hash of additional parameters to send with the request (e.g. {:limit => 5})
|
54
|
-
#
|
55
|
-
def changed_remotes_since(time = nil, params = {})
|
56
|
-
time ||= 1.week.ago
|
57
|
-
remote_class.find :all, :from => :updated, :params => {:since => time.to_s}.merge(params)
|
55
|
+
def updated_remotes( options = {} )
|
56
|
+
time = last_synchronization.try(:last_record_updated_at) || 1.week.ago
|
57
|
+
remote_class.find :all, :from => :updated, :params => { :since => time.to_s, :last_record_id => last_synchronization.try(:last_record_id) }.merge( options )
|
58
58
|
end
|
59
59
|
|
60
60
|
# Will update all records that have been created, updated or deleted on the remote host
|
@@ -62,15 +62,14 @@ module HasRemote
|
|
62
62
|
#
|
63
63
|
# *Options*
|
64
64
|
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
# All other options are passed in as parameters to <tt>changes_remotes_since</tt>.
|
65
|
+
# Options are passed through to <tt>updated_remotes</tt> which is called in order to fetch the updated records.
|
66
|
+
# Use this if you want to override its default options.
|
68
67
|
#
|
69
68
|
def synchronize!(options = {})
|
70
69
|
logger.info( "*** Start synchronizing #{table_name} at #{Time.now.to_s :long} ***\n" )
|
71
70
|
@sync_count = 0
|
72
71
|
begin
|
73
|
-
changed_objects =
|
72
|
+
changed_objects = updated_remotes( options )
|
74
73
|
if changed_objects.any?
|
75
74
|
# Do everything within transaction to prevent ending up in half-synchronized situation if an exception is raised.
|
76
75
|
transaction { sync_all_records_for(changed_objects) }
|
@@ -80,25 +79,25 @@ module HasRemote
|
|
80
79
|
rescue => e
|
81
80
|
logger.warn( " - Synchronization of #{table_name} failed: #{e} \n #{e.backtrace}" )
|
82
81
|
else
|
83
|
-
|
82
|
+
if changed_objects.any?
|
83
|
+
last_record_updated_at = time_updated(changed_objects.last)
|
84
|
+
last_record_id = changed_objects.last.send(remote_primary_key)
|
85
|
+
HasRemote::Synchronization.create!(:model_name => self.name, :last_record_updated_at => last_record_updated_at, :last_record_id => last_record_id)
|
86
|
+
end
|
84
87
|
logger.info( " - Synchronized #{@sync_count} #{table_name}.\n" ) if @sync_count > 0
|
85
88
|
ensure
|
86
89
|
logger.info( "*** Stopped synchronizing #{table_name} at #{Time.now.to_s :long} ***\n" )
|
87
90
|
end
|
88
91
|
end
|
89
92
|
|
90
|
-
#
|
93
|
+
# The last successful synchronization for this model.
|
91
94
|
#
|
92
|
-
def
|
93
|
-
HasRemote::Synchronization.for(self.name).
|
95
|
+
def last_synchronization
|
96
|
+
HasRemote::Synchronization.for(self.name).last
|
94
97
|
end
|
95
98
|
|
96
99
|
private
|
97
100
|
|
98
|
-
def synchronized_at=(time) #:nodoc:
|
99
|
-
HasRemote::Synchronization.create!(:model_name => self.name, :latest_change => time)
|
100
|
-
end
|
101
|
-
|
102
101
|
def sync_all_records_for(resources) #:nodoc:
|
103
102
|
resources.each { |resource| sync_all_records_for_resource(resource) }
|
104
103
|
end
|
@@ -143,7 +142,7 @@ module HasRemote
|
|
143
142
|
update_and_save_record_for_resource(new(remote_foreign_key => resource.send(remote_primary_key)), resource)
|
144
143
|
end
|
145
144
|
|
146
|
-
def
|
145
|
+
def time_updated(resource)
|
147
146
|
(resource.respond_to?(:deleted_at) && resource.deleted_at) ? resource.deleted_at : resource.updated_at
|
148
147
|
end
|
149
148
|
|
@@ -1,15 +1,11 @@
|
|
1
1
|
module HasRemote
|
2
|
-
|
2
|
+
|
3
3
|
class Synchronization < ActiveRecord::Base #:nodoc:
|
4
4
|
set_table_name 'has_remote_synchronizations'
|
5
|
-
|
6
|
-
named_scope :for, lambda { |model_name| {:conditions => ["model_name = ?", model_name.to_s.classify] } }
|
7
|
-
|
8
|
-
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
validates_presence_of :model_name, :latest_change
|
5
|
+
|
6
|
+
named_scope :for, lambda { |model_name| {:conditions => ["model_name = ?", model_name.to_s.classify] } }
|
7
|
+
|
8
|
+
validates_presence_of :model_name, :last_record_updated_at, :last_record_id
|
13
9
|
end
|
14
|
-
|
10
|
+
|
15
11
|
end
|
data/lib/has_remote/tasks.rb
CHANGED
@@ -2,7 +2,7 @@ namespace :hr do
|
|
2
2
|
|
3
3
|
desc 'Synchronizes all attributes locally cached by has_remote'
|
4
4
|
task :sync => :environment do
|
5
|
-
models = ENV['MODELS']
|
5
|
+
models = ENV['MODELS'] ? extract_models : HasRemote.models
|
6
6
|
options = ENV['PARAMS'] ? Rack::Utils.parse_query(ENV['PARAMS']) : {}
|
7
7
|
models.each{|model| model.synchronize!(options)}
|
8
8
|
end
|
data/spec/caching_spec.rb
CHANGED
@@ -1,108 +1,112 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
2
|
|
3
3
|
context "Given existing remote resources" do
|
4
|
-
|
4
|
+
|
5
5
|
before(:each) do
|
6
6
|
User.delete_all
|
7
7
|
stub_resource 1, :email => "joeremote@foo.bar"
|
8
8
|
stub_resource 2, :email => "jane_remote@foo.bar"
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
describe "a user" do
|
12
|
-
|
12
|
+
|
13
13
|
it "should return cached attributes" do
|
14
14
|
User.should respond_to(:cached_attributes)
|
15
15
|
User.cached_attributes.should include(:email)
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
it "should return changed remotes since yesterday" do
|
19
19
|
user_1, user_2 = mock(:user), mock(:user)
|
20
20
|
time = 1.day.ago
|
21
|
-
User::Remote.should_receive(:find).with(:all,{:from => :updated, :params=>{:since=>time.to_s}}).once.and_return([user_1, user_2])
|
22
|
-
|
23
|
-
User.should respond_to(:
|
24
|
-
User.
|
21
|
+
User::Remote.should_receive(:find).with(:all,{:from => :updated, :params=>{:since=>time.to_s, :last_record_id=>nil}}).once.and_return([user_1, user_2])
|
22
|
+
|
23
|
+
User.should respond_to(:updated_remotes)
|
24
|
+
User.updated_remotes(:since => time.to_s).should include(user_1, user_2)
|
25
25
|
end
|
26
|
-
|
27
|
-
it "should find last synchronization
|
26
|
+
|
27
|
+
it "should find last synchronization" do
|
28
28
|
times = []
|
29
|
-
|
30
|
-
|
31
|
-
HasRemote::Synchronization.create(:model_name => 'HasRemoteSpec::User', :latest_change => times.last)
|
29
|
+
3.downto(1) do |i|
|
30
|
+
HasRemote::Synchronization.create(:model_name => 'HasRemoteSpec::User', :last_record_updated_at => i.days.ago, :last_record_id => i)
|
32
31
|
end
|
33
|
-
User.should respond_to(:
|
34
|
-
|
32
|
+
User.should respond_to(:last_synchronization)
|
33
|
+
sync = User.last_synchronization
|
34
|
+
sync.last_record_updated_at.to_s.should == 1.day.ago.to_s
|
35
|
+
sync.last_record_id.should == 1
|
35
36
|
end
|
36
|
-
|
37
|
+
|
37
38
|
it "should not delegate cached remote attributes" do
|
38
39
|
user = User.create! :remote_id => 1
|
39
40
|
User::Remote.should_not_receive(:find)
|
40
41
|
user.email.should == "joeremote@foo.bar"
|
41
42
|
end
|
42
|
-
|
43
|
+
|
43
44
|
it "should update its cached remote attributes on save" do
|
44
45
|
user = User.create! :remote_id => 1
|
45
46
|
user[:email].should == "joeremote@foo.bar"
|
46
47
|
user.update_attributes(:remote_id => 2)
|
47
|
-
user[:email].should == "jane_remote@foo.bar"
|
48
|
+
user[:email].should == "jane_remote@foo.bar"
|
48
49
|
end
|
49
50
|
|
50
51
|
it "should not update its cached remote attributes if skip_update_cache is true" do
|
51
52
|
user = User.create! :remote_id => 1, :skip_update_cache => true
|
52
53
|
user[:email].should == nil
|
53
54
|
end
|
54
|
-
|
55
|
+
|
55
56
|
end
|
56
57
|
|
57
58
|
describe "synchronization" do
|
58
|
-
|
59
|
+
|
59
60
|
describe "for the User model" do
|
60
|
-
|
61
|
+
|
61
62
|
describe "with updated and deleted remotes" do
|
62
|
-
|
63
|
+
|
63
64
|
before(:each) do
|
64
|
-
@user_1, @user_2, @user_3 = User.create!(:remote_id => 1), User.create!(:remote_id => 2), User.create!(:remote_id => 3)
|
65
|
-
|
66
65
|
@yesterday = DateTime.parse 1.day.ago.to_s
|
67
|
-
|
66
|
+
@last_id = 5
|
67
|
+
@user_1, @user_2, @user_3 = User.create!(:remote_id => @last_id), User.create!(:remote_id => 1), User.create!(:remote_id => 2)
|
68
|
+
|
68
69
|
resources = [
|
69
|
-
mock(:user, :id => 1, :email => "
|
70
|
-
mock(:user, :id => 2, :email => "
|
71
|
-
mock(:user, :id => 3, :email => "deleted@foo.bar", :updated_at => 2.days.ago, :deleted_at => 2.days.ago),
|
70
|
+
mock(:user, :id => 1, :email => "altered@foo.bar", :updated_at => 2.days.ago, :deleted_at => nil),
|
71
|
+
mock(:user, :id => 2, :email => "deleted@foo.bar", :updated_at => 2.days.ago, :deleted_at => 2.days.ago),
|
72
|
+
mock(:user, :id => 3, :email => "new-deleted@foo.bar", :updated_at => 2.days.ago, :deleted_at => 2.days.ago),
|
72
73
|
mock(:user, :id => 4, :email => "new@foo.bar", :updated_at => @yesterday),
|
73
|
-
mock(:user, :id =>
|
74
|
+
mock(:user, :id => @last_id, :email => "changed@foo.bar", :updated_at => @yesterday)
|
74
75
|
]
|
75
|
-
User.stub!(:
|
76
|
-
|
76
|
+
User.stub!(:updated_remotes).and_return(resources)
|
77
|
+
|
77
78
|
lambda { User.synchronize! }.should change(HasRemote::Synchronization, :count).by(1)
|
78
79
|
end
|
79
|
-
|
80
|
-
it "should keep track of the last
|
81
|
-
HasRemote::Synchronization.for("HasRemoteSpec::User").
|
80
|
+
|
81
|
+
it "should keep track of the last synchronized record" do
|
82
|
+
sync = HasRemote::Synchronization.for("HasRemoteSpec::User").last
|
83
|
+
|
84
|
+
sync.last_record_updated_at.should == @yesterday
|
85
|
+
sync.last_record_id.should == @last_id
|
82
86
|
end
|
83
|
-
|
87
|
+
|
84
88
|
it "should update changed users" do
|
85
89
|
@user_1.reload[:email].should == "changed@foo.bar"
|
86
90
|
@user_2.reload[:email].should == "altered@foo.bar"
|
87
91
|
end
|
88
|
-
|
92
|
+
|
89
93
|
it "should destroy deleted users" do
|
90
94
|
User.exists?(@user_3).should be_false
|
91
95
|
end
|
92
|
-
|
96
|
+
|
93
97
|
it "should create added users" do
|
94
98
|
User.exists?(:remote_id => 4).should be_true
|
95
99
|
end
|
96
|
-
|
100
|
+
|
97
101
|
it "should not create deleted users" do
|
98
|
-
User.exists?(:remote_id =>
|
102
|
+
User.exists?(:remote_id => 3).should be_false
|
99
103
|
end
|
100
104
|
end
|
101
|
-
|
105
|
+
|
102
106
|
describe "that fails" do
|
103
|
-
|
107
|
+
|
104
108
|
before(:each) do
|
105
|
-
|
109
|
+
|
106
110
|
@failure = lambda {
|
107
111
|
user_1, user_2 = User.create!(:remote_id => 1), User.create!(:remote_id => 2)
|
108
112
|
|
@@ -112,64 +116,64 @@ context "Given existing remote resources" do
|
|
112
116
|
mock(:user, :id => 1, :email => "changed@foo.bar", :updated_at => yesterday),
|
113
117
|
mock(:user, :id => 2, :email => "altered@foo.bar", :updated_at => 2.days.ago)
|
114
118
|
]
|
115
|
-
|
116
|
-
User.stub!(:
|
117
|
-
|
119
|
+
|
120
|
+
User.stub!(:updated_remotes).and_return(resources)
|
121
|
+
|
118
122
|
resources.last.should_receive(:send).and_raise "All hell breaks loose" # Raise when attr is read from resource 2.
|
119
|
-
|
123
|
+
|
120
124
|
User.synchronize!
|
121
125
|
}
|
122
126
|
end
|
123
|
-
|
127
|
+
|
124
128
|
it "should do it silently" do
|
125
129
|
@failure.should_not raise_error
|
126
130
|
end
|
127
|
-
|
131
|
+
|
128
132
|
it "should not create a synchronization record" do
|
129
133
|
@failure.should_not change(HasRemote::Synchronization, :count)
|
130
134
|
end
|
131
|
-
|
135
|
+
|
132
136
|
end
|
133
|
-
|
137
|
+
|
134
138
|
end
|
135
|
-
|
139
|
+
|
136
140
|
describe "for a single user" do
|
137
|
-
|
141
|
+
|
138
142
|
it "should update the user" do
|
139
143
|
user = User.create! :remote_id => 1
|
140
144
|
user[:email].should == "joeremote@foo.bar"
|
141
|
-
|
145
|
+
|
142
146
|
stub_resource 1, :email => "changed@foo.bar"
|
143
|
-
|
147
|
+
|
144
148
|
user.update_cached_attributes!
|
145
149
|
user[:email].should == "changed@foo.bar"
|
146
150
|
end
|
147
|
-
|
151
|
+
|
148
152
|
end
|
149
|
-
|
153
|
+
|
150
154
|
end
|
151
|
-
|
155
|
+
|
152
156
|
describe "synchronizing new cheeses" do
|
153
157
|
before do
|
154
158
|
resources = [
|
155
159
|
mock(:cheese, :id => 1, :name => "Brie", :updated_at => Date.yesterday)
|
156
160
|
]
|
157
|
-
Cheese.stub!(:
|
161
|
+
Cheese.stub!(:updated_remotes).and_return(resources)
|
158
162
|
lambda{ Cheese.synchronize! }.should change(Cheese, :count).from(0).to(1)
|
159
163
|
end
|
160
|
-
|
164
|
+
|
161
165
|
after { Cheese.delete_all }
|
162
|
-
|
166
|
+
|
163
167
|
it "should populate the local 'maturity' attribute with its default database value" do
|
164
168
|
Cheese.first.maturity.should == 5
|
165
169
|
end
|
166
|
-
|
170
|
+
|
167
171
|
it "should populate the local 'smell' attribute with the value set inside of a before_validation callback" do
|
168
|
-
Cheese.first.smell.should == 5 * 10
|
172
|
+
Cheese.first.smell.should == 5 * 10
|
169
173
|
end
|
170
|
-
|
174
|
+
|
171
175
|
end
|
172
|
-
|
176
|
+
|
173
177
|
end
|
174
178
|
|
175
179
|
def stub_resource(id, attrs)
|
data/spec/schema.rb
CHANGED
@@ -4,22 +4,23 @@ ActiveRecord::Schema.define(:version => 0) do
|
|
4
4
|
create_table :books do |t|
|
5
5
|
t.integer :custom_remote_id
|
6
6
|
end
|
7
|
-
|
7
|
+
|
8
8
|
create_table :users do |t|
|
9
9
|
t.integer :remote_id
|
10
10
|
t.string :email
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
create_table :products do |t|
|
14
14
|
t.integer :remote_id
|
15
15
|
end
|
16
16
|
|
17
17
|
create_table :has_remote_synchronizations do |t|
|
18
18
|
t.string :model_name, :null => false
|
19
|
-
t.datetime :
|
19
|
+
t.datetime :last_record_updated_at, :null => false
|
20
|
+
t.integer :last_record_id, :null => false
|
20
21
|
t.datetime :created_at
|
21
22
|
end
|
22
|
-
|
23
|
+
|
23
24
|
create_table :cheeses do |t|
|
24
25
|
t.string :name
|
25
26
|
t.integer :maturity, :default => 5
|
@@ -1,11 +1,12 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
2
|
|
3
3
|
describe HasRemote::Synchronization do
|
4
|
-
|
4
|
+
|
5
5
|
subject { HasRemote::Synchronization.new }
|
6
|
-
|
6
|
+
|
7
7
|
it { should validate_presence_of(:model_name) }
|
8
|
-
it { should validate_presence_of(:
|
8
|
+
it { should validate_presence_of(:last_record_updated_at) }
|
9
|
+
it { should validate_presence_of(:last_record_id) }
|
9
10
|
it { should have_named_scope("for('User')") }
|
10
11
|
|
11
12
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: HasRemote
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sjoerd Andringa
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-
|
12
|
+
date: 2010-02-18 00:00:00 +01:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|