remotable 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in remotable.gemspec
4
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << 'lib'
7
+ t.libs << 'test'
8
+ t.pattern = 'test/**/*_test.rb'
9
+ t.verbose = false
10
+ end
@@ -0,0 +1,60 @@
1
+ require 'active_resource'
2
+
3
+
4
+ module ActiveResourceFixes
5
+ end
6
+
7
+
8
+ module ActiveResourceFixes30
9
+ include ActiveResourceFixes
10
+
11
+ # ! in this method, don't check the Content-Type header: rack doesn't always return it
12
+ def load_attributes_from_response(response)
13
+ if !response.body.nil? && response.body.strip.size > 0
14
+ load(self.class.format.decode(response.body))
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+
21
+ module ActiveResourceFixes31
22
+ include ActiveResourceFixes
23
+
24
+ # ! in this method, don't check the Content-Type header: rack doesn't always return it
25
+ def load_attributes_from_response(response)
26
+ if !response.body.nil? && response.body.strip.size > 0
27
+ load(self.class.format.decode(response.body), true)
28
+ @persisted = true
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+
35
+ if Rails.version < '3.1'
36
+ ActiveResource::Base.send(:include, ActiveResourceFixes30)
37
+ else
38
+ ActiveResource::Base.send(:include, ActiveResourceFixes31)
39
+ end
40
+
41
+
42
+ # ActiveResource expects that errors will be an array of string
43
+ # However, this is not what Rails Responders are inclined to return.
44
+
45
+ class ActiveResource::Errors
46
+
47
+ # Grabs errors from an array of messages (like ActiveRecord::Validations).
48
+ # The second parameter directs the errors cache to be cleared (default)
49
+ # or not (by passing true).
50
+ def from_array(array, save_cache=false)
51
+ clear unless save_cache
52
+ hash = array[0] || {}
53
+ hash.each do |key, value|
54
+ Array.wrap(value).each do |message|
55
+ self[key] = message
56
+ end
57
+ end
58
+ end
59
+
60
+ end
@@ -0,0 +1,7 @@
1
+ module Enumerable
2
+
3
+ def map_to_self(result={})
4
+ inject(result) {|hash, value| hash.merge(value => value)}
5
+ end
6
+
7
+ end
@@ -0,0 +1 @@
1
+ require "remotable/core_ext/enumerable"
@@ -0,0 +1,3 @@
1
+ module Remotable
2
+ VERSION = "0.0.1"
3
+ end
data/lib/remotable.rb ADDED
@@ -0,0 +1,298 @@
1
+ require "remotable/version"
2
+ require "remotable/core_ext"
3
+ require "remotable/active_resource_fixes"
4
+ require "active_support/concern"
5
+
6
+
7
+ # Remotable keeps a locally-stored ActiveRecord
8
+ # synchronized with a remote resource.
9
+ #
10
+ # == Requirements ==
11
+ # Remotable expects there to be an <tt>expires_at</tt>
12
+ # field on the record.
13
+ #
14
+ # == New resources ==
15
+ # When a resource isn't found locally, Remotable
16
+ # fetches it from the remote API and creates a local
17
+ # copy of the resource.
18
+ #
19
+ # == Expired resources ==
20
+ # When a resource is found locally, Remotable checks
21
+ # the value of <tt>expires_at</tt> and re-fetches the
22
+ # remote resource if need be.
23
+ #
24
+ # == Deleted resources ==
25
+ # When a remote resources has been deleted, the local
26
+ # resource should be removed when it is expired.
27
+ #
28
+ # == Creating, Updating, and Destroying local resources ==
29
+ # Before a local record is saved, Remotable tries
30
+ # to apply the changes remotely.
31
+ #
32
+ #
33
+ module Remotable
34
+ extend ActiveSupport::Concern
35
+
36
+
37
+
38
+ included do
39
+ before_update :update_remote_resource, :unless => :nosync?
40
+ before_create :create_remote_resource, :unless => :nosync?
41
+ before_destroy :destroy_remote_resource, :unless => :nosync?
42
+
43
+ before_validation :reset_expiration_date, :on => :create, :unless => :nosync?
44
+
45
+ validates_presence_of :expires_at
46
+
47
+ default_remote_attributes = column_names - ["id", "created_at", "updated_at", "expires_at"]
48
+ @remote_model_name = "#{self.name}::Remote#{self.name}"
49
+ @remote_attribute_map = default_remote_attributes.map_to_self
50
+ @expires_after = 1.day
51
+ end
52
+
53
+
54
+
55
+ module ClassMethods
56
+
57
+ def remote_model_name(name)
58
+ @remote_model = nil
59
+ @remote_model_name = name
60
+ end
61
+
62
+ def attr_remote(*attrs)
63
+ map = attrs.extract_options!
64
+ map = attrs.map_to_self(map)
65
+ @remote_attribute_map = map
66
+ end
67
+
68
+ def fetch_with(local_key)
69
+ @local_key = local_key
70
+ @remote_key = remote_attribute_name(local_key)
71
+
72
+ class_eval <<-RUBY
73
+ def self.find_by_#{local_key}(value)
74
+ local_resource = where(:#{local_key} => value).first
75
+ local_resource || fetch_new_from_remote(value)
76
+ end
77
+
78
+ def self.find_by_#{local_key}!(value)
79
+ find_by_#{local_key}(value) || raise(ActiveRecord::RecordNotFound)
80
+ end
81
+ RUBY
82
+ end
83
+
84
+ def expires_after(val)
85
+ @expires_after = val
86
+ end
87
+
88
+
89
+
90
+ attr_reader :local_key,
91
+ :remote_key,
92
+ :expires_after,
93
+ :remote_attribute_map
94
+
95
+ def remote_model
96
+ @remote_model ||= @remote_model_name.constantize
97
+ end
98
+
99
+ def remote_attribute_names
100
+ remote_attribute_map.keys
101
+ end
102
+
103
+ def local_attribute_names
104
+ remote_attribute_map.values
105
+ end
106
+
107
+ def remote_attribute_name(local_attr)
108
+ remote_attribute_map.key(local_attr) || local_attr
109
+ end
110
+
111
+ def local_attribute_name(remote_attr)
112
+ remote_attribute_map[remote_attr] || remote_attr
113
+ end
114
+
115
+
116
+
117
+ # !nb: this method is called when associations are loaded
118
+ # so you can use the remoted record in associations.
119
+ def instantiate(*args)
120
+ record = super
121
+ record.pull_remote_data! if record.expired?
122
+ record = nil if record.destroyed?
123
+ record
124
+ end
125
+
126
+
127
+
128
+ private
129
+
130
+
131
+
132
+ def fetch_new_from_remote(value)
133
+ record = self.new
134
+ record.send("#{local_key}=", value) # {local_key => value} not passed to :new so local_key can be protected
135
+ if record.remote_resource
136
+ record.pull_remote_data!
137
+ else
138
+ nil
139
+ end
140
+ end
141
+
142
+ end
143
+
144
+
145
+
146
+ delegate :local_key,
147
+ :remote_key,
148
+ :remote_model,
149
+ :remote_attribute_map,
150
+ :remote_attribute_names,
151
+ :remote_attribute_name,
152
+ :local_attribute_names,
153
+ :local_attribute_name,
154
+ :expires_after,
155
+ :to => "self.class"
156
+
157
+ def expired?
158
+ expires_at.nil? || expires_at < Time.now
159
+ end
160
+
161
+ def expired!
162
+ update_attribute(:expires_at, 1.day.ago)
163
+ end
164
+
165
+
166
+
167
+ def pull_remote_data!
168
+ merge_remote_data!(remote_resource)
169
+ end
170
+
171
+
172
+
173
+ def remote_resource
174
+ @remote_resource ||= fetch_remote_resource
175
+ end
176
+
177
+ def any_remote_changes?
178
+ (changed.map(&:to_sym) & local_attribute_names).any?
179
+ end
180
+
181
+
182
+
183
+ def nosync!
184
+ @nosync = true
185
+ end
186
+
187
+ def nosync
188
+ value = @nosync
189
+ @nosync = true
190
+ yield
191
+ ensure
192
+ @nosync = value
193
+ end
194
+
195
+ def nosync=(val)
196
+ @nosync = (val == true)
197
+ end
198
+
199
+ def nosync?
200
+ @nosync == true
201
+ end
202
+
203
+
204
+
205
+ private
206
+
207
+ def fetch_remote_resource
208
+ fetch_value = self[local_key]
209
+ if remote_key == :id
210
+ remote_model.find(fetch_value)
211
+ else
212
+ remote_model.send("find_by_#{remote_key}", fetch_value)
213
+ end
214
+ end
215
+
216
+ def merge_remote_data!(remote_resource)
217
+ if remote_resource.nil?
218
+ nosync { destroy }
219
+
220
+ else
221
+ merge_remote_data(remote_resource)
222
+ reset_expiration_date
223
+ nosync { save! }
224
+ end
225
+
226
+ self
227
+ end
228
+
229
+
230
+
231
+ def update_remote_resource
232
+ if any_remote_changes?
233
+ merge_local_data(remote_resource, true)
234
+ unless remote_resource.save
235
+ merge_remote_errors(remote_resource.errors)
236
+ raise ActiveRecord::RecordInvalid.new(self)
237
+ end
238
+ end
239
+ end
240
+
241
+ def create_remote_resource
242
+ @remote_resource = remote_model.new
243
+ merge_local_data(@remote_resource)
244
+
245
+ if @remote_resource.save
246
+
247
+ # This line is especially crucial if the primary key
248
+ # of the remote resource needs to be stored locally.
249
+ merge_remote_data(@remote_resource)
250
+ else
251
+
252
+ merge_remote_errors(remote_resource.errors)
253
+ raise ActiveRecord::RecordInvalid.new(self)
254
+ end
255
+ end
256
+
257
+ def destroy_remote_resource
258
+ remote_resource && remote_resource.destroy
259
+ end
260
+
261
+
262
+
263
+ def reset_expiration_date
264
+ self.expires_at = expires_after.from_now
265
+ end
266
+
267
+
268
+
269
+ protected
270
+
271
+ def merge_remote_errors(errors)
272
+ errors.each do |attribute, message|
273
+ self.errors[local_attribute_name(attribute)] = message
274
+ end
275
+ self
276
+ end
277
+
278
+ def merge_remote_data(remote_resource)
279
+ remote_attribute_map.each do |remote_attr, local_attr|
280
+ if remote_resource.respond_to?(remote_attr)
281
+ send("#{local_attr}=", remote_resource.send(remote_attr))
282
+ end
283
+ end
284
+ self
285
+ end
286
+
287
+ def merge_local_data(remote_resource, changes_only=false)
288
+ remote_attribute_map.each do |remote_attr, local_attr|
289
+ if !changes_only || changed.member?(local_attr.to_s)
290
+ remote_resource.send("#{remote_attr}=", send(local_attr))
291
+ end
292
+ end
293
+ self
294
+ end
295
+
296
+
297
+
298
+ end
data/remotable.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "remotable/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "remotable"
7
+ s.version = Remotable::VERSION
8
+ s.authors = ["Robert Lail"]
9
+ s.email = ["robert.lail@cph.org"]
10
+ s.homepage = ""
11
+ s.summary = %q{Binds an ActiveRecord model to a remote resource and keeps the two synchronized}
12
+ s.description = %q{Remotable keeps a locally-stored ActiveRecord synchronized with a remote resource.}
13
+
14
+ s.rubyforge_project = "remotable"
15
+
16
+ s.add_dependency "activeresource"
17
+ s.add_dependency "activerecord"
18
+ s.add_dependency "activesupport"
19
+
20
+ s.add_development_dependency "rails"
21
+ s.add_development_dependency "turn"
22
+ s.add_development_dependency "factory_girl"
23
+ s.add_development_dependency "sqlite3"
24
+ s.add_development_dependency "active_resource_simulator"
25
+
26
+ s.files = `git ls-files`.split("\n")
27
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
28
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
29
+ s.require_paths = ["lib"]
30
+ end
@@ -0,0 +1,8 @@
1
+ Factory.define :tenant do |f|
2
+ f.sequence(:slug) { |n| "test#{n}" }
3
+ f.name "Test"
4
+
5
+ f.active true
6
+ f.expires_at 100.years.from_now
7
+ f.nosync true
8
+ end
@@ -0,0 +1,220 @@
1
+ require 'test_helper'
2
+ require 'remotable'
3
+ require 'support/active_resource'
4
+ require 'active_resource_simulator'
5
+
6
+
7
+ class RemotableTest < ActiveSupport::TestCase
8
+
9
+
10
+ test "should create a record locally when fetching a new remote resource" do
11
+ new_tenant_slug = "not_found"
12
+
13
+ assert_equal 0, Tenant.where(:slug => new_tenant_slug).count,
14
+ "There's not supposed to be a Tenant with the subdomain #{new_tenant_slug}."
15
+
16
+ assert_difference "Tenant.count", +1 do
17
+ Tenant::RemoteTenant.run_simulation do |s|
18
+ attrs = {
19
+ :id => 1,
20
+ :slug => "not_found",
21
+ :church_name => "Not Found"
22
+ }
23
+
24
+ s.show(nil, attrs, :path => "/api/accounts/by_slug/#{attrs[:slug]}.json")
25
+
26
+ Tenant.find_by_slug(new_tenant_slug)
27
+ end
28
+ end
29
+ end
30
+
31
+
32
+
33
+ test "should not fetch a remote record when a local record is not expired" do
34
+ tenant = Factory(:tenant, :expires_at => 100.years.from_now)
35
+ unexpected_name = "Totally Wonky"
36
+
37
+ Tenant::RemoteTenant.run_simulation do |s|
38
+ attrs = {
39
+ :id => tenant.id,
40
+ :slug => tenant.slug,
41
+ :church_name => unexpected_name
42
+ }
43
+
44
+ s.show(nil, attrs, :path => "/api/accounts/by_slug/#{attrs[:slug]}.json")
45
+
46
+ tenant = Tenant.find_by_slug(tenant.slug)
47
+ assert_not_equal unexpected_name, tenant.name
48
+ end
49
+ end
50
+
51
+
52
+
53
+ test "should fetch a remote record when a local record is expired" do
54
+ tenant = Factory(:tenant, :expires_at => 1.year.ago)
55
+ unexpected_name = "Totally Wonky"
56
+
57
+ Tenant::RemoteTenant.run_simulation do |s|
58
+ attrs = {
59
+ :id => tenant.id,
60
+ :slug => tenant.slug,
61
+ :church_name => unexpected_name
62
+ }
63
+
64
+ s.show(nil, attrs, :path => "/api/accounts/by_slug/#{attrs[:slug]}.json")
65
+
66
+ tenant = Tenant.find_by_slug(tenant.slug)
67
+ assert_equal unexpected_name, tenant.name
68
+ end
69
+ end
70
+
71
+
72
+
73
+ test "should delete a local record when a remote record has been deleted" do
74
+ tenant = Factory(:tenant, :expires_at => 1.year.ago)
75
+
76
+ assert_difference "Tenant.count", -1 do
77
+ Tenant::RemoteTenant.run_simulation do |s|
78
+
79
+ s.show(nil, nil, :path => "/api/accounts/by_slug/#{tenant.slug}.json", :status => 404)
80
+
81
+ tenant = Tenant.find_by_slug(tenant.slug)
82
+ assert_equal nil, tenant
83
+ end
84
+ end
85
+ end
86
+
87
+
88
+
89
+ test "should update a record remotely when updating one locally" do
90
+ tenant = Factory(:tenant)
91
+ new_name = "Totally Wonky"
92
+
93
+ Tenant::RemoteTenant.run_simulation do |s|
94
+ s.show(nil, {
95
+ :id => tenant.id,
96
+ :slug => tenant.slug,
97
+ :church_name => tenant.name
98
+ }, :path => "/api/accounts/by_slug/#{tenant.slug}.json")
99
+
100
+ s.update(tenant.id)
101
+
102
+ tenant.nosync = false
103
+ tenant.name = "Totally Wonky"
104
+ assert_equal true, tenant.any_remote_changes?
105
+
106
+ tenant.save!
107
+
108
+ pending "Not sure how to test that an update happened"
109
+ end
110
+ end
111
+
112
+ test "should fail to update a record locally when failing to update one remotely" do
113
+ tenant = Factory(:tenant)
114
+ new_name = "Totally Wonky"
115
+
116
+ Tenant::RemoteTenant.run_simulation do |s|
117
+ s.show(nil, {
118
+ :id => tenant.id,
119
+ :slug => tenant.slug,
120
+ :church_name => tenant.name
121
+ }, :path => "/api/accounts/by_slug/#{tenant.slug}.json")
122
+
123
+ s.update(tenant.id, :status => 422, :body => {
124
+ :errors => {:church_name => ["is already taken"]}
125
+ })
126
+
127
+ tenant.nosync = false
128
+ tenant.name = new_name
129
+ assert_raises(ActiveRecord::RecordInvalid) do
130
+ tenant.save!
131
+ end
132
+ assert_equal ["is already taken"], tenant.errors[:name]
133
+ end
134
+ end
135
+
136
+
137
+
138
+ test "should create a record remotely when creating one locally" do
139
+ tenant = Tenant.new({
140
+ :slug => "brand_new",
141
+ :name => "Brand New"
142
+ })
143
+
144
+ Tenant::RemoteTenant.run_simulation do |s|
145
+ s.create({
146
+ :id => 143,
147
+ :slug => tenant.slug,
148
+ :church_name => tenant.name
149
+ })
150
+
151
+ tenant.save!
152
+
153
+ assert_equal true, tenant.remote_resource.persisted?
154
+ end
155
+ end
156
+
157
+ test "should fail to create a record locally when failing to create one remotely" do
158
+ tenant = Tenant.new({
159
+ :slug => "brand_new",
160
+ :name => "Brand New"
161
+ })
162
+
163
+ Tenant::RemoteTenant.run_simulation do |s|
164
+ s.create({
165
+ :errors => {
166
+ :what => ["ever"],
167
+ :church_name => ["is already taken"]}
168
+ }, :status => 422)
169
+
170
+ assert_raises(ActiveRecord::RecordInvalid) do
171
+ tenant.save!
172
+ end
173
+
174
+ assert_equal ["is already taken"], tenant.errors[:name]
175
+ end
176
+ end
177
+
178
+
179
+
180
+ test "should destroy a record remotely when destroying one locally" do
181
+ tenant = Factory(:tenant)
182
+
183
+ Tenant::RemoteTenant.run_simulation do |s|
184
+ s.show(nil, {
185
+ :id => tenant.id,
186
+ :slug => tenant.slug,
187
+ :church_name => tenant.name
188
+ }, :path => "/api/accounts/by_slug/#{tenant.slug}.json")
189
+
190
+ s.destroy(tenant.id)
191
+
192
+ tenant.nosync = false
193
+ tenant.destroy
194
+
195
+ pending # how do I check for this?
196
+ end
197
+ end
198
+
199
+ test "should fail to destroy a record locally when failing to destroy one remotely" do
200
+ tenant = Factory(:tenant)
201
+
202
+ Tenant::RemoteTenant.run_simulation do |s|
203
+ s.show(nil, {
204
+ :id => tenant.id,
205
+ :slug => tenant.slug,
206
+ :church_name => tenant.name
207
+ }, :path => "/api/accounts/by_slug/#{tenant.slug}.json")
208
+
209
+ s.destroy(tenant.id, :status => 500)
210
+
211
+ tenant.nosync = false
212
+ assert_raises(ActiveResource::ServerError) do
213
+ tenant.destroy
214
+ end
215
+ end
216
+ end
217
+
218
+
219
+
220
+ end
@@ -0,0 +1,32 @@
1
+ require 'active_record'
2
+ require 'active_resource'
3
+
4
+ class Tenant < ActiveRecord::Base
5
+ include Remotable
6
+
7
+ attr_remote :slug, :church_name => :name, :active_unite_account => :active
8
+ fetch_with :slug
9
+
10
+ class RemoteTenant < ActiveResource::Base
11
+
12
+ self.site = "http://example.com/api/"
13
+ self.element_name = "account"
14
+ self.format = :json
15
+ self.include_root_in_json = false
16
+ self.user = "username"
17
+ self.password = "password"
18
+
19
+ class << self
20
+ def find_by_slug!(slug)
21
+ find(:one, :from => "/api/accounts/by_slug/#{slug}.json")
22
+ end
23
+
24
+ def find_by_slug(slug)
25
+ find_by_slug!(slug)
26
+ rescue ActiveResource::ResourceNotFound
27
+ nil
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,24 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # Note that this schema.rb definition is the authoritative source for your
6
+ # database schema. If you need to create the application database on another
7
+ # system, you should be using db:schema:load, not running all the migrations
8
+ # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
10
+ #
11
+ # It's strongly recommended to check this file into your version control system.
12
+
13
+ ActiveRecord::Schema.define(:version => 20110507152635) do
14
+
15
+ create_table "tenants", :force => true do |t|
16
+ t.string "slug", :null => false
17
+ t.string "name", :null => false
18
+ t.boolean "active"
19
+ t.datetime "created_at"
20
+ t.datetime "updated_at"
21
+ t.datetime "expires_at"
22
+ end
23
+
24
+ end
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'rails'
3
+ require 'rails/test_help'
4
+ require 'active_support/core_ext'
5
+ require 'factory_girl'
6
+ require 'turn'
7
+
8
+
9
+ require 'active_record'
10
+
11
+ ActiveRecord::Base.establish_connection(
12
+ :adapter => "sqlite3",
13
+ :database => ":memory:",
14
+ :verbosity => "quiet")
15
+
16
+ load File.join(File.dirname(__FILE__), "support", "schema.rb")
17
+
18
+
19
+ require 'factories/tenants'
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remotable
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Robert Lail
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-08-18 00:00:00 -05:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: activeresource
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: activesupport
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: rails
51
+ prerelease: false
52
+ requirement: &id004 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ type: :development
59
+ version_requirements: *id004
60
+ - !ruby/object:Gem::Dependency
61
+ name: turn
62
+ prerelease: false
63
+ requirement: &id005 !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ type: :development
70
+ version_requirements: *id005
71
+ - !ruby/object:Gem::Dependency
72
+ name: factory_girl
73
+ prerelease: false
74
+ requirement: &id006 !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ type: :development
81
+ version_requirements: *id006
82
+ - !ruby/object:Gem::Dependency
83
+ name: sqlite3
84
+ prerelease: false
85
+ requirement: &id007 !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: "0"
91
+ type: :development
92
+ version_requirements: *id007
93
+ - !ruby/object:Gem::Dependency
94
+ name: active_resource_simulator
95
+ prerelease: false
96
+ requirement: &id008 !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: "0"
102
+ type: :development
103
+ version_requirements: *id008
104
+ description: Remotable keeps a locally-stored ActiveRecord synchronized with a remote resource.
105
+ email:
106
+ - robert.lail@cph.org
107
+ executables: []
108
+
109
+ extensions: []
110
+
111
+ extra_rdoc_files: []
112
+
113
+ files:
114
+ - .gitignore
115
+ - Gemfile
116
+ - Rakefile
117
+ - lib/remotable.rb
118
+ - lib/remotable/active_resource_fixes.rb
119
+ - lib/remotable/core_ext.rb
120
+ - lib/remotable/core_ext/enumerable.rb
121
+ - lib/remotable/version.rb
122
+ - remotable.gemspec
123
+ - test/factories/tenants.rb
124
+ - test/remotable_test.rb
125
+ - test/support/active_resource.rb
126
+ - test/support/schema.rb
127
+ - test/test_helper.rb
128
+ has_rdoc: true
129
+ homepage: ""
130
+ licenses: []
131
+
132
+ post_install_message:
133
+ rdoc_options: []
134
+
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: "0"
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ none: false
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: "0"
149
+ requirements: []
150
+
151
+ rubyforge_project: remotable
152
+ rubygems_version: 1.6.2
153
+ signing_key:
154
+ specification_version: 3
155
+ summary: Binds an ActiveRecord model to a remote resource and keeps the two synchronized
156
+ test_files:
157
+ - test/factories/tenants.rb
158
+ - test/remotable_test.rb
159
+ - test/support/active_resource.rb
160
+ - test/support/schema.rb
161
+ - test/test_helper.rb