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 +4 -0
- data/Gemfile +4 -0
- data/Rakefile +10 -0
- data/lib/remotable/active_resource_fixes.rb +60 -0
- data/lib/remotable/core_ext/enumerable.rb +7 -0
- data/lib/remotable/core_ext.rb +1 -0
- data/lib/remotable/version.rb +3 -0
- data/lib/remotable.rb +298 -0
- data/remotable.gemspec +30 -0
- data/test/factories/tenants.rb +8 -0
- data/test/remotable_test.rb +220 -0
- data/test/support/active_resource.rb +32 -0
- data/test/support/schema.rb +24 -0
- data/test/test_helper.rb +19 -0
- metadata +161 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -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 @@
|
|
1
|
+
require "remotable/core_ext/enumerable"
|
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,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
|
data/test/test_helper.rb
ADDED
@@ -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
|