remotable 0.0.1 → 0.1.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/Rakefile +0 -1
- data/lib/remotable.rb +9 -257
- data/lib/remotable/active_record_extender.rb +343 -0
- data/lib/remotable/active_resource_fixes.rb +1 -1
- data/lib/remotable/adapters/active_resource.rb +68 -0
- data/lib/remotable/version.rb +1 -1
- data/test/factories/tenants.rb +1 -2
- data/test/remotable_test.rb +113 -62
- data/test/support/active_resource.rb +38 -27
- data/test/support/schema.rb +4 -4
- metadata +4 -2
data/Rakefile
CHANGED
data/lib/remotable.rb
CHANGED
@@ -1,7 +1,4 @@
|
|
1
1
|
require "remotable/version"
|
2
|
-
require "remotable/core_ext"
|
3
|
-
require "remotable/active_resource_fixes"
|
4
|
-
require "active_support/concern"
|
5
2
|
|
6
3
|
|
7
4
|
# Remotable keeps a locally-stored ActiveRecord
|
@@ -31,268 +28,23 @@ require "active_support/concern"
|
|
31
28
|
#
|
32
29
|
#
|
33
30
|
module Remotable
|
34
|
-
extend ActiveSupport::Concern
|
35
31
|
|
36
32
|
|
37
33
|
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
34
|
+
def remote_model(*args)
|
35
|
+
if args.any?
|
36
|
+
@remote_model = args.first
|
246
37
|
|
247
|
-
|
248
|
-
|
249
|
-
merge_remote_data(@remote_resource)
|
38
|
+
require "remotable/active_record_extender"
|
39
|
+
include Remotable::ActiveRecordExtender
|
250
40
|
else
|
251
|
-
|
252
|
-
merge_remote_errors(remote_resource.errors)
|
253
|
-
raise ActiveRecord::RecordInvalid.new(self)
|
41
|
+
@remote_model
|
254
42
|
end
|
255
43
|
end
|
256
44
|
|
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
45
|
|
297
46
|
|
298
47
|
end
|
48
|
+
|
49
|
+
|
50
|
+
ActiveRecord::Base.extend(Remotable) if defined?(ActiveRecord::Base)
|
@@ -0,0 +1,343 @@
|
|
1
|
+
require "remotable/core_ext"
|
2
|
+
require "active_support/concern"
|
3
|
+
|
4
|
+
|
5
|
+
module Remotable
|
6
|
+
module ActiveRecordExtender
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
included do
|
12
|
+
before_update :update_remote_resource, :unless => :nosync?
|
13
|
+
before_create :create_remote_resource, :unless => :nosync?
|
14
|
+
before_destroy :destroy_remote_resource, :unless => :nosync?
|
15
|
+
|
16
|
+
before_validation :reset_expiration_date, :on => :create, :unless => :nosync?
|
17
|
+
|
18
|
+
validates_presence_of :expires_at
|
19
|
+
|
20
|
+
default_remote_attributes = column_names - %w{id created_at updated_at expires_at}
|
21
|
+
@remote_attribute_map = default_remote_attributes.map_to_self
|
22
|
+
@remote_attribute_routes = {}
|
23
|
+
@expires_after = 1.day
|
24
|
+
|
25
|
+
extend_remote_model
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
|
32
|
+
def remote_key(*args)
|
33
|
+
if args.any?
|
34
|
+
remote_key = args.first
|
35
|
+
raise("#{remote_key} is not the name of a remote attribute") unless remote_attribute_names.member?(remote_key)
|
36
|
+
@remote_key = remote_key
|
37
|
+
fetch_with(remote_key)
|
38
|
+
remote_key
|
39
|
+
else
|
40
|
+
@remote_key || generate_default_remote_key
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def expires_after(*args)
|
45
|
+
if args.any?
|
46
|
+
@expires_after = args.first
|
47
|
+
else
|
48
|
+
@expires_after
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def attr_remote(*attrs)
|
53
|
+
map = attrs.extract_options!
|
54
|
+
map = attrs.map_to_self(map)
|
55
|
+
@remote_attribute_map = map
|
56
|
+
end
|
57
|
+
|
58
|
+
def fetch_with(*args)
|
59
|
+
keys_and_routes = extract_keys_and_routes(*args)
|
60
|
+
@remote_attribute_routes.merge!(keys_and_routes)
|
61
|
+
end
|
62
|
+
alias :find_by :fetch_with
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
attr_reader :remote_attribute_map,
|
67
|
+
:remote_attribute_routes
|
68
|
+
|
69
|
+
def local_key
|
70
|
+
local_attribute_name(remote_key)
|
71
|
+
end
|
72
|
+
|
73
|
+
def remote_attribute_names
|
74
|
+
remote_attribute_map.keys
|
75
|
+
end
|
76
|
+
|
77
|
+
def local_attribute_names
|
78
|
+
remote_attribute_map.values
|
79
|
+
end
|
80
|
+
|
81
|
+
def remote_attribute_name(local_attr)
|
82
|
+
remote_attribute_map.key(local_attr) || local_attr
|
83
|
+
end
|
84
|
+
|
85
|
+
def local_attribute_name(remote_attr)
|
86
|
+
remote_attribute_map[remote_attr] || remote_attr
|
87
|
+
end
|
88
|
+
|
89
|
+
def route_for(local_key)
|
90
|
+
remote_key = remote_attribute_name(local_key)
|
91
|
+
remote_attribute_routes[remote_key] || default_route_for(local_key, remote_key)
|
92
|
+
end
|
93
|
+
|
94
|
+
def default_route_for(local_key, remote_key=nil)
|
95
|
+
remote_key ||= remote_attribute_name(local_key)
|
96
|
+
"by_#{remote_key}/:#{local_key}.json"
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
# !nb: this method is called when associations are loaded
|
102
|
+
# so you can use the remoted record in associations.
|
103
|
+
def instantiate(*args)
|
104
|
+
record = super
|
105
|
+
record.pull_remote_data! if record.expired?
|
106
|
+
record = nil if record.destroyed?
|
107
|
+
record
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
|
112
|
+
def method_missing(method_sym, *args, &block)
|
113
|
+
method_name = method_sym.to_s
|
114
|
+
|
115
|
+
if method_name =~ /find_by_(.*)(!?)/
|
116
|
+
local_attr, bang, value = $1.to_sym, !$2.blank?, args.first
|
117
|
+
remote_attr = remote_attribute_name(local_attr)
|
118
|
+
|
119
|
+
remote_key # Make sure we've figured out the remote
|
120
|
+
# primary key if we're evaluating a finder
|
121
|
+
|
122
|
+
if remote_attribute_routes.key?(remote_attr)
|
123
|
+
local_resource = where(local_attr => value).first
|
124
|
+
unless local_resource
|
125
|
+
remote_resource = remote_model.find_by(remote_attr, value)
|
126
|
+
local_resource = new_from_remote(remote_resource) if remote_resource
|
127
|
+
end
|
128
|
+
|
129
|
+
raise ActiveRecord::RecordNotFound if local_resource.nil? && bang
|
130
|
+
return local_resource
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
super(method_sym, *args, &block)
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
|
142
|
+
|
143
|
+
def extend_remote_model
|
144
|
+
if remote_model < ActiveResource::Base
|
145
|
+
require "remotable/adapters/active_resource"
|
146
|
+
remote_model.send(:include, Remotable::Adapters::ActiveResource)
|
147
|
+
remote_model.local_model = self
|
148
|
+
|
149
|
+
# !todo
|
150
|
+
# Adapters for other API consumers can be implemented here
|
151
|
+
#
|
152
|
+
|
153
|
+
else
|
154
|
+
raise("#{remote_model} is not a recognized remote resource")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
def extract_keys_and_routes(*local_keys)
|
160
|
+
keys_and_routes = local_keys.extract_options!
|
161
|
+
{}.tap do |hash|
|
162
|
+
local_keys.each {|local_key| hash[remote_attribute_name(local_key)] = nil}
|
163
|
+
keys_and_routes.each {|local_key, value| hash[remote_attribute_name(local_key)] = value}
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
def generate_default_remote_key
|
169
|
+
raise("No remote key supplied and :id is not a remote attribute") unless remote_attribute_names.member?(:id)
|
170
|
+
remote_key(:id)
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
def new_from_remote(remote_resource)
|
175
|
+
record = self.new
|
176
|
+
record.instance_variable_set(:@remote_resource, remote_resource)
|
177
|
+
record.pull_remote_data!
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
|
186
|
+
delegate :local_key,
|
187
|
+
:remote_key,
|
188
|
+
:remote_model,
|
189
|
+
:remote_attribute_map,
|
190
|
+
:remote_attribute_names,
|
191
|
+
:remote_attribute_name,
|
192
|
+
:local_attribute_names,
|
193
|
+
:local_attribute_name,
|
194
|
+
:expires_after,
|
195
|
+
:to => "self.class"
|
196
|
+
|
197
|
+
def expired?
|
198
|
+
expires_at.nil? || expires_at < Time.now
|
199
|
+
end
|
200
|
+
|
201
|
+
def expired!
|
202
|
+
update_attribute(:expires_at, 1.day.ago)
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
|
207
|
+
def pull_remote_data!
|
208
|
+
merge_remote_data!(remote_resource)
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
|
213
|
+
def remote_resource
|
214
|
+
@remote_resource ||= fetch_remote_resource
|
215
|
+
end
|
216
|
+
|
217
|
+
def any_remote_changes?
|
218
|
+
(changed.map(&:to_sym) & local_attribute_names).any?
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
|
223
|
+
def nosync!
|
224
|
+
@nosync = true
|
225
|
+
end
|
226
|
+
|
227
|
+
def nosync
|
228
|
+
value = @nosync
|
229
|
+
@nosync = true
|
230
|
+
yield
|
231
|
+
ensure
|
232
|
+
@nosync = value
|
233
|
+
end
|
234
|
+
|
235
|
+
def nosync=(val)
|
236
|
+
@nosync = (val == true)
|
237
|
+
end
|
238
|
+
|
239
|
+
def nosync?
|
240
|
+
@nosync == true
|
241
|
+
end
|
242
|
+
|
243
|
+
|
244
|
+
|
245
|
+
private
|
246
|
+
|
247
|
+
def fetch_remote_resource
|
248
|
+
fetch_value = self[local_key]
|
249
|
+
remote_model.find_by(remote_key, fetch_value)
|
250
|
+
end
|
251
|
+
|
252
|
+
def merge_remote_data!(remote_resource)
|
253
|
+
if remote_resource.nil?
|
254
|
+
nosync { destroy }
|
255
|
+
|
256
|
+
else
|
257
|
+
merge_remote_data(remote_resource)
|
258
|
+
reset_expiration_date
|
259
|
+
nosync { save! }
|
260
|
+
end
|
261
|
+
|
262
|
+
self
|
263
|
+
end
|
264
|
+
|
265
|
+
|
266
|
+
|
267
|
+
def update_remote_resource
|
268
|
+
if any_remote_changes?
|
269
|
+
merge_local_data(remote_resource, true)
|
270
|
+
unless remote_resource.save
|
271
|
+
merge_remote_errors(remote_resource.errors)
|
272
|
+
raise ActiveRecord::RecordInvalid.new(self)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def create_remote_resource
|
278
|
+
@remote_resource = remote_model.new
|
279
|
+
merge_local_data(@remote_resource)
|
280
|
+
|
281
|
+
if @remote_resource.save
|
282
|
+
|
283
|
+
# This line is especially crucial if the primary key
|
284
|
+
# of the remote resource needs to be stored locally.
|
285
|
+
merge_remote_data(@remote_resource)
|
286
|
+
else
|
287
|
+
|
288
|
+
merge_remote_errors(remote_resource.errors)
|
289
|
+
raise ActiveRecord::RecordInvalid.new(self)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def destroy_remote_resource
|
294
|
+
remote_resource && remote_resource.destroy
|
295
|
+
end
|
296
|
+
|
297
|
+
|
298
|
+
|
299
|
+
def reset_expiration_date
|
300
|
+
self.expires_at = expires_after.from_now
|
301
|
+
end
|
302
|
+
|
303
|
+
|
304
|
+
|
305
|
+
def local_attribute_changed?(name)
|
306
|
+
changed.member?(name.to_s)
|
307
|
+
end
|
308
|
+
|
309
|
+
|
310
|
+
|
311
|
+
protected
|
312
|
+
|
313
|
+
|
314
|
+
|
315
|
+
def merge_remote_errors(errors)
|
316
|
+
errors.each do |attribute, message|
|
317
|
+
self.errors[local_attribute_name(attribute)] = message
|
318
|
+
end
|
319
|
+
self
|
320
|
+
end
|
321
|
+
|
322
|
+
def merge_remote_data(remote_resource)
|
323
|
+
remote_attribute_map.each do |remote_attr, local_attr|
|
324
|
+
if remote_resource.respond_to?(remote_attr)
|
325
|
+
send("#{local_attr}=", remote_resource.send(remote_attr))
|
326
|
+
end
|
327
|
+
end
|
328
|
+
self
|
329
|
+
end
|
330
|
+
|
331
|
+
def merge_local_data(remote_resource, changes_only=false)
|
332
|
+
remote_attribute_map.each do |remote_attr, local_attr|
|
333
|
+
if !changes_only || local_attribute_changed?(local_attr)
|
334
|
+
remote_resource.send("#{remote_attr}=", send(local_attr))
|
335
|
+
end
|
336
|
+
end
|
337
|
+
self
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
|
342
|
+
end
|
343
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require "remotable/active_resource_fixes"
|
2
|
+
require "active_support/concern"
|
3
|
+
|
4
|
+
|
5
|
+
module Remotable
|
6
|
+
module Adapters
|
7
|
+
module ActiveResource
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
|
13
|
+
attr_accessor :local_model
|
14
|
+
|
15
|
+
delegate :local_attribute_name,
|
16
|
+
:route_for,
|
17
|
+
:to => :local_model
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
def find_by!(key, value)
|
22
|
+
if key == :id
|
23
|
+
find(value)
|
24
|
+
else
|
25
|
+
find(:one, :from => path_for(key, value))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_by(key, value)
|
30
|
+
find_by!(key, value)
|
31
|
+
rescue ::ActiveResource::ResourceNotFound
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
def path_for(remote_key, value)
|
38
|
+
local_key = local_attribute_name(remote_key)
|
39
|
+
route = route_for(local_key)
|
40
|
+
path = route.gsub(/:#{local_key}/, value.to_s)
|
41
|
+
if relative_path?(path)
|
42
|
+
path
|
43
|
+
else
|
44
|
+
join_url_segments(prefix, collection_name, path)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
|
53
|
+
|
54
|
+
def relative_path?(path)
|
55
|
+
path.start_with?("/") || path["://"]
|
56
|
+
end
|
57
|
+
|
58
|
+
def join_url_segments(*segments)
|
59
|
+
segments.flatten.join("/").gsub(/\/+/, "/")
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/remotable/version.rb
CHANGED
data/test/factories/tenants.rb
CHANGED
data/test/remotable_test.rb
CHANGED
@@ -1,29 +1,88 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
1
|
+
require "test_helper"
|
2
|
+
require "remotable"
|
3
|
+
require "support/active_resource"
|
4
|
+
require "active_resource_simulator"
|
5
5
|
|
6
6
|
|
7
7
|
class RemotableTest < ActiveSupport::TestCase
|
8
8
|
|
9
9
|
|
10
|
-
|
10
|
+
|
11
|
+
test "should consider :id to be the remote key if none is specified" do
|
12
|
+
assert_equal :id, RemoteWithoutKey.remote_key
|
13
|
+
assert_equal :remote_id, RemoteWithoutKey.local_key
|
14
|
+
end
|
15
|
+
|
16
|
+
test "should use a different remote_key if one is supplied" do
|
17
|
+
assert_equal :slug, RemoteWithKey.remote_key
|
18
|
+
assert_equal :slug, RemoteWithKey.local_key
|
19
|
+
end
|
20
|
+
|
21
|
+
test "should be able to generate paths for with different attributes" do
|
22
|
+
assert_equal "/api/accounts/by_slug/value.json", RemoteTenant.path_for(:slug, "value")
|
23
|
+
assert_equal "/api/accounts/by_nombre/value.json", RemoteTenant.path_for(:name, "value")
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
test "should be able to find resources by different attributes" do
|
11
29
|
new_tenant_slug = "not_found"
|
12
30
|
|
13
31
|
assert_equal 0, Tenant.where(:slug => new_tenant_slug).count,
|
14
|
-
"There's not supposed to be a Tenant with the
|
32
|
+
"There's not supposed to be a Tenant with the slug #{new_tenant_slug}."
|
15
33
|
|
16
34
|
assert_difference "Tenant.count", +1 do
|
17
|
-
|
18
|
-
|
19
|
-
:id =>
|
20
|
-
:slug =>
|
35
|
+
RemoteTenant.run_simulation do |s|
|
36
|
+
s.show(nil, {
|
37
|
+
:id => 46,
|
38
|
+
:slug => new_tenant_slug,
|
21
39
|
:church_name => "Not Found"
|
22
|
-
}
|
40
|
+
}, :path => "/api/accounts/by_slug/#{new_tenant_slug}.json")
|
23
41
|
|
24
|
-
|
42
|
+
new_tenant = Tenant.find_by_slug(new_tenant_slug)
|
43
|
+
assert_not_nil new_tenant, "A remote tenant was not found with the slug #{new_tenant_slug.inspect}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
test "should be able to find resources by different attributes and specify a path" do
|
49
|
+
new_tenant_name = "JohnnyG"
|
50
|
+
|
51
|
+
assert_equal 0, Tenant.where(:name => new_tenant_name).count,
|
52
|
+
"There's not supposed to be a Tenant with the name #{new_tenant_name}."
|
53
|
+
|
54
|
+
assert_difference "Tenant.count", +1 do
|
55
|
+
RemoteTenant.run_simulation do |s|
|
56
|
+
s.show(nil, {
|
57
|
+
:id => 46,
|
58
|
+
:slug => "not_found",
|
59
|
+
:church_name => new_tenant_name
|
60
|
+
}, :path => "/api/accounts/by_nombre/#{new_tenant_name}.json")
|
61
|
+
|
62
|
+
new_tenant = Tenant.find_by_name(new_tenant_name)
|
63
|
+
assert_not_nil new_tenant, "A remote tenant was not found with the name #{new_tenant_name.inspect}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
test "should create a record locally when fetching a new remote resource" do
|
71
|
+
new_tenant_id = 17
|
72
|
+
|
73
|
+
assert_equal 0, Tenant.where(:remote_id => new_tenant_id).count,
|
74
|
+
"There's not supposed to be a Tenant with the id #{new_tenant_id}."
|
75
|
+
|
76
|
+
assert_difference "Tenant.count", +1 do
|
77
|
+
RemoteTenant.run_simulation do |s|
|
78
|
+
s.show(new_tenant_id, {
|
79
|
+
:id => new_tenant_id,
|
80
|
+
:slug => "not_found",
|
81
|
+
:church_name => "Not Found"
|
82
|
+
})
|
25
83
|
|
26
|
-
Tenant.
|
84
|
+
new_tenant = Tenant.find_by_remote_id(new_tenant_id)
|
85
|
+
assert_not_nil new_tenant, "A remote tenant was not found with the id #{new_tenant_id.inspect}"
|
27
86
|
end
|
28
87
|
end
|
29
88
|
end
|
@@ -34,16 +93,14 @@ class RemotableTest < ActiveSupport::TestCase
|
|
34
93
|
tenant = Factory(:tenant, :expires_at => 100.years.from_now)
|
35
94
|
unexpected_name = "Totally Wonky"
|
36
95
|
|
37
|
-
|
38
|
-
|
39
|
-
:id => tenant.
|
96
|
+
RemoteTenant.run_simulation do |s|
|
97
|
+
s.show(tenant.remote_id, {
|
98
|
+
:id => tenant.remote_id,
|
40
99
|
:slug => tenant.slug,
|
41
100
|
:church_name => unexpected_name
|
42
|
-
}
|
43
|
-
|
44
|
-
s.show(nil, attrs, :path => "/api/accounts/by_slug/#{attrs[:slug]}.json")
|
101
|
+
})
|
45
102
|
|
46
|
-
tenant = Tenant.
|
103
|
+
tenant = Tenant.find_by_remote_id(tenant.remote_id)
|
47
104
|
assert_not_equal unexpected_name, tenant.name
|
48
105
|
end
|
49
106
|
end
|
@@ -54,16 +111,14 @@ class RemotableTest < ActiveSupport::TestCase
|
|
54
111
|
tenant = Factory(:tenant, :expires_at => 1.year.ago)
|
55
112
|
unexpected_name = "Totally Wonky"
|
56
113
|
|
57
|
-
|
58
|
-
|
59
|
-
:id => tenant.
|
114
|
+
RemoteTenant.run_simulation do |s|
|
115
|
+
s.show(tenant.remote_id, {
|
116
|
+
:id => tenant.remote_id,
|
60
117
|
:slug => tenant.slug,
|
61
118
|
:church_name => unexpected_name
|
62
|
-
}
|
63
|
-
|
64
|
-
s.show(nil, attrs, :path => "/api/accounts/by_slug/#{attrs[:slug]}.json")
|
119
|
+
})
|
65
120
|
|
66
|
-
tenant = Tenant.
|
121
|
+
tenant = Tenant.find_by_remote_id(tenant.remote_id)
|
67
122
|
assert_equal unexpected_name, tenant.name
|
68
123
|
end
|
69
124
|
end
|
@@ -74,11 +129,10 @@ class RemotableTest < ActiveSupport::TestCase
|
|
74
129
|
tenant = Factory(:tenant, :expires_at => 1.year.ago)
|
75
130
|
|
76
131
|
assert_difference "Tenant.count", -1 do
|
77
|
-
|
78
|
-
|
79
|
-
s.show(nil, nil, :path => "/api/accounts/by_slug/#{tenant.slug}.json", :status => 404)
|
132
|
+
RemoteTenant.run_simulation do |s|
|
133
|
+
s.show(tenant.remote_id, nil, :status => 404)
|
80
134
|
|
81
|
-
tenant = Tenant.
|
135
|
+
tenant = Tenant.find_by_remote_id(tenant.remote_id)
|
82
136
|
assert_equal nil, tenant
|
83
137
|
end
|
84
138
|
end
|
@@ -90,14 +144,13 @@ class RemotableTest < ActiveSupport::TestCase
|
|
90
144
|
tenant = Factory(:tenant)
|
91
145
|
new_name = "Totally Wonky"
|
92
146
|
|
93
|
-
|
94
|
-
s.show(
|
95
|
-
:id => tenant.
|
147
|
+
RemoteTenant.run_simulation do |s|
|
148
|
+
s.show(tenant.remote_id, {
|
149
|
+
:id => tenant.remote_id,
|
96
150
|
:slug => tenant.slug,
|
97
151
|
:church_name => tenant.name
|
98
|
-
|
99
|
-
|
100
|
-
s.update(tenant.id)
|
152
|
+
})
|
153
|
+
s.update(tenant.remote_id)
|
101
154
|
|
102
155
|
tenant.nosync = false
|
103
156
|
tenant.name = "Totally Wonky"
|
@@ -113,14 +166,13 @@ class RemotableTest < ActiveSupport::TestCase
|
|
113
166
|
tenant = Factory(:tenant)
|
114
167
|
new_name = "Totally Wonky"
|
115
168
|
|
116
|
-
|
117
|
-
s.show(
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
s.update(tenant.id, :status => 422, :body => {
|
169
|
+
RemoteTenant.run_simulation do |s|
|
170
|
+
s.show(tenant.remote_id, {
|
171
|
+
:id => tenant.remote_id,
|
172
|
+
:slug => tenant.slug,
|
173
|
+
:church_name => tenant.name
|
174
|
+
})
|
175
|
+
s.update(tenant.remote_id, :status => 422, :body => {
|
124
176
|
:errors => {:church_name => ["is already taken"]}
|
125
177
|
})
|
126
178
|
|
@@ -141,7 +193,7 @@ class RemotableTest < ActiveSupport::TestCase
|
|
141
193
|
:name => "Brand New"
|
142
194
|
})
|
143
195
|
|
144
|
-
|
196
|
+
RemoteTenant.run_simulation do |s|
|
145
197
|
s.create({
|
146
198
|
:id => 143,
|
147
199
|
:slug => tenant.slug,
|
@@ -160,7 +212,7 @@ class RemotableTest < ActiveSupport::TestCase
|
|
160
212
|
:name => "Brand New"
|
161
213
|
})
|
162
214
|
|
163
|
-
|
215
|
+
RemoteTenant.run_simulation do |s|
|
164
216
|
s.create({
|
165
217
|
:errors => {
|
166
218
|
:what => ["ever"],
|
@@ -180,14 +232,13 @@ class RemotableTest < ActiveSupport::TestCase
|
|
180
232
|
test "should destroy a record remotely when destroying one locally" do
|
181
233
|
tenant = Factory(:tenant)
|
182
234
|
|
183
|
-
|
184
|
-
s.show(
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
s.destroy(tenant.id)
|
235
|
+
RemoteTenant.run_simulation do |s|
|
236
|
+
s.show(tenant.remote_id, {
|
237
|
+
:id => tenant.remote_id,
|
238
|
+
:slug => tenant.slug,
|
239
|
+
:church_name => tenant.name
|
240
|
+
})
|
241
|
+
s.destroy(tenant.remote_id)
|
191
242
|
|
192
243
|
tenant.nosync = false
|
193
244
|
tenant.destroy
|
@@ -199,14 +250,14 @@ class RemotableTest < ActiveSupport::TestCase
|
|
199
250
|
test "should fail to destroy a record locally when failing to destroy one remotely" do
|
200
251
|
tenant = Factory(:tenant)
|
201
252
|
|
202
|
-
|
203
|
-
s.show(
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
253
|
+
RemoteTenant.run_simulation do |s|
|
254
|
+
s.show(tenant.remote_id, {
|
255
|
+
:id => tenant.remote_id,
|
256
|
+
:slug => tenant.slug,
|
257
|
+
:church_name => tenant.name
|
258
|
+
})
|
208
259
|
|
209
|
-
s.destroy(tenant.
|
260
|
+
s.destroy(tenant.remote_id, :status => 500)
|
210
261
|
|
211
262
|
tenant.nosync = false
|
212
263
|
assert_raises(ActiveResource::ServerError) do
|
@@ -1,32 +1,43 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "active_record"
|
2
|
+
require "active_resource"
|
3
|
+
|
4
|
+
|
5
|
+
class RemoteTenant < ActiveResource::Base
|
6
|
+
self.site = "http://example.com/api/"
|
7
|
+
self.element_name = "account"
|
8
|
+
self.format = :json
|
9
|
+
self.include_root_in_json = false
|
10
|
+
self.user = "username"
|
11
|
+
self.password = "password"
|
12
|
+
end
|
3
13
|
|
4
14
|
class Tenant < ActiveRecord::Base
|
5
|
-
|
15
|
+
remote_model RemoteTenant
|
16
|
+
attr_remote :slug, :church_name => :name, :id => :remote_id
|
17
|
+
find_by :slug
|
18
|
+
find_by :name => "by_nombre/:name.json"
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
class RemoteTenant2 < ActiveResource::Base
|
24
|
+
end
|
25
|
+
|
26
|
+
class RemoteWithoutKey < ActiveRecord::Base
|
27
|
+
set_table_name "tenants"
|
6
28
|
|
7
|
-
|
8
|
-
|
29
|
+
remote_model RemoteTenant2
|
30
|
+
attr_remote :id => :remote_id
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
class RemoteTenant3 < ActiveResource::Base
|
35
|
+
end
|
36
|
+
|
37
|
+
class RemoteWithKey < ActiveRecord::Base
|
38
|
+
set_table_name "tenants"
|
9
39
|
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
40
|
+
remote_model RemoteTenant3
|
41
|
+
attr_remote :slug
|
42
|
+
remote_key :slug
|
32
43
|
end
|
data/test/support/schema.rb
CHANGED
@@ -13,12 +13,12 @@
|
|
13
13
|
ActiveRecord::Schema.define(:version => 20110507152635) do
|
14
14
|
|
15
15
|
create_table "tenants", :force => true do |t|
|
16
|
-
t.string "slug"
|
17
|
-
t.string "name"
|
18
|
-
t.
|
16
|
+
t.string "slug"
|
17
|
+
t.string "name"
|
18
|
+
t.integer "remote_id"
|
19
19
|
t.datetime "created_at"
|
20
20
|
t.datetime "updated_at"
|
21
|
-
t.datetime "expires_at"
|
21
|
+
t.datetime "expires_at", :null => false
|
22
22
|
end
|
23
23
|
|
24
24
|
end
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: remotable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.
|
5
|
+
version: 0.1.1
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Robert Lail
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-08-
|
13
|
+
date: 2011-08-26 00:00:00 -05:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -115,7 +115,9 @@ files:
|
|
115
115
|
- Gemfile
|
116
116
|
- Rakefile
|
117
117
|
- lib/remotable.rb
|
118
|
+
- lib/remotable/active_record_extender.rb
|
118
119
|
- lib/remotable/active_resource_fixes.rb
|
120
|
+
- lib/remotable/adapters/active_resource.rb
|
119
121
|
- lib/remotable/core_ext.rb
|
120
122
|
- lib/remotable/core_ext/enumerable.rb
|
121
123
|
- lib/remotable/version.rb
|