HasRemote 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/.gitignore +3 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +135 -0
- data/Rakefile +35 -0
- data/TODO +21 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/lib/generators/has_remote_migration/has_remote_migration_generator.rb +7 -0
- data/lib/generators/has_remote_migration/templates/create_has_remote_synchronizations.erb +13 -0
- data/lib/has_remote/synchronizable.rb +144 -0
- data/lib/has_remote/synchronization.rb +15 -0
- data/lib/has_remote.rb +234 -0
- data/lib/tasks/has_remote.rake +12 -0
- data/shoulda_macros/has_remote_macros.rb +21 -0
- data/spec/caching_spec.rb +179 -0
- data/spec/database.yml +3 -0
- data/spec/has_remote_spec/book.rb +7 -0
- data/spec/has_remote_spec/cheese.rb +10 -0
- data/spec/has_remote_spec/product.rb +9 -0
- data/spec/has_remote_spec/user.rb +11 -0
- data/spec/has_remote_spec.rb +108 -0
- data/spec/schema.rb +29 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/synchronization_spec.rb +11 -0
- metadata +87 -0
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009-2010 Innovation Factory, Amsterdam
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
= HasRemote
|
2
|
+
|
3
|
+
Gives your local ActiveRecord model an ActiveResource equivalent,
|
4
|
+
which enables you to look for certain attributes remotely using a RESTful webservice.
|
5
|
+
|
6
|
+
=== Installation
|
7
|
+
|
8
|
+
script/plugins install git://github.com/innovationfactory/has_remote.git
|
9
|
+
|
10
|
+
Only if you require synchronization:
|
11
|
+
|
12
|
+
script/generate has_remote_migration
|
13
|
+
|
14
|
+
rake db:migrate
|
15
|
+
|
16
|
+
=== Examples
|
17
|
+
|
18
|
+
First make sure your records have a reference to a remote resource:
|
19
|
+
add_column :users, :remote_id, :integer
|
20
|
+
|
21
|
+
The default key is 'remote_id', but this can be changed, see options for has_remote.
|
22
|
+
|
23
|
+
class User < ActiveRecord::Base
|
24
|
+
has_remote :site => 'http://people.local'
|
25
|
+
end
|
26
|
+
|
27
|
+
User.remote_class
|
28
|
+
# => User::Remote (subclass of ActiveResource::Base)
|
29
|
+
|
30
|
+
@user.remote
|
31
|
+
# => #<User::Remote:...>
|
32
|
+
|
33
|
+
@user.remote.username
|
34
|
+
# => "User name from remote server"
|
35
|
+
|
36
|
+
has_remote optionally takes a block which can be used to specify remote attributes:
|
37
|
+
|
38
|
+
class User < ActiveRecord::Base
|
39
|
+
has_remote :site => '...' do |remote|
|
40
|
+
remote.attribute :username
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
@user.username
|
45
|
+
# => "User name from remote server"
|
46
|
+
|
47
|
+
Note that the current version of HasRemote only offers read-only support for remote attributes.
|
48
|
+
|
49
|
+
The <tt>:through</tt> option enables you to specify your own ActiveResource class:
|
50
|
+
|
51
|
+
class RemoteUser < ActiveResource::Base
|
52
|
+
self.site = "people.local"
|
53
|
+
self.element_name = "person"
|
54
|
+
end
|
55
|
+
|
56
|
+
class User < ActiveRecord::Base
|
57
|
+
has_remote :through => "RemoteUser"
|
58
|
+
end
|
59
|
+
|
60
|
+
See documentation for has_remote for a description of all options.
|
61
|
+
|
62
|
+
=== Caching attributes locally
|
63
|
+
|
64
|
+
In case certain attributes are used a lot and performance is getting bad, or in case you need to do database operations on remote attributes, like sorting, you can tell has_remote
|
65
|
+
to locally cache specific attributes in the following manner:
|
66
|
+
|
67
|
+
class User < ActiveRecord::Base
|
68
|
+
has_remote :site => '...' do |remote|
|
69
|
+
remote.attribute :username, :local_cache => true
|
70
|
+
remote.attribute :email_address, :as => :email, :local_cache => true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
This assumes you also have a 'username' and 'email' column in the local 'users' table. Note that when using the
|
75
|
+
<tt>:as</tt> option the local column is assumed to be named after this value.
|
76
|
+
|
77
|
+
=== Synchronization of cached attributes
|
78
|
+
|
79
|
+
There are two ways of keeping the locally cached attributes in sync with their remote values.
|
80
|
+
|
81
|
+
1. Inline synchronization
|
82
|
+
2. Rake task <tt>hr:sync</tt>
|
83
|
+
|
84
|
+
==== Inline synchronization
|
85
|
+
|
86
|
+
Synchronize a single record's attributes:
|
87
|
+
@user.update_cached_attributes
|
88
|
+
|
89
|
+
*Tip!* It is often useful to trigger this method by means of a callback in order to initialize remote attributes when the record is created:
|
90
|
+
before_create :update_cached_attributes
|
91
|
+
|
92
|
+
Appending an exclamation mark will also save the record:
|
93
|
+
@user.update_cached_attributes!
|
94
|
+
|
95
|
+
Synchronize all records of one specific model:
|
96
|
+
User.synchronize!
|
97
|
+
|
98
|
+
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.
|
99
|
+
You may need to override the <tt>changed_remotes_since</tt> class method in your model to match your host's REST API.
|
100
|
+
|
101
|
+
See <tt>HasRemote::Synchronizable</tt> for more information.
|
102
|
+
|
103
|
+
==== Rake hr:sync
|
104
|
+
|
105
|
+
The rake task <tt>hr:sync</tt> is provided to allow easy synchronization from the command line.
|
106
|
+
You could set up a cron tab that runs this task regularly to keep the data in sync.
|
107
|
+
|
108
|
+
By default <tt>hr:sync</tt> updates all records of each model that has remotes. You can limit this to
|
109
|
+
certain models by using the <tt>MODELS</tt> variable:
|
110
|
+
|
111
|
+
rake hr:sync MODELS=Contact,Company
|
112
|
+
|
113
|
+
=== Documentation
|
114
|
+
|
115
|
+
To generate RDocs for this plugin, from the has_remote directory run:
|
116
|
+
rake rdoc
|
117
|
+
or from your application's root directory, run:
|
118
|
+
rake doc:plugins:has_remote
|
119
|
+
|
120
|
+
=== Testing
|
121
|
+
|
122
|
+
To run the specs of the plugin, from the has_remote directory run:
|
123
|
+
rake spec
|
124
|
+
|
125
|
+
(This requires you to have both RSpec and Shoulda installed.)
|
126
|
+
|
127
|
+
=== More information & patches
|
128
|
+
|
129
|
+
- Simple example of how HasRemote simplifies your code: http://gist.github.com/176335
|
130
|
+
- Simple API authentication with HasRemote: http://gist.github.com/174497
|
131
|
+
|
132
|
+
Questions, requests and patches can be directed to sjoerd.andringa[AT]innovationfactory[DOT]nl.
|
133
|
+
|
134
|
+
|
135
|
+
Copyright (c) 2009 Innovation Factory.
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
|
6
|
+
desc 'Generate documentation for the has_remote plugin.'
|
7
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
8
|
+
rdoc.rdoc_dir = 'doc'
|
9
|
+
rdoc.title = 'HasRemote'
|
10
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
11
|
+
rdoc.rdoc_files.include('README.rdoc')
|
12
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
13
|
+
rdoc.rdoc_files.exclude('lib/generators')
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Run all RSpec examples"
|
17
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
18
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
19
|
+
t.spec_opts = %w(-cfs)
|
20
|
+
end
|
21
|
+
|
22
|
+
begin
|
23
|
+
require 'jeweler'
|
24
|
+
Jeweler::Tasks.new do |gemspec|
|
25
|
+
gemspec.name = "HasRemote"
|
26
|
+
gemspec.summary = "Bind a remote ActiveResource object to your local ActiveRecord objects."
|
27
|
+
gemspec.description = "Bind a remote ActiveResource object to your local ActiveRecord objects, delegate attributes and optionally cache remote attributes locally."
|
28
|
+
gemspec.email = "sjoerd.andringa@innovationfactory.eu"
|
29
|
+
gemspec.homepage = "http://github.com/innovationfactory/has_remote"
|
30
|
+
gemspec.authors = ["Sjoerd Andringa"]
|
31
|
+
end
|
32
|
+
Jeweler::GemcutterTasks.new
|
33
|
+
rescue LoadError
|
34
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
35
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
= Todos and ideas
|
2
|
+
|
3
|
+
- Creating, updating and destroying resources both locally and remote transparently.
|
4
|
+
- Allow for multiple remotes, so that composing one object from different resources becomes a possibility.
|
5
|
+
- Make <tt>to_xml</tt> and <tt>to_json</tt> etc. optionally merge in remote attributes (<tt>to_xml(:remotes => :merge)</tt>).
|
6
|
+
|
7
|
+
<user>
|
8
|
+
<id type="integer">1</id>
|
9
|
+
<remote_id type="integer">1</remote_id>
|
10
|
+
<name>George Remote</name>
|
11
|
+
</user>
|
12
|
+
|
13
|
+
- Make <tt>to_xml</tt> etc. optionally include remotes, so consumers may talk to origins directly (<tt>to_xml(:remotes => :include)</tt>).
|
14
|
+
|
15
|
+
<user>
|
16
|
+
<id type="integer">1</id>
|
17
|
+
<remote site="...">
|
18
|
+
<id type="integer">1</id>
|
19
|
+
<name>George Remote</name>
|
20
|
+
</remote>
|
21
|
+
</user>
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ActiveRecord::Base.send(:include, HasRemote)
|
@@ -0,0 +1,7 @@
|
|
1
|
+
class HasRemoteMigrationGenerator < Rails::Generator::Base #:nodoc:
|
2
|
+
def manifest
|
3
|
+
record do |m|
|
4
|
+
m.migration_template "create_has_remote_synchronizations.erb", File.join("db", "migrate"), :migration_file_name => 'create_has_remote_synchronizations'
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateHasRemoteSynchronizations < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :has_remote_synchronizations do |t|
|
4
|
+
t.string :model_name, :null => false
|
5
|
+
t.datetime :latest_change, :null => false
|
6
|
+
t.datetime :created_at
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.down
|
11
|
+
drop_table :has_remote_synchronizations
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module HasRemote
|
2
|
+
|
3
|
+
# Contains class methods regarding synchronization of changed, added and deleted remotes.
|
4
|
+
#
|
5
|
+
# === Synchronization examples
|
6
|
+
#
|
7
|
+
# Update all cached attributes, destroy deleted records and add new records for all models that have a remote:
|
8
|
+
# HasRemote.synchronize!
|
9
|
+
#
|
10
|
+
# For users only:
|
11
|
+
# User.synchronize!
|
12
|
+
#
|
13
|
+
# You can also update a single record's cached attributes:
|
14
|
+
# @user.update_cached_attributes
|
15
|
+
#
|
16
|
+
# Or with also saving the record:
|
17
|
+
# @user.update_cached_attributes!
|
18
|
+
#
|
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.
|
22
|
+
#
|
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.
|
26
|
+
#
|
27
|
+
module Synchronizable
|
28
|
+
|
29
|
+
# Returns an array of all attributes that are locally cached.
|
30
|
+
#
|
31
|
+
def cached_attributes
|
32
|
+
@cached_attributes ||= []
|
33
|
+
end
|
34
|
+
|
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>' on your resources URL, where 'time' is
|
40
|
+
# the latest updated_at time of the last processed remote objects.
|
41
|
+
#
|
42
|
+
# You may need to override this method in your model to match your host's REST API or to change
|
43
|
+
# the default time, e.g.:
|
44
|
+
#
|
45
|
+
# def self.changed_remotes_since(time = nil)
|
46
|
+
# time ||= 12.hours.ago
|
47
|
+
# User::Remote.find :all, :from => :search, :params => {:updated_since => time.strftime('...') }
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
def changed_remotes_since(time = nil)
|
51
|
+
time ||= 1.week.ago
|
52
|
+
remote_class.find :all, :from => :updated, :params => {:since => time.to_s}
|
53
|
+
end
|
54
|
+
|
55
|
+
# Will update all records that have been created, updated or deleted on the remote host
|
56
|
+
# since the last successful synchronization.
|
57
|
+
#
|
58
|
+
def synchronize!(options = {})
|
59
|
+
logger.info( "*** Start synchronizing #{table_name} at #{Time.now.to_s :long} ***\n" )
|
60
|
+
@sync_count = 0
|
61
|
+
begin
|
62
|
+
changed_objects = changed_remotes_since( options[:since] || synchronized_at )
|
63
|
+
if changed_objects.any?
|
64
|
+
# Do everything within transaction to prevent ending up in half-synchronized situation if an exception is raised.
|
65
|
+
transaction { sync_all_records_for(changed_objects) }
|
66
|
+
else
|
67
|
+
logger.info( " - No #{table_name} to update.\n" )
|
68
|
+
end
|
69
|
+
rescue => e
|
70
|
+
logger.warn( " - Synchronization of #{table_name} failed: #{e} \n #{e.backtrace}" )
|
71
|
+
else
|
72
|
+
self.synchronized_at = changed_objects.map { |o| time_of_update(o) }.sort.last if changed_objects.any?
|
73
|
+
logger.info( " - Synchronized #{@sync_count} #{table_name}.\n" ) if @sync_count > 0
|
74
|
+
ensure
|
75
|
+
logger.info( "*** Stopped synchronizing #{table_name} at #{Time.now.to_s :long} ***\n" )
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Time of the last successful synchronization.
|
80
|
+
#
|
81
|
+
def synchronized_at
|
82
|
+
HasRemote::Synchronization.for(self.name).latest_change
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def synchronized_at=(time) #:nodoc:
|
88
|
+
HasRemote::Synchronization.create!(:model_name => self.name, :latest_change => time)
|
89
|
+
end
|
90
|
+
|
91
|
+
def sync_all_records_for(resources) #:nodoc:
|
92
|
+
resources.each { |resource| sync_all_records_for_resource(resource) }
|
93
|
+
end
|
94
|
+
|
95
|
+
def sync_all_records_for_resource(resource) #:nodoc:
|
96
|
+
records = find(:all, :conditions => ["#{remote_foreign_key} = ?", resource.send(remote_primary_key)])
|
97
|
+
if records.empty?
|
98
|
+
create_record_for_resource(resource) unless deleted?(resource)
|
99
|
+
else
|
100
|
+
records.each { |record| sync_record_for_resource(record, resource) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def sync_record_for_resource(record, resource) #:nodoc:
|
105
|
+
if deleted?(resource)
|
106
|
+
delete_record_for_resource(record, resource)
|
107
|
+
else
|
108
|
+
update_and_save_record_for_resource(record, resource)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def update_and_save_record_for_resource(record, resource) #:nodoc:
|
113
|
+
was_it_new = record.new_record?
|
114
|
+
cached_attributes.each do |remote_attr|
|
115
|
+
local_attr = remote_attribute_aliases[remote_attr] || remote_attr
|
116
|
+
record.send :write_attribute, local_attr, resource.send(remote_attr)
|
117
|
+
end
|
118
|
+
record.skip_update_cache = true # Dont update cache again on save:
|
119
|
+
if record.save!
|
120
|
+
@sync_count += 1
|
121
|
+
logger.info( was_it_new ? " - Created #{name.downcase} with id #{record.id}.\n" : " - Updated #{name.downcase} with id #{record.id}.\n" )
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def delete_record_for_resource(record, resource) #:nodoc:
|
126
|
+
record.destroy
|
127
|
+
@sync_count += 1
|
128
|
+
logger.info( " - Deleted #{name.downcase} with id #{record.id}.\n" )
|
129
|
+
end
|
130
|
+
|
131
|
+
def create_record_for_resource(resource) #:nodoc:
|
132
|
+
update_and_save_record_for_resource(new(remote_foreign_key => resource.send(remote_primary_key)), resource)
|
133
|
+
end
|
134
|
+
|
135
|
+
def time_of_update(resource)
|
136
|
+
(resource.respond_to?(:deleted_at) && resource.deleted_at) ? resource.deleted_at : resource.updated_at
|
137
|
+
end
|
138
|
+
|
139
|
+
def deleted?(resource)
|
140
|
+
resource.respond_to?(:deleted_at) && resource.deleted_at && resource.deleted_at <= Time.now
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module HasRemote
|
2
|
+
|
3
|
+
class Synchronization < ActiveRecord::Base #:nodoc:
|
4
|
+
set_table_name 'has_remote_synchronizations'
|
5
|
+
|
6
|
+
named_scope :for, lambda { |model_name| {:conditions => ["model_name = ?", model_name.to_s.classify] } } do
|
7
|
+
def latest_change
|
8
|
+
self.find(:first, :order => 'latest_change DESC').latest_change rescue nil
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
validates_presence_of :model_name, :latest_change
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
data/lib/has_remote.rb
ADDED
@@ -0,0 +1,234 @@
|
|
1
|
+
# The main module for the has_remote plugin. Please see README for more information.
|
2
|
+
#
|
3
|
+
module HasRemote
|
4
|
+
|
5
|
+
def self.included(base) #:nodoc:
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
# Returns an array of all models that have a remote.
|
10
|
+
#
|
11
|
+
def self.models
|
12
|
+
# Make sure all models are loaded:
|
13
|
+
Dir[File.join(RAILS_ROOT, 'app', 'models', '*.rb')].each { |f| require_dependency f }
|
14
|
+
|
15
|
+
@models ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
# Updates cached attributes, destroys deleted records and adds new records of all models that have a remote.
|
19
|
+
# Also see HasRemote::Synchronizable.
|
20
|
+
#
|
21
|
+
def self.synchornize!
|
22
|
+
models.each(&:synchronize!)
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
|
27
|
+
# Gives your local ActiveRecord model a remote proxy (ActiveResource::Base),
|
28
|
+
# which enables you to look for certain attributes remotely.
|
29
|
+
#
|
30
|
+
# ==== Options
|
31
|
+
#
|
32
|
+
# [:foreign_key] The name of the column used to store the id of the remote resource. Defaults to :remote_id.
|
33
|
+
# [:remote_primary_key] The name of the remote resource's primary key. Defaults to :id.
|
34
|
+
# [:site, :user, :password, ...] Basically all ActiveResource configuration settings are available,
|
35
|
+
# see http://api.rubyonrails.org/classes/ActiveResource/Base.html
|
36
|
+
# [:through] Optional custom ActiveResource class name to use for the proxy. If not set, a default class called
|
37
|
+
# "<ModelName>::Remote" will be created dynamically. *Note* that any ActiveResource
|
38
|
+
# configuration options will still be applied to this class.
|
39
|
+
#
|
40
|
+
# ==== Usage
|
41
|
+
#
|
42
|
+
# class User < ActiveRecord::Base
|
43
|
+
# has_remote :site => 'http://people.local'
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# # In a migration:
|
47
|
+
# add_column :users, :remote_id, :integer
|
48
|
+
#
|
49
|
+
# User.find(1).remote
|
50
|
+
# # => #<User::Remote> (inherits from ActiveResource::Base)
|
51
|
+
# User.find(1).remote.username
|
52
|
+
# # => "User name from remote server"
|
53
|
+
#
|
54
|
+
# has_remote also takes a block which is passed in a HasRemote::Config object which can be used to specify
|
55
|
+
# remote attributes:
|
56
|
+
#
|
57
|
+
# class User < ActiveRecord::Base
|
58
|
+
# has_remote :site => '...' do |remote|
|
59
|
+
# remote.attribute :username
|
60
|
+
# remote.attribute :full_name, :local_cache => true
|
61
|
+
# remote.attribute :email_address, :as => :email
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# User.find(1).username
|
66
|
+
# # => "User name from remote server"
|
67
|
+
#
|
68
|
+
def has_remote(options, &block)
|
69
|
+
unless options[:through] || self.const_defined?("Remote")
|
70
|
+
self.const_set("Remote", ActiveResource::Base.clone)
|
71
|
+
end
|
72
|
+
|
73
|
+
@remote_class = options[:through] ? options.delete(:through).constantize : self::Remote
|
74
|
+
|
75
|
+
@remote_foreign_key = options.delete(:foreign_key) || :remote_id
|
76
|
+
|
77
|
+
@remote_primary_key = options.delete(:remote_primary_key) || :id
|
78
|
+
|
79
|
+
# create extra class methods
|
80
|
+
class << self
|
81
|
+
attr_reader :remote_class
|
82
|
+
attr_reader :remote_foreign_key
|
83
|
+
attr_reader :remote_finder
|
84
|
+
attr_reader :remote_primary_key
|
85
|
+
attr_writer :remote_attribute_aliases
|
86
|
+
|
87
|
+
def remote_attributes # :nodoc:
|
88
|
+
@remote_attributes ||= []
|
89
|
+
end
|
90
|
+
|
91
|
+
def remote_attribute_aliases # :nodoc:
|
92
|
+
@remote_attribute_aliases ||= {}
|
93
|
+
end
|
94
|
+
|
95
|
+
include HasRemote::Synchronizable
|
96
|
+
end
|
97
|
+
|
98
|
+
# set ARes to look for correct resource (only if not manually specified)
|
99
|
+
unless options[:element_name] || @remote_class.element_name != "remote"
|
100
|
+
@remote_class.element_name = self.name.underscore.split('/').last
|
101
|
+
end
|
102
|
+
|
103
|
+
# setup ARes class with given options
|
104
|
+
options.each do |option, value|
|
105
|
+
@remote_class.send "#{option}=", value
|
106
|
+
end
|
107
|
+
|
108
|
+
attr_accessor :skip_update_cache
|
109
|
+
|
110
|
+
block.call( Config.new(self) ) if block_given?
|
111
|
+
|
112
|
+
include InstanceMethods
|
113
|
+
HasRemote.models << self
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
module InstanceMethods
|
119
|
+
|
120
|
+
# Returns the remote proxy for this record as an <tt>ActiveResource::Base</tt> object. Returns nil
|
121
|
+
# if foreign key is nil.
|
122
|
+
#
|
123
|
+
# *Arguments*
|
124
|
+
#
|
125
|
+
# - <tt>force_reload</tt>: Forces a reload from the remote server if set to true. Defaults to false.
|
126
|
+
#
|
127
|
+
def remote(force_reload = false)
|
128
|
+
if force_reload || @remote.nil?
|
129
|
+
id = self.send(self.class.remote_foreign_key)
|
130
|
+
@remote = id ? (self.class.remote_finder ? self.class.remote_finder[id] : self.class.remote_class.find(id)) : nil
|
131
|
+
end
|
132
|
+
@remote
|
133
|
+
end
|
134
|
+
|
135
|
+
# Checks whether a remote proxy exists.
|
136
|
+
#
|
137
|
+
def has_remote?
|
138
|
+
# NOTE ARes#exists? is broken:
|
139
|
+
# https://rails.lighthouseapp.com/projects/8994/tickets/1223-activeresource-head-request-sends-headers-with-a-nil-key
|
140
|
+
#
|
141
|
+
return !remote(true).nil? rescue false
|
142
|
+
end
|
143
|
+
|
144
|
+
# Synchronizes all locally cached remote attributes to this object and saves the object.
|
145
|
+
#
|
146
|
+
def update_cached_attributes!
|
147
|
+
update_cached_attributes
|
148
|
+
save!
|
149
|
+
end
|
150
|
+
|
151
|
+
# Synchronizes all locally cached remote attributes to this object, but does not save the object.
|
152
|
+
#
|
153
|
+
# Note that when the remote does no longer exist, all remote attributes will be
|
154
|
+
# set to nil.
|
155
|
+
#
|
156
|
+
def update_cached_attributes
|
157
|
+
unless self.skip_update_cache || self.class.cached_attributes.empty?
|
158
|
+
self.class.cached_attributes.each do |remote_attr|
|
159
|
+
local_attr = self.class.remote_attribute_aliases[remote_attr] || remote_attr
|
160
|
+
write_attribute(local_attr, has_remote? ? remote(true).send(remote_attr) : nil)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
class Config
|
168
|
+
def initialize(base) #:nodoc:
|
169
|
+
@base = base
|
170
|
+
end
|
171
|
+
|
172
|
+
# Defines a remote attribute. Adds a getter method on instances, which delegates to the remote object.
|
173
|
+
#
|
174
|
+
# *Options*
|
175
|
+
#
|
176
|
+
# [:local_cache] If set to true the attribute will also be saved locally. See README for more information
|
177
|
+
# about caching and synchronization.
|
178
|
+
# [:as] Optionally map remote attribute to this name.
|
179
|
+
#
|
180
|
+
# *Example*
|
181
|
+
#
|
182
|
+
# class User < ActiveRecord::Base
|
183
|
+
# has_remote :site => '...' do |remote|
|
184
|
+
# remote.attribute :name, :local_cache => true
|
185
|
+
# remote.attribute :email, :as => :email_address
|
186
|
+
# end
|
187
|
+
# end
|
188
|
+
#
|
189
|
+
def attribute(attr_name, options = {})
|
190
|
+
method_name = options[:as] || attr_name
|
191
|
+
|
192
|
+
@base.remote_attributes << attr_name
|
193
|
+
@base.remote_attribute_aliases = @base.remote_attribute_aliases.merge(attr_name => method_name)
|
194
|
+
|
195
|
+
unless options[:local_cache]
|
196
|
+
@base.class_eval <<-RB
|
197
|
+
|
198
|
+
def #{method_name}
|
199
|
+
remote.try(:#{attr_name})
|
200
|
+
end
|
201
|
+
|
202
|
+
def #{method_name}=(arg)
|
203
|
+
raise NoMethodError.new("Remote attributes can't be set directly in this version of has_remote.")
|
204
|
+
end
|
205
|
+
|
206
|
+
RB
|
207
|
+
else
|
208
|
+
@base.cached_attributes << attr_name
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
|
213
|
+
# Lets you specify custom finder logic to find the record's remote object.
|
214
|
+
# It takes a block which is passed in the id of the remote object.
|
215
|
+
#
|
216
|
+
# (By default <tt>Model.remote_class.find(id)</tt> would be called.)
|
217
|
+
#
|
218
|
+
# *Example*
|
219
|
+
#
|
220
|
+
# class User < ActiveRecord::Base
|
221
|
+
# has_remote :site => "..." do |remote|
|
222
|
+
# remote.finder do |id|
|
223
|
+
# User::Remote.find :one, :from => "users/active/#{id}.xml"
|
224
|
+
# end
|
225
|
+
# end
|
226
|
+
# end
|
227
|
+
#
|
228
|
+
def finder(&block)
|
229
|
+
@base.instance_variable_set "@remote_finder", block
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
namespace :hr do
|
2
|
+
|
3
|
+
desc 'Synchronizes all attributes locally cached by has_remote'
|
4
|
+
task :sync => :environment do
|
5
|
+
models = ENV['MODELS'].nil? ? HasRemote.models : extract_models
|
6
|
+
models.each(&:synchronize!)
|
7
|
+
end
|
8
|
+
|
9
|
+
def extract_models
|
10
|
+
ENV['MODELS'].split(',').map(&:constantize)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module HasRemoteMacros
|
2
|
+
|
3
|
+
def should_have_remote(&block)
|
4
|
+
klass = model_class
|
5
|
+
should "have remote resources" do
|
6
|
+
assert HasRemote.models.include?(klass)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
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)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
Test::Unit::TestCase.extend HasRemoteMacros
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
context "Given existing remote resources" do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
User.delete_all
|
7
|
+
stub_resource 1, :email => "joeremote@foo.bar"
|
8
|
+
stub_resource 2, :email => "jane_remote@foo.bar"
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "a user" do
|
12
|
+
|
13
|
+
it "should return cached attributes" do
|
14
|
+
User.should respond_to(:cached_attributes)
|
15
|
+
User.cached_attributes.should include(:email)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should return changed remotes since yesterday" do
|
19
|
+
user_1, user_2 = mock(:user), mock(:user)
|
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(:changed_remotes_since)
|
24
|
+
User.changed_remotes_since(time).should include(user_1, user_2)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should find last synchronization time" do
|
28
|
+
times = []
|
29
|
+
1.upto(3) do |i|
|
30
|
+
times << i.days.ago
|
31
|
+
HasRemote::Synchronization.create(:model_name => 'HasRemoteSpec::User', :latest_change => times.last)
|
32
|
+
end
|
33
|
+
User.should respond_to(:synchronized_at)
|
34
|
+
User.synchronized_at.to_s.should == times.first.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should not delegate cached remote attributes" do
|
38
|
+
user = User.create! :remote_id => 1
|
39
|
+
User::Remote.should_not_receive(:find)
|
40
|
+
user.email.should == "joeremote@foo.bar"
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should update its cached remote attributes on save" do
|
44
|
+
user = User.create! :remote_id => 1
|
45
|
+
user[:email].should == "joeremote@foo.bar"
|
46
|
+
user.update_attributes(:remote_id => 2)
|
47
|
+
user[:email].should == "jane_remote@foo.bar"
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should not update its cached remote attributes if skip_update_cache is true" do
|
51
|
+
user = User.create! :remote_id => 1, :skip_update_cache => true
|
52
|
+
user[:email].should == nil
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "synchronization" do
|
58
|
+
|
59
|
+
describe "for the User model" do
|
60
|
+
|
61
|
+
describe "with updated and deleted remotes" do
|
62
|
+
|
63
|
+
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
|
+
@yesterday = DateTime.parse 1.day.ago.to_s
|
67
|
+
|
68
|
+
resources = [
|
69
|
+
mock(:user, :id => 1, :email => "changed@foo.bar", :updated_at => @yesterday),
|
70
|
+
mock(:user, :id => 2, :email => "altered@foo.bar", :updated_at => 2.days.ago, :deleted_at => nil),
|
71
|
+
mock(:user, :id => 3, :email => "deleted@foo.bar", :updated_at => 2.days.ago, :deleted_at => 2.days.ago),
|
72
|
+
mock(:user, :id => 4, :email => "new@foo.bar", :updated_at => @yesterday),
|
73
|
+
mock(:user, :id => 5, :email => "new-deleted@foo.bar", :updated_at => 2.days.ago, :deleted_at => 2.days.ago),
|
74
|
+
]
|
75
|
+
User.stub!(:changed_remotes_since).and_return(resources)
|
76
|
+
|
77
|
+
lambda { User.synchronize! }.should change(HasRemote::Synchronization, :count).by(1)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should keep track of the last synchronization" do
|
81
|
+
HasRemote::Synchronization.for("HasRemoteSpec::User").latest_change.should == @yesterday
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should update changed users" do
|
85
|
+
@user_1.reload[:email].should == "changed@foo.bar"
|
86
|
+
@user_2.reload[:email].should == "altered@foo.bar"
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should destroy deleted users" do
|
90
|
+
User.exists?(@user_3).should be_false
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should create added users" do
|
94
|
+
User.exists?(:remote_id => 4).should be_true
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should not create deleted users" do
|
98
|
+
User.exists?(:remote_id => 5).should be_false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "that fails" do
|
103
|
+
|
104
|
+
before(:each) do
|
105
|
+
|
106
|
+
@failure = lambda {
|
107
|
+
user_1, user_2 = User.create!(:remote_id => 1), User.create!(:remote_id => 2)
|
108
|
+
|
109
|
+
yesterday = DateTime.parse 1.day.ago.to_s
|
110
|
+
|
111
|
+
resources = [
|
112
|
+
mock(:user, :id => 1, :email => "changed@foo.bar", :updated_at => yesterday),
|
113
|
+
mock(:user, :id => 2, :email => "altered@foo.bar", :updated_at => 2.days.ago)
|
114
|
+
]
|
115
|
+
|
116
|
+
User.stub!(:changed_remotes_since).and_return(resources)
|
117
|
+
|
118
|
+
resources.last.should_receive(:send).and_raise "All hell breaks loose" # Raise when attr is read from resource 2.
|
119
|
+
|
120
|
+
User.synchronize!
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should do it silently" do
|
125
|
+
@failure.should_not raise_error
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should not create a synchronization record" do
|
129
|
+
@failure.should_not change(HasRemote::Synchronization, :count)
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
describe "for a single user" do
|
137
|
+
|
138
|
+
it "should update the user" do
|
139
|
+
user = User.create! :remote_id => 1
|
140
|
+
user[:email].should == "joeremote@foo.bar"
|
141
|
+
|
142
|
+
stub_resource 1, :email => "changed@foo.bar"
|
143
|
+
|
144
|
+
user.update_cached_attributes!
|
145
|
+
user[:email].should == "changed@foo.bar"
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
describe "synchronizing new cheeses" do
|
153
|
+
before do
|
154
|
+
resources = [
|
155
|
+
mock(:cheese, :id => 1, :name => "Brie", :updated_at => Date.yesterday)
|
156
|
+
]
|
157
|
+
Cheese.stub!(:changed_remotes_since).and_return(resources)
|
158
|
+
lambda{ Cheese.synchronize! }.should change(Cheese, :count).from(0).to(1)
|
159
|
+
end
|
160
|
+
|
161
|
+
after { Cheese.delete_all }
|
162
|
+
|
163
|
+
it "should populate the local 'maturity' attribute with its default database value" do
|
164
|
+
Cheese.first.maturity.should == 5
|
165
|
+
end
|
166
|
+
|
167
|
+
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
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
def stub_resource(id, attrs)
|
176
|
+
resource = mock(:user, {:id => id}.merge(attrs))
|
177
|
+
User::Remote.stub!(:find).with(id).and_return(resource)
|
178
|
+
resource
|
179
|
+
end
|
data/spec/database.yml
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
module HasRemoteSpec
|
2
|
+
class User < ActiveRecord::Base
|
3
|
+
has_remote :site => "http://dummy.local" do |remote|
|
4
|
+
remote.attribute :name
|
5
|
+
remote.attribute :email, :local_cache => true
|
6
|
+
remote.attribute :phone, :as => :telephone
|
7
|
+
end
|
8
|
+
|
9
|
+
before_save :update_cached_attributes
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe HasRemote do
|
4
|
+
|
5
|
+
it "should know about registered models" do
|
6
|
+
HasRemote.should respond_to(:models)
|
7
|
+
HasRemote.models.should include(User, Book)
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
describe User do
|
13
|
+
|
14
|
+
it "should respond to 'remote_class' and 'remote_foreign_key'" do
|
15
|
+
User.should respond_to(:remote_class, :remote_foreign_key)
|
16
|
+
User.remote_class.should == User::Remote
|
17
|
+
User.remote_foreign_key.should == :remote_id
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should set remote class' configuration" do
|
21
|
+
User.remote_class.site.should_not be_nil
|
22
|
+
User.remote_class.element_name.should == "user"
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should return remote attributes" do
|
26
|
+
User.remote_attributes.should include(:name, :email)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should return remote attribute aliases" do
|
30
|
+
User.remote_attribute_aliases[:name].should == :name
|
31
|
+
User.remote_attribute_aliases[:phone].should == :telephone
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "instances" do
|
35
|
+
|
36
|
+
before(:each) do
|
37
|
+
@user = User.new
|
38
|
+
@user.remote_id = 1
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should have a generated remote" do
|
42
|
+
User::Remote.should_receive(:find).twice.with(1).and_return( mock(:resource, :name => "John") )
|
43
|
+
|
44
|
+
@user.should respond_to(:remote)
|
45
|
+
@user.remote.should respond_to(:name)
|
46
|
+
@user.has_remote?.should be_true
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should delegate remote attribute" do
|
50
|
+
User::Remote.should_receive(:find).once.with(1).and_return( mock(:resource, :name => "John") )
|
51
|
+
|
52
|
+
@user.should respond_to(:name)
|
53
|
+
@user.name.should == "John"
|
54
|
+
end
|
55
|
+
|
56
|
+
context "without a remote" do
|
57
|
+
|
58
|
+
before(:each) do
|
59
|
+
@user.remote_id = nil
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should return nil for remote attributes" do
|
63
|
+
@user.remote.should be_nil
|
64
|
+
@user.name.should be_nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe Book do
|
71
|
+
|
72
|
+
it "should use a custom remote key" do
|
73
|
+
Book.remote_foreign_key.should == :custom_remote_id
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "instances" do
|
77
|
+
|
78
|
+
it "should have a custom remote" do
|
79
|
+
@book = Book.new
|
80
|
+
@book.custom_remote_id = 1
|
81
|
+
|
82
|
+
HasRemoteSpec::RemoteBook.should_receive(:find).twice.with(1).and_return( mock(:resource, :title => "Ruby for Rails") )
|
83
|
+
|
84
|
+
@book.should respond_to(:remote)
|
85
|
+
@book.remote.should respond_to(:title)
|
86
|
+
@book.has_remote?.should be_true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe Product do
|
92
|
+
|
93
|
+
it "should have a custom finder" do
|
94
|
+
Product.should respond_to(:remote_finder)
|
95
|
+
Product.remote_finder.should be_a(Proc)
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "instances" do
|
99
|
+
|
100
|
+
it "should use a custom finder" do
|
101
|
+
@product = Product.new :remote_id => 1
|
102
|
+
Product::Remote.should_receive(:find).once.with(:one, :from => "/special/place/products/1.xml").and_return("resource")
|
103
|
+
|
104
|
+
@product.remote.should == "resource"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
data/spec/schema.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
ActiveRecord::Migration.verbose = false
|
2
|
+
|
3
|
+
ActiveRecord::Schema.define(:version => 0) do
|
4
|
+
create_table :books do |t|
|
5
|
+
t.integer :custom_remote_id
|
6
|
+
end
|
7
|
+
|
8
|
+
create_table :users do |t|
|
9
|
+
t.integer :remote_id
|
10
|
+
t.string :email
|
11
|
+
end
|
12
|
+
|
13
|
+
create_table :products do |t|
|
14
|
+
t.integer :remote_id
|
15
|
+
end
|
16
|
+
|
17
|
+
create_table :has_remote_synchronizations do |t|
|
18
|
+
t.string :model_name, :null => false
|
19
|
+
t.datetime :latest_change, :null => false
|
20
|
+
t.datetime :created_at
|
21
|
+
end
|
22
|
+
|
23
|
+
create_table :cheeses do |t|
|
24
|
+
t.string :name
|
25
|
+
t.integer :maturity, :default => 5
|
26
|
+
t.integer :smell
|
27
|
+
t.integer :remote_id
|
28
|
+
end
|
29
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Fake Rails constants
|
2
|
+
RAILS_ENV = ENV["RAILS_ENV"] ||= 'test'
|
3
|
+
RAILS_ROOT = File.dirname(__FILE__)
|
4
|
+
|
5
|
+
# Include required libraries
|
6
|
+
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'
|
16
|
+
|
17
|
+
# Initialize plugin
|
18
|
+
require "#{File.dirname(__FILE__)}/../init"
|
19
|
+
|
20
|
+
# Create logger
|
21
|
+
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
|
22
|
+
|
23
|
+
# Setup database connection and structure
|
24
|
+
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
|
25
|
+
ActiveRecord::Base.establish_connection(config[ENV['RAILS_ENV']])
|
26
|
+
load(File.dirname(__FILE__) + "/schema.rb")
|
27
|
+
|
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
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe HasRemote::Synchronization do
|
4
|
+
|
5
|
+
subject { HasRemote::Synchronization.new }
|
6
|
+
|
7
|
+
it { should validate_presence_of(:model_name) }
|
8
|
+
it { should validate_presence_of(:latest_change) }
|
9
|
+
it { should have_named_scope("for('User')") }
|
10
|
+
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: HasRemote
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sjoerd Andringa
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-18 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Bind a remote ActiveResource object to your local ActiveRecord objects, delegate attributes and optionally cache remote attributes locally.
|
17
|
+
email: sjoerd.andringa@innovationfactory.eu
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
- TODO
|
25
|
+
files:
|
26
|
+
- .gitignore
|
27
|
+
- MIT-LICENSE
|
28
|
+
- README.rdoc
|
29
|
+
- Rakefile
|
30
|
+
- TODO
|
31
|
+
- VERSION
|
32
|
+
- init.rb
|
33
|
+
- lib/generators/has_remote_migration/has_remote_migration_generator.rb
|
34
|
+
- lib/generators/has_remote_migration/templates/create_has_remote_synchronizations.erb
|
35
|
+
- lib/has_remote.rb
|
36
|
+
- lib/has_remote/synchronizable.rb
|
37
|
+
- lib/has_remote/synchronization.rb
|
38
|
+
- lib/tasks/has_remote.rake
|
39
|
+
- shoulda_macros/has_remote_macros.rb
|
40
|
+
- spec/caching_spec.rb
|
41
|
+
- spec/database.yml
|
42
|
+
- spec/has_remote_spec.rb
|
43
|
+
- spec/has_remote_spec/book.rb
|
44
|
+
- spec/has_remote_spec/cheese.rb
|
45
|
+
- spec/has_remote_spec/product.rb
|
46
|
+
- spec/has_remote_spec/user.rb
|
47
|
+
- spec/schema.rb
|
48
|
+
- spec/spec_helper.rb
|
49
|
+
- spec/synchronization_spec.rb
|
50
|
+
has_rdoc: true
|
51
|
+
homepage: http://github.com/innovationfactory/has_remote
|
52
|
+
licenses: []
|
53
|
+
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options:
|
56
|
+
- --charset=UTF-8
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: "0"
|
70
|
+
version:
|
71
|
+
requirements: []
|
72
|
+
|
73
|
+
rubyforge_project:
|
74
|
+
rubygems_version: 1.3.5
|
75
|
+
signing_key:
|
76
|
+
specification_version: 3
|
77
|
+
summary: Bind a remote ActiveResource object to your local ActiveRecord objects.
|
78
|
+
test_files:
|
79
|
+
- spec/caching_spec.rb
|
80
|
+
- spec/has_remote_spec/book.rb
|
81
|
+
- spec/has_remote_spec/cheese.rb
|
82
|
+
- spec/has_remote_spec/product.rb
|
83
|
+
- spec/has_remote_spec/user.rb
|
84
|
+
- spec/has_remote_spec.rb
|
85
|
+
- spec/schema.rb
|
86
|
+
- spec/spec_helper.rb
|
87
|
+
- spec/synchronization_spec.rb
|