remotable 0.0.1

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 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