remotable 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.mdown +47 -7
- data/lib/remotable.rb +99 -3
- data/lib/remotable/active_record_extender.rb +162 -68
- data/lib/remotable/active_resource_fixes.rb +15 -0
- data/lib/remotable/adapters/active_resource.rb +8 -19
- data/lib/remotable/core_ext.rb +2 -1
- data/lib/remotable/core_ext/object.rb +27 -0
- data/lib/remotable/nosync.rb +0 -1
- data/lib/remotable/validate_models.rb +23 -0
- data/lib/remotable/version.rb +1 -1
- data/lib/remotable/with_remote_model_proxy.rb +18 -0
- data/remotable.gemspec +3 -0
- data/test/active_resource_test.rb +332 -0
- data/test/bespoke_test.rb +197 -0
- data/test/factories/tenants.rb +8 -0
- data/test/remotable_test.rb +48 -256
- data/test/support/active_resource.rb +14 -12
- data/test/support/bespoke.rb +57 -0
- data/test/support/schema.rb +1 -0
- data/test/test_helper.rb +13 -0
- data/test/understanding_test.rb +16 -0
- metadata +53 -9
data/README.mdown
CHANGED
@@ -42,7 +42,9 @@ Specify the attributes of the local model that you want to keep in sync with the
|
|
42
42
|
:id => :remote_id
|
43
43
|
end
|
44
44
|
|
45
|
-
|
45
|
+
### Remote Keys
|
46
|
+
|
47
|
+
By default Remotable assumes that the local model and remote model are joined with the connection you might express in SQL this way: `local_model INNER JOIN remote_model ON local_model.id=remote_model.id`. But it is generally impractical to join on `local_model.id`.
|
46
48
|
|
47
49
|
If you specify `attr_remote :id => :remote_id`, then the join will be on `local_model.remote_id=remote_model.id`, but you can also use a different attribute as the join key:
|
48
50
|
|
@@ -56,17 +58,39 @@ If you specify `attr_remote :id => :remote_id`, then the join will be on `local_
|
|
56
58
|
|
57
59
|
Now, the join could be expressed this way: `local_model.slug=remote_model.slug`.
|
58
60
|
|
61
|
+
If you must look up a remote model with more than one attribute, you can express a composite key this way:
|
62
|
+
|
63
|
+
class Event < ActiveRecord::Base
|
64
|
+
remote_model RemoteEvent
|
65
|
+
attr_remote :calendar_id,
|
66
|
+
:id => :remote_id
|
67
|
+
remote_key [:calendar_id, :remote_id]
|
68
|
+
end
|
69
|
+
|
59
70
|
### Finders
|
60
71
|
|
61
|
-
For
|
72
|
+
For `:id` or whatever you chose to be the remote key, Remotable will create a finder method on the ActiveRecord model. These finders will _first_ look in the local database for the requested record and, if it isn't found, look for the resource remotely. If a finder finds the resource remotely, it creates a local copy and returns that.
|
62
73
|
|
63
|
-
|
74
|
+
You can create additional finder with the `find_by` method:
|
64
75
|
|
65
|
-
|
66
|
-
|
67
|
-
|
76
|
+
class Tenant < ActiveRecord::Base
|
77
|
+
remote_model RemoteTenant
|
78
|
+
attr_remote :slug,
|
79
|
+
:customer_name => :name,
|
80
|
+
:id => :remote_id
|
81
|
+
find_by :slug
|
82
|
+
find_by :name
|
83
|
+
end
|
68
84
|
|
69
|
-
|
85
|
+
Remotable will create the following methods and assume the URI for the custom finders from the attribute. The example above will create the following methods:
|
86
|
+
|
87
|
+
find_by_remote_id(...) # Looks in api_path/tenants/:id
|
88
|
+
find_by_slug(...) # Looks in api_path/tenants/by_slug/:slug
|
89
|
+
find_by_name(...) # Looks in api_path/tenants/by_name/:name
|
90
|
+
|
91
|
+
Note that the finder methods are named with the _local_ attributes.
|
92
|
+
|
93
|
+
You can specify a custom path with the `find_by` method:
|
70
94
|
|
71
95
|
class Tenant < ActiveRecord::Base
|
72
96
|
remote_model RemoteTenant
|
@@ -76,6 +100,7 @@ You can specify a custom path with the `find_by` method or you can always write
|
|
76
100
|
find_by :name, :path => "by_nombre/:name"
|
77
101
|
end
|
78
102
|
|
103
|
+
|
79
104
|
When you use `find_by`, give the name of the _local_ attribute not the remote one (if they differ). Also, the name of the symbolic part of the path should match the local attribute name as well.
|
80
105
|
|
81
106
|
### Expiration
|
@@ -91,6 +116,21 @@ Whenever a remoted record is instantiated, Remotable checks the value of its `ex
|
|
91
116
|
|
92
117
|
Remotable checks class you hand to `remote_model` to see what it inherits from. Remotable checks this to use the correct adapter with the remote model. Currently, the only adapter included in Remotable is for ActiveResource::Base.
|
93
118
|
|
119
|
+
### Custom Backends / Adapters
|
120
|
+
|
121
|
+
You can write your own backends for Remotable. Just hand `remote_model` an object which responds to two methods: `new_resource` and `find_by`.
|
122
|
+
|
123
|
+
* `new_resource` should take 0 arguments and return an uninitialized object which represents a remote resource.
|
124
|
+
* `find_by` — either `find_by(path)` or `find_by(remote_attr, value)` — should take either 1 or 2 arguments. If it takes 1 argument, it will be passed the relative path of a remote resource. If it takes 2 arguments, it will be passed an attribute name and value to be used to look up a remote resource. `find_by` should return either a single remote resource or nil (if none could be found).
|
125
|
+
|
126
|
+
The instances of a remote resource must also respond to certain methods. Instance should respond to:
|
127
|
+
|
128
|
+
* `save` (return true if successful, false if not)
|
129
|
+
* `destroy`
|
130
|
+
* `errors` (a hash of errors to be populated by an unsuccessful save)
|
131
|
+
* the getters and setters for all attributes which will be synchronized remotely
|
132
|
+
|
133
|
+
|
94
134
|
## Development
|
95
135
|
|
96
136
|
### To Do
|
data/lib/remotable.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require "remotable/version"
|
2
2
|
require "remotable/nosync"
|
3
|
+
require "remotable/validate_models"
|
4
|
+
require "remotable/with_remote_model_proxy"
|
3
5
|
|
4
6
|
|
5
7
|
# Remotable keeps a locally-stored ActiveRecord
|
@@ -30,22 +32,116 @@ require "remotable/nosync"
|
|
30
32
|
#
|
31
33
|
module Remotable
|
32
34
|
extend Nosync
|
35
|
+
extend ValidateModels
|
33
36
|
|
37
|
+
# By default, Remotable will validate the models you
|
38
|
+
# supply it via +remote_model+. You can set validate_models
|
39
|
+
# to false to skip this validation. It is recommended that
|
40
|
+
# you keep validation on in development and test environments,
|
41
|
+
# but turn it off in production.
|
42
|
+
self.validate_models = true
|
34
43
|
|
35
44
|
|
45
|
+
# == remote_model( model [optional] )
|
46
|
+
#
|
47
|
+
# When called without arguments, this method returns
|
48
|
+
# the remote model connected to this local ActiveRecord
|
49
|
+
# model.
|
50
|
+
#
|
51
|
+
# When called with an argument, it extends the ActiveRecord
|
52
|
+
# model on which it is called.
|
53
|
+
#
|
54
|
+
# <tt>model</tt> can be a class that inherits from any
|
55
|
+
# of these API consumers:
|
56
|
+
#
|
57
|
+
# * ActiveResource
|
58
|
+
#
|
59
|
+
# <tt>model</tt> can be any object that responds
|
60
|
+
# to these two methods for getting a resource:
|
61
|
+
#
|
62
|
+
# * +find_by(path)+ or +find_by(remote_attr, value)+
|
63
|
+
# +find_by+ can be defined to take either one argument or two.
|
64
|
+
# If it takes one argument, it will be passed path.
|
65
|
+
# If it takes two, it will be passed remote_attr and value.
|
66
|
+
# * +new_resource+
|
67
|
+
#
|
68
|
+
# Resources must respond to:
|
69
|
+
#
|
70
|
+
# * +save+ (return true on success and false on failure)
|
71
|
+
# * +destroy+
|
72
|
+
# * +errors+ (returning a hash of error messages by attribute)
|
73
|
+
# * getters and setters for each attribute
|
74
|
+
#
|
36
75
|
def remote_model(*args)
|
37
76
|
if args.any?
|
38
77
|
@remote_model = args.first
|
39
78
|
|
40
|
-
|
41
|
-
|
79
|
+
@__remotable_included ||= begin
|
80
|
+
require "remotable/active_record_extender"
|
81
|
+
include Remotable::ActiveRecordExtender
|
82
|
+
true
|
83
|
+
end
|
84
|
+
|
85
|
+
extend_remote_model(@remote_model)
|
86
|
+
end
|
87
|
+
@remote_model
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
|
92
|
+
def with_remote_model(model)
|
93
|
+
if block_given?
|
94
|
+
begin
|
95
|
+
original = self.remote_model
|
96
|
+
self.remote_model(model)
|
97
|
+
yield
|
98
|
+
ensure
|
99
|
+
self.remote_model(original)
|
100
|
+
end
|
42
101
|
else
|
43
|
-
|
102
|
+
WithRemoteModelProxy.new(self, model)
|
44
103
|
end
|
45
104
|
end
|
46
105
|
|
47
106
|
|
48
107
|
|
108
|
+
REQUIRED_CLASS_METHODS = [:find_by, :new_resource]
|
109
|
+
REQUIRED_INSTANCE_METHODS = [:save, :errors, :destroy]
|
110
|
+
|
111
|
+
class InvalidRemoteModel < ArgumentError; end
|
112
|
+
|
113
|
+
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def extend_remote_model(remote_model)
|
118
|
+
if remote_model.is_a?(Class) and (remote_model < ActiveResource::Base)
|
119
|
+
require "remotable/adapters/active_resource"
|
120
|
+
remote_model.send(:include, Remotable::Adapters::ActiveResource)
|
121
|
+
|
122
|
+
#
|
123
|
+
# Adapters for other API consumers can be implemented here
|
124
|
+
#
|
125
|
+
|
126
|
+
else
|
127
|
+
assert_that_remote_model_meets_api_requirements!(remote_model) if Remotable.validate_models?
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def assert_that_remote_model_meets_api_requirements!(model)
|
132
|
+
unless model.respond_to_all?(REQUIRED_CLASS_METHODS)
|
133
|
+
raise InvalidRemoteModel,
|
134
|
+
"#{model} cannot be used as a remote model with Remotable " <<
|
135
|
+
"because it does not define these methods: #{model.does_not_respond_to(REQUIRED_CLASS_METHODS).join(", ")}."
|
136
|
+
end
|
137
|
+
instance = model.new_resource
|
138
|
+
unless instance.respond_to_all?(REQUIRED_INSTANCE_METHODS)
|
139
|
+
raise InvalidRemoteModel,
|
140
|
+
"#{instance.class} cannot be used as a remote resource with Remotable " <<
|
141
|
+
"because it does not define these methods: #{instance.does_not_respond_to(REQUIRED_INSTANCE_METHODS).join(", ")}."
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
49
145
|
end
|
50
146
|
|
51
147
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require "remotable/core_ext"
|
2
2
|
require "active_support/concern"
|
3
|
+
require "active_support/core_ext/array/wrap"
|
3
4
|
|
4
5
|
|
5
6
|
module Remotable
|
@@ -22,12 +23,9 @@ module Remotable
|
|
22
23
|
|
23
24
|
validates_presence_of :expires_at
|
24
25
|
|
25
|
-
|
26
|
-
@
|
27
|
-
@
|
28
|
-
@expires_after = 1.day
|
29
|
-
|
30
|
-
extend_remote_model
|
26
|
+
@remote_attribute_map ||= default_remote_attributes.map_to_self
|
27
|
+
@local_attribute_routes ||= {}
|
28
|
+
@expires_after ||= 1.day
|
31
29
|
end
|
32
30
|
|
33
31
|
|
@@ -39,24 +37,44 @@ module Remotable
|
|
39
37
|
Remotable.nosync? || super
|
40
38
|
end
|
41
39
|
|
40
|
+
# Sets the key with which a resource is identified remotely.
|
41
|
+
# If no remote key is set, the remote key is assumed to be :id.
|
42
|
+
# Which could be explicitly set like this:
|
43
|
+
#
|
44
|
+
# remote_key :id
|
45
|
+
#
|
46
|
+
# It can can be a composite key:
|
47
|
+
#
|
48
|
+
# remote_key [:calendar_id, :id]
|
49
|
+
#
|
50
|
+
# You can also supply a path for the remote key which will
|
51
|
+
# be passed to +fetch_with+:
|
52
|
+
#
|
53
|
+
# remote_key [:calendar_id, :id], :path => "calendars/:calendar_id/events/:id"
|
54
|
+
#
|
42
55
|
def remote_key(*args)
|
43
56
|
if args.any?
|
44
|
-
remote_key = args.
|
45
|
-
|
57
|
+
remote_key = args.shift
|
58
|
+
options = args.shift || {}
|
59
|
+
|
60
|
+
# remote_key may be a composite of several attributes
|
61
|
+
# ensure that all of the attributs have been defined
|
62
|
+
Array.wrap(remote_key).each do |attribute|
|
63
|
+
raise(":#{attribute} is not the name of a remote attribute") unless remote_attribute_names.member?(attribute)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Set up a finder method for the remote_key
|
67
|
+
fetch_with(local_key(remote_key), options)
|
68
|
+
|
46
69
|
@remote_key = remote_key
|
47
|
-
fetch_with(remote_key)
|
48
|
-
remote_key
|
49
70
|
else
|
50
71
|
@remote_key || generate_default_remote_key
|
51
72
|
end
|
52
73
|
end
|
53
74
|
|
54
75
|
def expires_after(*args)
|
55
|
-
if args.any?
|
56
|
-
|
57
|
-
else
|
58
|
-
@expires_after
|
59
|
-
end
|
76
|
+
@expires_after = args.first if args.any?
|
77
|
+
@expires_after
|
60
78
|
end
|
61
79
|
|
62
80
|
def attr_remote(*attrs)
|
@@ -64,23 +82,29 @@ module Remotable
|
|
64
82
|
map = attrs.map_to_self.merge(map)
|
65
83
|
@remote_attribute_map = map
|
66
84
|
|
67
|
-
|
68
|
-
|
85
|
+
assert_that_remote_resource_responds_to_remote_attributes!(remote_model) if Remotable.validate_models?
|
86
|
+
|
87
|
+
# Reset routes
|
88
|
+
@local_attribute_routes = {}
|
69
89
|
end
|
70
90
|
|
71
|
-
def fetch_with(
|
72
|
-
|
73
|
-
@remote_attribute_routes.merge!(remote_keys_and_routes)
|
91
|
+
def fetch_with(local_key, options={})
|
92
|
+
@local_attribute_routes.merge!(local_key => options[:path])
|
74
93
|
end
|
75
94
|
alias :find_by :fetch_with
|
76
95
|
|
77
96
|
|
78
97
|
|
79
98
|
attr_reader :remote_attribute_map,
|
80
|
-
:
|
99
|
+
:local_attribute_routes
|
81
100
|
|
82
|
-
def local_key
|
83
|
-
|
101
|
+
def local_key(remote_key=nil)
|
102
|
+
remote_key ||= self.remote_key
|
103
|
+
if remote_key.is_a?(Array)
|
104
|
+
remote_key.map(&method(:local_attribute_name))
|
105
|
+
else
|
106
|
+
local_attribute_name(remote_key)
|
107
|
+
end
|
84
108
|
end
|
85
109
|
|
86
110
|
def remote_attribute_names
|
@@ -99,17 +123,18 @@ module Remotable
|
|
99
123
|
remote_attribute_map[remote_attr] || remote_attr
|
100
124
|
end
|
101
125
|
|
102
|
-
def route_for(
|
103
|
-
|
104
|
-
|
126
|
+
def route_for(remote_key)
|
127
|
+
local_key = self.local_key(remote_key)
|
128
|
+
local_attribute_routes[local_key] || default_route_for(local_key, remote_key)
|
105
129
|
end
|
106
130
|
|
107
131
|
def default_route_for(local_key, remote_key=nil)
|
132
|
+
puts "local_key: #{local_key}; remote_key: #{remote_key}"
|
108
133
|
remote_key ||= remote_attribute_name(local_key)
|
109
134
|
if remote_key.to_s == primary_key
|
110
135
|
":#{local_key}"
|
111
136
|
else
|
112
|
-
"by_#{
|
137
|
+
"by_#{local_key}/:#{local_key}"
|
113
138
|
end
|
114
139
|
end
|
115
140
|
|
@@ -128,26 +153,55 @@ module Remotable
|
|
128
153
|
|
129
154
|
|
130
155
|
|
156
|
+
def respond_to?(method_sym, include_private=false)
|
157
|
+
return true if recognize_remote_finder_method(method_sym)
|
158
|
+
super(method_sym, include_private)
|
159
|
+
end
|
160
|
+
|
131
161
|
def method_missing(method_sym, *args, &block)
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
remote_attr = remote_attribute_name(local_attr)
|
137
|
-
|
138
|
-
remote_key # Make sure we've figured out the remote
|
139
|
-
# primary key if we're evaluating a finder
|
162
|
+
method_details = recognize_remote_finder_method(method_sym)
|
163
|
+
if method_details
|
164
|
+
local_attributes = method_details[:local_attributes]
|
165
|
+
values = args
|
140
166
|
|
141
|
-
|
142
|
-
|
143
|
-
fetch_by(remote_attr, value)
|
144
|
-
|
145
|
-
raise ActiveRecord::RecordNotFound if local_resource.nil? && bang
|
146
|
-
return local_resource
|
167
|
+
unless values.length == local_attributes.length
|
168
|
+
raise ArgumentError, "#{method_sym} was called with #{values.length} but #{local_attributes.length} was expected"
|
147
169
|
end
|
170
|
+
|
171
|
+
local_resource = ((0...local_attributes.length).inject(scoped) do |scope, i|
|
172
|
+
scope.where(local_attributes[i] => values[i])
|
173
|
+
end).first || fetch_by(method_details[:remote_key], *values)
|
174
|
+
|
175
|
+
raise ActiveRecord::RecordNotFound if local_resource.nil? && (method_sym =~ /!$/)
|
176
|
+
local_resource
|
177
|
+
else
|
178
|
+
super(method_sym, *args, &block)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# If the missing method IS a Remotable finder method,
|
183
|
+
# returns the remote key (may be a composite key).
|
184
|
+
# Otherwise, returns false.
|
185
|
+
def recognize_remote_finder_method(method_sym)
|
186
|
+
method_name = method_sym.to_s
|
187
|
+
return false unless method_name =~ /find_by_([^!]*)(!?)/
|
188
|
+
|
189
|
+
local_attributes = $1.split("_and_").map(&:to_sym)
|
190
|
+
remote_attributes = local_attributes.map(&method(:remote_attribute_name))
|
191
|
+
|
192
|
+
local_key, remote_key = if local_attributes.length == 1
|
193
|
+
[local_attributes[0], remote_attributes[0]]
|
194
|
+
else
|
195
|
+
[local_attributes, remote_attributes]
|
148
196
|
end
|
149
197
|
|
150
|
-
|
198
|
+
generate_default_remote_key # <- Make sure we've figured out the remote
|
199
|
+
# primary key if we're evaluating a finder
|
200
|
+
|
201
|
+
return false unless local_attribute_routes.key?(local_key)
|
202
|
+
|
203
|
+
{ :local_attributes => local_attributes,
|
204
|
+
:remote_key => remote_key }
|
151
205
|
end
|
152
206
|
|
153
207
|
|
@@ -161,56 +215,90 @@ module Remotable
|
|
161
215
|
# Looks the resource up remotely, by the given attribute
|
162
216
|
# If the resource is found, wraps it in a new local resource
|
163
217
|
# and returns that.
|
164
|
-
def fetch_by(remote_attr,
|
165
|
-
remote_resource =
|
218
|
+
def fetch_by(remote_attr, *values)
|
219
|
+
remote_resource = find_remote_resource_by(remote_attr, *values)
|
166
220
|
remote_resource && new_from_remote(remote_resource)
|
167
221
|
end
|
168
222
|
|
223
|
+
# Looks the resource up remotely;
|
224
|
+
# Returns the remote resource.
|
225
|
+
def find_remote_resource_by(remote_attr, *values)
|
226
|
+
find_by = remote_model.method(:find_by)
|
227
|
+
case find_by.arity
|
228
|
+
when 1; find_by.call(remote_path_for(remote_attr, *values))
|
229
|
+
when 2; find_by.call(remote_attr, *values)
|
230
|
+
else
|
231
|
+
raise InvalidRemoteModel, "#{remote_model}.find_by should take either 1 or 2 parameters"
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def remote_path_for(remote_key, *values)
|
236
|
+
route = route_for(remote_key)
|
237
|
+
local_key = self.local_key(remote_key)
|
238
|
+
|
239
|
+
if remote_key.is_a?(Array)
|
240
|
+
remote_path_for_composite_key(route, local_key, values)
|
241
|
+
else
|
242
|
+
remote_path_for_simple_key(route, local_key, values.first)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def remote_path_for_simple_key(route, local_key, value)
|
247
|
+
route.gsub(/:#{local_key}/, value.to_s)
|
248
|
+
end
|
249
|
+
|
250
|
+
def remote_path_for_composite_key(route, local_key, values)
|
251
|
+
unless values.length == local_key.length
|
252
|
+
raise ArgumentError, "local_key has #{local_key.length} attributes but values has #{values.length}"
|
253
|
+
end
|
254
|
+
|
255
|
+
(0...values.length).inject(route) do |route, i|
|
256
|
+
route.gsub(/:#{local_key[i]}/, values[i].to_s)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
169
260
|
|
170
261
|
|
171
262
|
private
|
172
263
|
|
173
264
|
|
174
265
|
|
175
|
-
def
|
176
|
-
|
177
|
-
require "remotable/adapters/active_resource"
|
178
|
-
remote_model.send(:include, Remotable::Adapters::ActiveResource)
|
179
|
-
remote_model.local_model = self
|
180
|
-
|
181
|
-
# !todo
|
182
|
-
# Adapters for other API consumers can be implemented here
|
183
|
-
#
|
184
|
-
|
185
|
-
else
|
186
|
-
raise("#{remote_model} is not a recognized remote resource")
|
187
|
-
end
|
266
|
+
def default_remote_attributes
|
267
|
+
column_names - %w{id created_at updated_at expires_at}
|
188
268
|
end
|
189
269
|
|
190
270
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
271
|
+
|
272
|
+
def assert_that_remote_resource_responds_to_remote_attributes!(model)
|
273
|
+
# Skip this for ActiveResource because it won't define a method until it has
|
274
|
+
# loaded an JSON for that method
|
275
|
+
return if model.is_a?(Class) and (model < ActiveResource::Base)
|
276
|
+
|
277
|
+
instance = model.new_resource
|
278
|
+
attr_getters_and_setters = remote_attribute_names + remote_attribute_names.map {|attr| :"#{attr}="}
|
279
|
+
unless instance.respond_to_all?(attr_getters_and_setters)
|
280
|
+
raise InvalidRemoteModel,
|
281
|
+
"#{instance.class} does not respond to getters and setters " <<
|
282
|
+
"for each remote attribute (not implemented: #{instance.does_not_respond_to(attr_getters_and_setters).sort.join(", ")}).\n"
|
196
283
|
end
|
197
284
|
end
|
198
285
|
|
199
286
|
|
287
|
+
|
200
288
|
def generate_default_remote_key
|
289
|
+
return @remote_key if @remote_key
|
201
290
|
raise("No remote key supplied and :id is not a remote attribute") unless remote_attribute_names.member?(:id)
|
202
291
|
remote_key(:id)
|
203
292
|
end
|
204
293
|
|
205
294
|
|
295
|
+
|
206
296
|
def new_from_remote(remote_resource)
|
207
297
|
record = self.new
|
208
298
|
record.instance_variable_set(:@remote_resource, remote_resource)
|
209
299
|
record.pull_remote_data!
|
210
300
|
end
|
211
301
|
|
212
|
-
|
213
|
-
|
214
302
|
end
|
215
303
|
|
216
304
|
|
@@ -224,6 +312,7 @@ module Remotable
|
|
224
312
|
:local_attribute_names,
|
225
313
|
:local_attribute_name,
|
226
314
|
:expires_after,
|
315
|
+
:find_remote_resource_by,
|
227
316
|
:to => "self.class"
|
228
317
|
|
229
318
|
def expired?
|
@@ -256,7 +345,10 @@ module Remotable
|
|
256
345
|
|
257
346
|
def fetch_remote_resource
|
258
347
|
fetch_value = self[local_key]
|
259
|
-
|
348
|
+
# puts "local_key", local_key.inspect, "",
|
349
|
+
# "remote_key", remote_key.inspect, "",
|
350
|
+
# "fetch_value", fetch_value
|
351
|
+
find_remote_resource_by(remote_key, fetch_value)
|
260
352
|
end
|
261
353
|
|
262
354
|
def merge_remote_data!(remote_resource)
|
@@ -285,7 +377,7 @@ module Remotable
|
|
285
377
|
end
|
286
378
|
|
287
379
|
def create_remote_resource
|
288
|
-
@remote_resource = remote_model.
|
380
|
+
@remote_resource = remote_model.new_resource
|
289
381
|
merge_local_data(@remote_resource)
|
290
382
|
|
291
383
|
if @remote_resource.save
|
@@ -323,8 +415,10 @@ module Remotable
|
|
323
415
|
|
324
416
|
|
325
417
|
def merge_remote_errors(errors)
|
326
|
-
errors.each do |attribute,
|
327
|
-
|
418
|
+
errors.each do |attribute, messages|
|
419
|
+
Array.wrap(messages).each do |message|
|
420
|
+
self.errors.add(local_attribute_name(attribute), message)
|
421
|
+
end
|
328
422
|
end
|
329
423
|
self
|
330
424
|
end
|