remotable 0.1.2 → 0.2.0
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/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
|