HasRemote 0.1.7 → 0.2.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.
@@ -0,0 +1,21 @@
1
+ require 'rails'
2
+
3
+ module HasRemote
4
+ # @private
5
+ class Railtie < Rails::Railtie #:nodoc:
6
+ initializer 'has_remote.load' do
7
+ ActiveSupport.on_load(:active_record) do
8
+ HasRemote::Railtie.load!
9
+ end
10
+ end
11
+
12
+ rake_tasks do
13
+ load 'tasks/has_remote.rake'
14
+ end
15
+
16
+ def self.load!
17
+ ActiveRecord::Base.send :include, HasRemote
18
+ ActiveSupport.run_load_hooks(:has_remote)
19
+ end
20
+ end
21
+ end
@@ -1,8 +1,8 @@
1
1
  module HasRemote
2
2
 
3
- # Contains class methods regarding synchronization of changed, added and deleted remotes.
3
+ # Contains class methods regarding synchronization of changed, added and deleted remote data.
4
4
  #
5
- # === Synchronization examples
5
+ # ==== Synchronization examples:
6
6
  #
7
7
  # Update all cached attributes, destroy deleted records and add new records for all models that have a remote:
8
8
  # HasRemote.synchronize!
@@ -17,12 +17,15 @@ module HasRemote
17
17
  # @user.update_cached_attributes!
18
18
  #
19
19
  # You can make your application call these methods whenever you need to be sure
20
- # your cache is up to date or use the 'hr:sync' rake task to synchronize all models
21
- # from the command line.
20
+ # your cache is up to date.
22
21
  #
23
- # *Note*
24
- # All remote resources need to have an 'updated_at' field in order for synchronization to work. Records will
25
- # be destroyed if their remote resource's 'deleted_at' time lies before the time of synchronization.
22
+ # You can also use a rake task to synchronize from the command line:
23
+ # rake hr:sync
24
+ #
25
+ # See the {file:README.rdoc README} for more rake options.
26
+ #
27
+ # @note All remote resources need to have an <tt>updated_at</tt> field in order for synchronization to work. Optionally, records will
28
+ # be destroyed if their remote has a <tt>deleted_at</tt> field and its time lies before the time of synchronization.
26
29
  #
27
30
  module Synchronizable
28
31
 
@@ -32,44 +35,40 @@ module HasRemote
32
35
  @cached_attributes ||= []
33
36
  end
34
37
 
35
- # Returns all remote objects that have been changed since the given time or one week ago if no
36
- # time is given. This may include new and optionally deleted (tagged by a 'deleted_at' attribute) resources.
37
- #
38
- # This is used by the <tt>synchronize!</tt> class method. By default
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*
38
+ # Returns all remote objects that have been changed since the last synchornization _(or since ever if no
39
+ # time is given)_. This may include new and optionally deleted (tagged by a <tt>deleted_at</tt> attribute) resources.
43
40
  #
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
+ # This method is called from the {#synchronize!} class method. By default
42
+ # it queries <tt>/updated?since=[time]&last_record_id=[id]</tt> on your resources URL, where <tt>time</tt> is
43
+ # the updated_at value of the last processed remote object and <tt>last_record_id</tt> is its ID.
46
44
  #
47
- # You may need to override this method in your model to match your host's REST API or to change
48
- # the default time, e.g.:
45
+ # *Important:* If you are using synchronization you'll probably need to override this method in your model to match
46
+ # your host's REST API; here's an example of another implementation:
49
47
  #
50
48
  # def self.updated_remotes(options = nil)
51
49
  # time = last_synchronization.try(:last_record_updated_at) || 12.hours.ago
52
50
  # User::Remote.find :all, :from => :search, :params => { :updated_since => time.to_s(:db) }
53
51
  # end
54
52
  #
55
- def updated_remotes( options = {} )
53
+ # @param [Hash] parameters The parameter options passed in to {#synchronize!} will be passed through to this method.
54
+ # They are used as request parameters for remotely requesting the updated records.
55
+ #
56
+ def updated_remotes( parameters = {} )
56
57
  time = last_synchronization.try(:last_record_updated_at) || DateTime.parse("Jan 1 1970")
57
- remote_class.find :all, :from => :updated, :params => { :since => time.to_s, :last_record_id => last_synchronization.try(:last_record_id) }.merge( options )
58
+ remote_class.find :all, :from => :updated, :params => { :since => time.to_s, :last_record_id => last_synchronization.try(:last_record_id) }.merge( parameters )
58
59
  end
59
60
 
60
61
  # Will update all records that have been created, updated or deleted on the remote host
61
62
  # since the last successful synchronization.
62
63
  #
63
- # *Options*
64
- #
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.
64
+ # @param [Hash] parameters Parameter options are passed through to {#updated_remotes}. There they are used
65
+ # as request parameters for remotely requesting the updated records.
67
66
  #
68
- def synchronize!(options = {})
67
+ def synchronize!(parameters = {})
69
68
  logger.info( "*** Start synchronizing #{table_name} at #{Time.now.to_s :long} ***\n" )
70
69
  @sync_count = 0
71
70
  begin
72
- changed_objects = updated_remotes( options )
71
+ changed_objects = updated_remotes( parameters )
73
72
  if changed_objects.any?
74
73
  # Do everything within transaction to prevent ending up in half-synchronized situation if an exception is raised.
75
74
  transaction { sync_all_records_for(changed_objects) }
@@ -90,7 +89,9 @@ module HasRemote
90
89
  end
91
90
  end
92
91
 
93
- # The last successful synchronization for this model.
92
+ # Returns the record for last successful synchronization for this model.
93
+ #
94
+ # @return [Synchronization]
94
95
  #
95
96
  def last_synchronization
96
97
  HasRemote::Synchronization.for(self.name).last
@@ -142,11 +143,11 @@ module HasRemote
142
143
  update_and_save_record_for_resource(new(remote_foreign_key => resource.send(remote_primary_key)), resource)
143
144
  end
144
145
 
145
- def time_updated(resource)
146
+ def time_updated(resource) #:nodoc:
146
147
  (resource.respond_to?(:deleted_at) && resource.deleted_at) ? resource.deleted_at : resource.updated_at
147
148
  end
148
149
 
149
- def deleted?(resource)
150
+ def deleted?(resource) #:nodoc:
150
151
  resource.respond_to?(:deleted_at) && resource.deleted_at && resource.deleted_at <= Time.now
151
152
  end
152
153
 
@@ -1,9 +1,18 @@
1
1
  module HasRemote
2
2
 
3
+ # This model represents a synchronization performed by HasRemote.
4
+ #
5
+ # ==== Attributes:
6
+ #
7
+ # [model_name] (String) Name of the model that was synchronized.
8
+ # [last_record_updated_at] (Time) Timestamp representing the <tt>updated_at</tt> of the last record that was synchronized during this synchronization.
9
+ # [last_record_id] (Integer) ID of the last record that was synchronized during this synchronization.
10
+ # [created_at] (Time) time when this synchronization finished.
11
+ #
3
12
  class Synchronization < ActiveRecord::Base #:nodoc:
4
13
  set_table_name 'has_remote_synchronizations'
5
14
 
6
- named_scope :for, lambda { |model_name| {:conditions => ["model_name = ?", model_name.to_s.classify] } }
15
+ scope :for, lambda { |model_name| where(:model_name => model_name.to_s.classify) }
7
16
 
8
17
  validates_presence_of :model_name, :last_record_updated_at, :last_record_id
9
18
  end
File without changes
@@ -1,21 +1,23 @@
1
- module HasRemoteMacros
1
+ module HasRemote
2
+ module ShouldaMacros
2
3
 
3
- def should_have_remote(&block)
4
- klass = model_class
5
- should "have remote resources" do
6
- assert HasRemote.models.include?(klass)
4
+ def should_have_remote
5
+ klass = model_class
6
+ should "have remote resources" do
7
+ assert HasRemote.models.include?(klass)
8
+ end
7
9
  end
8
- end
9
10
 
10
- def should_have_remote_attribute(attr_name, options = {})
11
- klass = model_class
12
- should "have remote attribute #{attr_name}" do
13
- assert klass.remote_attributes.include?(attr_name)
14
- assert klass.cached_attributes.include?(attr_name) if options[:local_cache]
15
- assert klass.new.respond_to?(options[:as] || attr_name)
11
+ def should_have_remote_attribute(attr_name, options = {})
12
+ klass = model_class
13
+ should "have remote attribute #{attr_name}" do
14
+ assert klass.remote_attributes.include?(attr_name)
15
+ assert klass.cached_attributes.include?(attr_name) if options[:local_cache]
16
+ assert klass.new.respond_to?(options[:as] || attr_name)
17
+ end
16
18
  end
17
- end
18
19
 
20
+ end
19
21
  end
20
22
 
21
- Test::Unit::TestCase.extend HasRemoteMacros
23
+ Test::Unit::TestCase.extend HasRemote::ShouldaMacros
data/spec/caching_spec.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper.rb'
2
2
 
3
- context "Given existing remote resources" do
3
+ describe "Given existing remote resources" do
4
4
 
5
5
  before(:each) do
6
6
  User.delete_all
@@ -27,7 +27,7 @@ context "Given existing remote resources" do
27
27
  it "should find last synchronization" do
28
28
  times = []
29
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)
30
+ HasRemote::Synchronization.create!(:model_name => 'User', :last_record_updated_at => i.days.ago, :last_record_id => i)
31
31
  end
32
32
  User.should respond_to(:last_synchronization)
33
33
  sync = User.last_synchronization
@@ -62,16 +62,14 @@ context "Given existing remote resources" do
62
62
  describe "with updated and deleted remotes" do
63
63
 
64
64
  before(:each) do
65
- @yesterday = DateTime.parse 1.day.ago.to_s
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)
65
+ @yesterday = 1.day.ago
66
+ @user_1, @user_2 = User.create!(:remote_id => 1), User.create!(:remote_id => 2)
68
67
 
69
68
  resources = [
70
69
  mock(:user, :id => 1, :email => "altered@foo.bar", :updated_at => 2.days.ago, :deleted_at => nil),
71
70
  mock(:user, :id => 2, :email => "deleted@foo.bar", :updated_at => 2.days.ago, :deleted_at => 2.days.ago),
72
71
  mock(:user, :id => 3, :email => "new-deleted@foo.bar", :updated_at => 2.days.ago, :deleted_at => 2.days.ago),
73
72
  mock(:user, :id => 4, :email => "new@foo.bar", :updated_at => @yesterday),
74
- mock(:user, :id => @last_id, :email => "changed@foo.bar", :updated_at => @yesterday)
75
73
  ]
76
74
  User.stub!(:updated_remotes).and_return(resources)
77
75
 
@@ -79,19 +77,18 @@ context "Given existing remote resources" do
79
77
  end
80
78
 
81
79
  it "should keep track of the last synchronized record" do
82
- sync = HasRemote::Synchronization.for("HasRemoteSpec::User").last
80
+ sync = HasRemote::Synchronization.for("User").last
83
81
 
84
82
  sync.last_record_updated_at.should == @yesterday
85
- sync.last_record_id.should == @last_id
83
+ sync.last_record_id.should == 4
86
84
  end
87
85
 
88
86
  it "should update changed users" do
89
- @user_1.reload[:email].should == "changed@foo.bar"
90
- @user_2.reload[:email].should == "altered@foo.bar"
87
+ @user_1.reload[:email].should == "altered@foo.bar"
91
88
  end
92
89
 
93
90
  it "should destroy deleted users" do
94
- User.exists?(@user_3).should be_false
91
+ User.exists?(@user_2).should be_false
95
92
  end
96
93
 
97
94
  it "should create added users" do
@@ -1,27 +1,27 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper.rb'
2
2
 
3
3
  describe HasRemote do
4
-
4
+
5
5
  it "should know about registered models" do
6
6
  HasRemote.should respond_to(:models)
7
7
  HasRemote.models.should include(User, Book)
8
8
  end
9
-
9
+
10
10
  end
11
11
 
12
12
  describe User do
13
-
13
+
14
14
  it "should respond to 'remote_class' and 'remote_foreign_key'" do
15
15
  User.should respond_to(:remote_class, :remote_foreign_key)
16
16
  User.remote_class.should == User::Remote
17
17
  User.remote_foreign_key.should == :remote_id
18
18
  end
19
-
19
+
20
20
  it "should set remote class' configuration" do
21
21
  User.remote_class.site.should_not be_nil
22
22
  User.remote_class.element_name.should == "user"
23
23
  end
24
-
24
+
25
25
  it "should return remote attributes" do
26
26
  User.remote_attributes.should include(:name, :email)
27
27
  end
@@ -54,7 +54,7 @@ describe User do
54
54
  end
55
55
 
56
56
  context "without a remote" do
57
-
57
+
58
58
  before(:each) do
59
59
  @user.remote_id = nil
60
60
  end
@@ -68,41 +68,41 @@ describe User do
68
68
  end
69
69
 
70
70
  describe Book do
71
-
71
+
72
72
  it "should use a custom remote key" do
73
73
  Book.remote_foreign_key.should == :custom_remote_id
74
74
  end
75
-
75
+
76
76
  describe "instances" do
77
-
77
+
78
78
  it "should have a custom remote" do
79
79
  @book = Book.new
80
80
  @book.custom_remote_id = 1
81
81
 
82
- HasRemoteSpec::RemoteBook.should_receive(:find).twice.with(1).and_return( mock(:resource, :title => "Ruby for Rails") )
82
+ RemoteBook.should_receive(:find).twice.with(1).and_return( mock(:resource, :title => "Ruby for Rails") )
83
83
 
84
84
  @book.should respond_to(:remote)
85
85
  @book.remote.should respond_to(:title)
86
86
  @book.has_remote?.should be_true
87
- end
87
+ end
88
88
  end
89
89
  end
90
90
 
91
91
  describe Product do
92
-
92
+
93
93
  it "should have a custom finder" do
94
94
  Product.should respond_to(:remote_finder)
95
95
  Product.remote_finder.should be_a(Proc)
96
96
  end
97
-
97
+
98
98
  describe "instances" do
99
-
99
+
100
100
  it "should use a custom finder" do
101
101
  @product = Product.new :remote_id => 1
102
102
  Product::Remote.should_receive(:find).once.with(:one, :from => "/special/place/products/1.xml").and_return("resource")
103
-
103
+
104
104
  @product.remote.should == "resource"
105
105
  end
106
106
  end
107
-
107
+
108
108
  end
data/spec/spec_helper.rb CHANGED
@@ -1,38 +1,23 @@
1
- # Fake Rails constants
2
- RAILS_ENV = ENV["RAILS_ENV"] ||= 'test'
3
- RAILS_ROOT = File.dirname(__FILE__)
4
-
5
- # Include required libraries
6
1
  require 'rubygems'
7
- require 'active_record'
8
- require 'active_resource'
9
- require 'shoulda/active_record/matchers'
10
- include Shoulda::ActiveRecord::Matchers
11
-
12
- # Include plugin's files
13
- require File.dirname(__FILE__) + '/../lib/has_remote'
14
- require File.dirname(__FILE__) + '/../lib/has_remote/synchronizable'
15
- require File.dirname(__FILE__) + '/../lib/has_remote/synchronization'
2
+ require 'bundler/setup'
16
3
 
17
4
  # Initialize plugin
18
- require "#{File.dirname(__FILE__)}/../init"
5
+ require "rails"
6
+ Rails.env = ENV["RAILS_ENV"] ||= 'test'
7
+ require "shoulda-matchers"
8
+ require "active_record"
9
+ require "active_resource"
10
+ require "has_remote"
11
+ require "has_remote/railtie"
12
+ HasRemote::Railtie.load!
19
13
 
20
14
  # Create logger
21
- ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
15
+ ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.dirname(__FILE__) + "/debug.log")
22
16
 
23
17
  # Setup database connection and structure
24
18
  config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
25
- ActiveRecord::Base.establish_connection(config[ENV['RAILS_ENV']])
19
+ ActiveRecord::Base.establish_connection(config[Rails.env])
26
20
  load(File.dirname(__FILE__) + "/schema.rb")
27
21
 
28
- # Require models
29
- require File.dirname(__FILE__) + '/has_remote_spec/user'
30
- require File.dirname(__FILE__) + '/has_remote_spec/book'
31
- require File.dirname(__FILE__) + '/has_remote_spec/product'
32
- require File.dirname(__FILE__) + '/has_remote_spec/cheese'
33
-
34
- # Create schortcuts
35
- User = HasRemoteSpec::User
36
- Book = HasRemoteSpec::Book
37
- Product = HasRemoteSpec::Product
38
- Cheese = HasRemoteSpec::Cheese
22
+ # Require test models
23
+ Dir[File.join(File.dirname(__FILE__), "support", "**/*.rb")].each { |file| require file }
@@ -0,0 +1,5 @@
1
+ class RemoteBook < ActiveResource::Base; end
2
+
3
+ class Book < ActiveRecord::Base
4
+ has_remote :through => "RemoteBook", :foreign_key => :custom_remote_id
5
+ end
@@ -0,0 +1,11 @@
1
+ class Cheese < ActiveRecord::Base
2
+ validates_presence_of :maturity, :smell
3
+ has_remote :site => "http://dummy.local"
4
+ before_validation :set_smell, :on => :create
5
+
6
+ protected
7
+
8
+ def set_smell
9
+ self.smell = self.maturity * 10 if self.smell.nil?
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ class Product < ActiveRecord::Base
2
+ has_remote :site => "http://dummy.local" do |remote|
3
+ remote.finder do |id|
4
+ Product::Remote.find :one, :from => "/special/place/products/#{id}.xml"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ class User < ActiveRecord::Base
2
+ has_remote :site => "http://dummy.local" do |remote|
3
+ remote.attribute :name
4
+ remote.attribute :email, :local_cache => true
5
+ remote.attribute :phone, :as => :telephone
6
+ end
7
+
8
+ before_save :update_cached_attributes
9
+ end
@@ -10,10 +10,11 @@ describe HasRemote::Synchronization do
10
10
 
11
11
  describe "named scope 'for'" do
12
12
  before do
13
+ HasRemote::Synchronization.delete_all
13
14
  @user_synchronization = HasRemote::Synchronization.create! :model_name => 'User', :last_record_updated_at => 1.day.ago, :last_record_id => 1
14
15
  @book_synchronization = HasRemote::Synchronization.create! :model_name => 'Book', :last_record_updated_at => 1.day.ago, :last_record_id => 2
15
16
  end
16
-
17
+
17
18
  it "should return synchronization records scoped by model_name" do
18
19
  HasRemote::Synchronization.for('User').should == [@user_synchronization]
19
20
  end