remotable 0.3.0 → 0.4.0.beta2
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile.lock +4 -1
- data/README.mdown +6 -6
- data/lib/remotable.rb +33 -32
- data/lib/remotable/active_record_extender.rb +186 -166
- data/lib/remotable/active_resource_fixes.rb +16 -16
- data/lib/remotable/adapters/active_resource.rb +45 -33
- data/lib/remotable/core_ext/enumerable.rb +2 -2
- data/lib/remotable/core_ext/object.rb +6 -6
- data/lib/remotable/core_ext/uri.rb +4 -4
- data/lib/remotable/errors.rb +2 -0
- data/lib/remotable/logger_wrapper.rb +10 -10
- data/lib/remotable/nosync.rb +8 -8
- data/lib/remotable/null_remote.rb +13 -13
- data/lib/remotable/validate_models.rb +6 -6
- data/lib/remotable/version.rb +1 -1
- data/lib/remotable/with_remote_model_proxy.rb +4 -4
- data/test/active_resource_test.rb +176 -126
- data/test/bespoke_test.rb +50 -50
- data/test/nosync_test.rb +16 -16
- data/test/null_remote_test.rb +22 -22
- data/test/remotable_test.rb +35 -35
- data/test/support/active_resource.rb +5 -5
- data/test/support/bespoke.rb +13 -13
- data/test/support/concurrently.rb +62 -0
- data/test/support/null.rb +1 -1
- data/test/support/schema.rb +3 -0
- data/test/test_helper.rb +1 -1
- data/test/understanding_test.rb +2 -2
- metadata +37 -34
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50bbb2463866bea7de985bdc5375aaac0b7c715b
|
4
|
+
data.tar.gz: ee16e4675c4c3c8a7cd4420e9180a0d532575a80
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 32767f6332fe922693bf2265c7dff1b73b916c491800e792b44e4d4104050335fcf562a5c729bf1ba9f1306d3bf14d2a0590e4211c98de7bf22a0f4a7865d26f
|
7
|
+
data.tar.gz: 46c8039a5195b3a629761798b359c228ce8448caa72b59686402182f17ea70773fe8191ee5c80fcd13a3cd8af9ebbf73947ae4c53317ceef78fa4fa9ae730d46
|
data/Gemfile.lock
CHANGED
data/README.mdown
CHANGED
@@ -71,15 +71,15 @@ If you must look up a remote model with more than one attribute, you can express
|
|
71
71
|
|
72
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.
|
73
73
|
|
74
|
-
You can create additional finder with the `
|
74
|
+
You can create additional finder with the `fetch_with` method:
|
75
75
|
|
76
76
|
class Tenant < ActiveRecord::Base
|
77
77
|
remote_model RemoteTenant
|
78
78
|
attr_remote :slug,
|
79
79
|
:customer_name => :name,
|
80
80
|
:id => :remote_id
|
81
|
-
|
82
|
-
|
81
|
+
fetch_with :slug
|
82
|
+
fetch_with :name
|
83
83
|
end
|
84
84
|
|
85
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:
|
@@ -90,18 +90,18 @@ Remotable will create the following methods and assume the URI for the custom fi
|
|
90
90
|
|
91
91
|
Note that the finder methods are named with the _local_ attributes.
|
92
92
|
|
93
|
-
You can specify a custom path with the `
|
93
|
+
You can specify a custom path with the `fetch_with` method:
|
94
94
|
|
95
95
|
class Tenant < ActiveRecord::Base
|
96
96
|
remote_model RemoteTenant
|
97
97
|
attr_remote :slug,
|
98
98
|
:customer_name => :name,
|
99
99
|
:id => :remote_id
|
100
|
-
|
100
|
+
fetch_with :name, :path => "by_nombre/:name"
|
101
101
|
end
|
102
102
|
|
103
103
|
|
104
|
-
When you use `
|
104
|
+
When you use `fetch_with`, 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.
|
105
105
|
|
106
106
|
### Expiration
|
107
107
|
|
data/lib/remotable.rb
CHANGED
@@ -36,27 +36,27 @@ require "remotable/logger_wrapper"
|
|
36
36
|
module Remotable
|
37
37
|
extend Nosync
|
38
38
|
extend ValidateModels
|
39
|
-
|
39
|
+
|
40
40
|
# By default, Remotable will validate the models you
|
41
41
|
# supply it via +remote_model+. You can set validate_models
|
42
42
|
# to false to skip this validation. It is recommended that
|
43
43
|
# you keep validation on in development and test environments,
|
44
44
|
# but turn it off in production.
|
45
45
|
self.validate_models = true
|
46
|
-
|
46
|
+
|
47
47
|
# Logger
|
48
48
|
def self.logger; @logger ||= LoggerWrapper.new(FakeLogger.new); end
|
49
49
|
def self.logger=(logger); @logger = LoggerWrapper.new(logger); end
|
50
|
-
|
50
|
+
|
51
51
|
class << self
|
52
52
|
attr_accessor :log_level
|
53
53
|
Remotable.log_level = :debug
|
54
54
|
end
|
55
|
-
|
56
|
-
|
57
|
-
|
55
|
+
|
56
|
+
|
57
|
+
|
58
58
|
# == remote_model( model [optional] )
|
59
|
-
#
|
59
|
+
#
|
60
60
|
# When called without arguments, this method returns
|
61
61
|
# the remote model connected to this local ActiveRecord
|
62
62
|
# model.
|
@@ -68,7 +68,7 @@ module Remotable
|
|
68
68
|
# of these API consumers:
|
69
69
|
#
|
70
70
|
# * ActiveResource
|
71
|
-
#
|
71
|
+
#
|
72
72
|
# <tt>model</tt> can be any object that responds
|
73
73
|
# to these two methods for getting a resource:
|
74
74
|
#
|
@@ -78,7 +78,7 @@ module Remotable
|
|
78
78
|
# If it takes one argument, it will be passed path.
|
79
79
|
# If it takes two, it will be passed remote_attr and value.
|
80
80
|
# * (Optional) +find_by_for_local(local_record, remote_key, fetch_value)+
|
81
|
-
#
|
81
|
+
#
|
82
82
|
# Resources must respond to:
|
83
83
|
#
|
84
84
|
# * +save+ (return true on success and false on failure)
|
@@ -89,20 +89,20 @@ module Remotable
|
|
89
89
|
def remote_model(*args)
|
90
90
|
if args.length >= 1
|
91
91
|
@remote_model = args.first
|
92
|
-
|
92
|
+
|
93
93
|
@__remotable_included ||= begin
|
94
94
|
require "remotable/active_record_extender"
|
95
95
|
include Remotable::ActiveRecordExtender
|
96
96
|
true
|
97
97
|
end
|
98
|
-
|
98
|
+
|
99
99
|
extend_remote_model(@remote_model) if @remote_model
|
100
100
|
end
|
101
101
|
@remote_model
|
102
102
|
end
|
103
|
-
|
104
|
-
|
105
|
-
|
103
|
+
|
104
|
+
|
105
|
+
|
106
106
|
def with_remote_model(model)
|
107
107
|
if block_given?
|
108
108
|
begin
|
@@ -116,50 +116,51 @@ module Remotable
|
|
116
116
|
WithRemoteModelProxy.new(self, model)
|
117
117
|
end
|
118
118
|
end
|
119
|
-
|
120
|
-
|
121
|
-
|
119
|
+
|
120
|
+
|
121
|
+
|
122
122
|
REQUIRED_CLASS_METHODS = [:find_by, :new_resource]
|
123
123
|
REQUIRED_INSTANCE_METHODS = [:save, :errors, :destroy]
|
124
|
-
|
124
|
+
|
125
125
|
class InvalidRemoteModel < ArgumentError; end
|
126
|
-
|
126
|
+
|
127
127
|
class FakeLogger
|
128
|
-
|
128
|
+
|
129
129
|
def write(s)
|
130
130
|
puts s
|
131
131
|
end
|
132
|
-
|
132
|
+
|
133
133
|
alias :debug :write
|
134
134
|
alias :info :write
|
135
135
|
alias :warn :write
|
136
136
|
alias :error :write
|
137
|
-
|
137
|
+
|
138
138
|
end
|
139
|
-
|
140
|
-
|
139
|
+
|
140
|
+
|
141
141
|
def self.http_format_time(time)
|
142
|
+
return "" unless time
|
142
143
|
time.utc.strftime("%a, %e %b %Y %H:%M:%S %Z")
|
143
144
|
end
|
144
|
-
|
145
|
-
|
146
|
-
|
145
|
+
|
146
|
+
|
147
|
+
|
147
148
|
private
|
148
|
-
|
149
|
+
|
149
150
|
def extend_remote_model(remote_model)
|
150
151
|
if remote_model.is_a?(Class) and (remote_model < ActiveResource::Base)
|
151
152
|
require "remotable/adapters/active_resource"
|
152
153
|
remote_model.send(:include, Remotable::Adapters::ActiveResource)
|
153
|
-
|
154
|
+
|
154
155
|
#
|
155
156
|
# Adapters for other API consumers can be implemented here
|
156
157
|
#
|
157
|
-
|
158
|
+
|
158
159
|
else
|
159
160
|
assert_that_remote_model_meets_api_requirements!(remote_model) if Remotable.validate_models?
|
160
161
|
end
|
161
162
|
end
|
162
|
-
|
163
|
+
|
163
164
|
def assert_that_remote_model_meets_api_requirements!(model)
|
164
165
|
unless model.respond_to_all?(REQUIRED_CLASS_METHODS)
|
165
166
|
raise InvalidRemoteModel,
|
@@ -173,7 +174,7 @@ private
|
|
173
174
|
"because it does not define these methods: #{instance.does_not_respond_to(REQUIRED_INSTANCE_METHODS).join(", ")}."
|
174
175
|
end
|
175
176
|
end
|
176
|
-
|
177
|
+
|
177
178
|
end
|
178
179
|
|
179
180
|
|
@@ -8,23 +8,23 @@ module Remotable
|
|
8
8
|
module ActiveRecordExtender
|
9
9
|
extend ActiveSupport::Concern
|
10
10
|
include Nosync
|
11
|
-
|
11
|
+
|
12
12
|
def nosync?
|
13
13
|
return super if nosync_value?
|
14
14
|
self.class.nosync?
|
15
15
|
end
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
|
17
|
+
|
18
|
+
|
19
19
|
included do
|
20
20
|
before_update :update_remote_resource, :unless => :nosync?
|
21
21
|
before_create :create_remote_resource, :unless => :nosync?
|
22
22
|
before_destroy :destroy_remote_resource, :unless => :nosync?
|
23
|
-
|
23
|
+
|
24
24
|
before_validation :initialize_expiration_date, :on => :create
|
25
|
-
|
25
|
+
|
26
26
|
validates_presence_of :expires_at
|
27
|
-
|
27
|
+
|
28
28
|
@remote_attribute_map ||= default_remote_attributes.map_to_self
|
29
29
|
@local_attribute_routes ||= {}
|
30
30
|
@expires_after ||= 1.day
|
@@ -37,18 +37,18 @@ module Remotable
|
|
37
37
|
:destroy => 2 # when we're destroying a remote resource
|
38
38
|
}
|
39
39
|
end
|
40
|
-
|
41
|
-
|
42
|
-
|
40
|
+
|
41
|
+
|
42
|
+
|
43
43
|
module ClassMethods
|
44
44
|
include Nosync
|
45
|
-
|
45
|
+
|
46
46
|
def nosync?
|
47
47
|
return true if remote_model.nil?
|
48
48
|
return super if nosync_value?
|
49
49
|
Remotable.nosync?
|
50
50
|
end
|
51
|
-
|
51
|
+
|
52
52
|
# Sets the key with which a resource is identified remotely.
|
53
53
|
# If no remote key is set, the remote key is assumed to be :id.
|
54
54
|
# Which could be explicitly set like this:
|
@@ -68,34 +68,34 @@ module Remotable
|
|
68
68
|
if args.any?
|
69
69
|
remote_key = args.shift
|
70
70
|
options = args.shift || {}
|
71
|
-
|
71
|
+
|
72
72
|
# remote_key may be a composite of several attributes
|
73
73
|
# ensure that all of the attributs have been defined
|
74
74
|
Array.wrap(remote_key).each do |attribute|
|
75
75
|
raise(":#{attribute} is not the name of a remote attribute") unless remote_attribute_names.member?(attribute)
|
76
76
|
end
|
77
|
-
|
77
|
+
|
78
78
|
# Set up a finder method for the remote_key
|
79
79
|
fetch_with(local_key(remote_key), options)
|
80
|
-
|
80
|
+
|
81
81
|
@remote_key = remote_key
|
82
82
|
else
|
83
83
|
@remote_key || generate_default_remote_key
|
84
84
|
end
|
85
85
|
end
|
86
|
-
|
86
|
+
|
87
87
|
def expires_after(*args)
|
88
88
|
@expires_after = args.first if args.any?
|
89
89
|
@expires_after
|
90
90
|
end
|
91
|
-
|
91
|
+
|
92
92
|
def attr_remote(*attrs)
|
93
93
|
map = attrs.extract_options!
|
94
94
|
map = attrs.map_to_self.merge(map)
|
95
95
|
@remote_attribute_map = map
|
96
96
|
@local_attribute_routes = {} # reset routes
|
97
97
|
end
|
98
|
-
|
98
|
+
|
99
99
|
def remote_timeout(*args)
|
100
100
|
if args.any?
|
101
101
|
@remote_timeout = n = args.first
|
@@ -103,17 +103,16 @@ module Remotable
|
|
103
103
|
end
|
104
104
|
@remote_timeout
|
105
105
|
end
|
106
|
-
|
106
|
+
|
107
107
|
def fetch_with(local_key, options={})
|
108
108
|
@local_attribute_routes.merge!(local_key => options[:path])
|
109
109
|
end
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
110
|
+
|
111
|
+
|
112
|
+
|
114
113
|
attr_reader :remote_attribute_map,
|
115
114
|
:local_attribute_routes
|
116
|
-
|
115
|
+
|
117
116
|
def local_key(remote_key=nil)
|
118
117
|
remote_key ||= self.remote_key
|
119
118
|
if remote_key.is_a?(Array)
|
@@ -122,28 +121,28 @@ module Remotable
|
|
122
121
|
local_attribute_name(remote_key)
|
123
122
|
end
|
124
123
|
end
|
125
|
-
|
124
|
+
|
126
125
|
def remote_attribute_names
|
127
126
|
remote_attribute_map.keys
|
128
127
|
end
|
129
|
-
|
128
|
+
|
130
129
|
def local_attribute_names
|
131
130
|
remote_attribute_map.values
|
132
131
|
end
|
133
|
-
|
132
|
+
|
134
133
|
def remote_attribute_name(local_attr)
|
135
134
|
remote_attribute_map.key(local_attr) || local_attr
|
136
135
|
end
|
137
|
-
|
136
|
+
|
138
137
|
def local_attribute_name(remote_attr)
|
139
138
|
remote_attribute_map[remote_attr] || remote_attr
|
140
139
|
end
|
141
|
-
|
140
|
+
|
142
141
|
def route_for(remote_key)
|
143
142
|
local_key = self.local_key(remote_key)
|
144
143
|
local_attribute_routes[local_key] || default_route_for(local_key, remote_key)
|
145
144
|
end
|
146
|
-
|
145
|
+
|
147
146
|
def default_route_for(local_key, remote_key=nil)
|
148
147
|
remote_key ||= remote_attribute_name(local_key)
|
149
148
|
if remote_key.to_s == primary_key
|
@@ -152,9 +151,9 @@ module Remotable
|
|
152
151
|
"by_#{local_key}/:#{local_key}"
|
153
152
|
end
|
154
153
|
end
|
155
|
-
|
156
|
-
|
157
|
-
|
154
|
+
|
155
|
+
|
156
|
+
|
158
157
|
# !nb: this method is called when associations are loaded
|
159
158
|
# so you can use the remoted record in associations.
|
160
159
|
def instantiate(*args)
|
@@ -163,87 +162,101 @@ module Remotable
|
|
163
162
|
begin
|
164
163
|
Remotable.logger.debug "[remotable:#{name.underscore}:instantiate](#{record.fetch_value.inspect}) expired #{record.expires_at}"
|
165
164
|
record.pull_remote_data!
|
166
|
-
record = nil if record.destroyed?
|
167
165
|
rescue Remotable::TimeoutError
|
168
166
|
report_ignored_timeout_error($!)
|
167
|
+
rescue Remotable::NetworkError
|
168
|
+
report_ignored_network_error($!)
|
169
169
|
rescue Remotable::ServiceUnavailableError
|
170
170
|
report_ignored_503_error($!)
|
171
171
|
end
|
172
172
|
end
|
173
173
|
record
|
174
174
|
end
|
175
|
-
|
175
|
+
|
176
176
|
def report_ignored_timeout_error(error)
|
177
177
|
Remotable.logger.error "[remotable:#{name.underscore}:instantiate] #{error.message}"
|
178
178
|
end
|
179
|
-
|
179
|
+
|
180
|
+
def report_ignored_network_error(error)
|
181
|
+
Remotable.logger.error "[remotable:#{name.underscore}:instantiate] #{error.message}"
|
182
|
+
end
|
183
|
+
|
180
184
|
def report_ignored_503_error(error)
|
181
185
|
Remotable.logger.error "[remotable:#{name.underscore}:instantiate] #{error.message}"
|
182
186
|
end
|
183
|
-
|
184
|
-
|
185
|
-
|
187
|
+
|
188
|
+
|
189
|
+
|
186
190
|
# !todo: create these methods on an anonymous module and mix it in
|
187
|
-
|
191
|
+
|
188
192
|
def respond_to?(method_sym, include_private=false)
|
189
193
|
return true if recognize_remote_finder_method(method_sym)
|
190
194
|
super(method_sym, include_private)
|
191
195
|
end
|
192
|
-
|
193
|
-
def method_missing(method_sym, *
|
196
|
+
|
197
|
+
def method_missing(method_sym, *values, &block)
|
194
198
|
method_details = recognize_remote_finder_method(method_sym)
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
199
|
+
return super(method_sym, *values, &block) unless method_details
|
200
|
+
|
201
|
+
local_attributes = method_details[:local_attributes]
|
202
|
+
raise ArgumentError, "#{method_sym} was called with #{values.length} but #{local_attributes.length} was expected" unless values.length == local_attributes.length
|
203
|
+
|
204
|
+
local_resource = __remotable_lookup(method_details[:remote_key], local_attributes, values)
|
205
|
+
local_resource = nil if local_resource && local_resource.destroyed?
|
206
|
+
raise ActiveRecord::RecordNotFound if local_resource.nil? && (method_sym =~ /!$/)
|
207
|
+
local_resource
|
208
|
+
end
|
209
|
+
|
210
|
+
def __remotable_lookup(key, local_attributes, values)
|
211
|
+
__remotable_local_lookup(local_attributes, values) || fetch_by(key, *values)
|
212
|
+
rescue ActiveRecord::RecordNotUnique
|
213
|
+
__remotable_local_lookup(local_attributes, values)
|
214
|
+
end
|
215
|
+
|
216
|
+
def __remotable_local_lookup(local_attributes, values)
|
217
|
+
(0...local_attributes.length)
|
218
|
+
.inject(self) { |scope, i| scope.where(local_attributes[i] => values[i]) }
|
219
|
+
.limit(1).first
|
212
220
|
end
|
213
|
-
|
221
|
+
|
214
222
|
# If the missing method IS a Remotable finder method,
|
215
223
|
# returns the remote key (may be a composite key).
|
216
224
|
# Otherwise, returns false.
|
217
225
|
def recognize_remote_finder_method(method_sym)
|
218
226
|
method_name = method_sym.to_s
|
219
227
|
return false unless method_name =~ /find_by_([^!]*)(!?)/
|
220
|
-
|
228
|
+
|
221
229
|
local_attributes = $1.split("_and_").map(&:to_sym)
|
222
230
|
remote_attributes = local_attributes.map(&method(:remote_attribute_name))
|
223
|
-
|
231
|
+
|
224
232
|
local_key, remote_key = if local_attributes.length == 1
|
225
233
|
[local_attributes[0], remote_attributes[0]]
|
226
234
|
else
|
227
235
|
[local_attributes, remote_attributes]
|
228
236
|
end
|
229
|
-
|
237
|
+
|
230
238
|
generate_default_remote_key # <- Make sure we've figured out the remote
|
231
239
|
# primary key if we're evaluating a finder
|
232
|
-
|
240
|
+
|
233
241
|
return false unless local_attribute_routes.key?(local_key)
|
234
|
-
|
242
|
+
|
235
243
|
{ :local_attributes => local_attributes,
|
236
244
|
:remote_key => remote_key }
|
237
245
|
end
|
238
|
-
|
239
|
-
|
240
|
-
|
246
|
+
|
247
|
+
|
248
|
+
|
241
249
|
def expire_all!
|
242
|
-
update_all(
|
250
|
+
update_all(expires_at: 1.day.ago)
|
243
251
|
end
|
244
|
-
|
245
|
-
|
246
|
-
|
252
|
+
|
253
|
+
def sync_all!
|
254
|
+
expire_all!
|
255
|
+
all.to_a
|
256
|
+
end
|
257
|
+
|
258
|
+
|
259
|
+
|
247
260
|
# Looks the resource up remotely, by the given attribute
|
248
261
|
# If the resource is found, wraps it in a new local resource
|
249
262
|
# and returns that.
|
@@ -251,11 +264,11 @@ module Remotable
|
|
251
264
|
remote_resource = find_remote_resource_by(remote_attr, *values)
|
252
265
|
remote_resource && new_from_remote(remote_resource)
|
253
266
|
end
|
254
|
-
|
267
|
+
|
255
268
|
def find_remote_resource_by(remote_attr, *values)
|
256
269
|
invoke_remote_model_find_by(remote_attr, *values)
|
257
270
|
end
|
258
|
-
|
271
|
+
|
259
272
|
def find_remote_resource_for_local_by(local_resource, remote_attr, *values)
|
260
273
|
if remote_model.respond_to?(:find_by_for_local)
|
261
274
|
invoke_remote_model_find_by_for_local(local_resource, remote_attr, *values)
|
@@ -263,10 +276,10 @@ module Remotable
|
|
263
276
|
invoke_remote_model_find_by(remote_attr, *values)
|
264
277
|
end
|
265
278
|
end
|
266
|
-
|
279
|
+
|
267
280
|
def invoke_remote_model_find_by(remote_attr, *values)
|
268
281
|
remote_set_timeout :fetch
|
269
|
-
|
282
|
+
|
270
283
|
find_by = remote_model.method(:find_by)
|
271
284
|
case find_by.arity
|
272
285
|
when 1; find_by.call(remote_path_for(remote_attr, *values))
|
@@ -275,10 +288,10 @@ module Remotable
|
|
275
288
|
raise InvalidRemoteModel, "#{remote_model}.find_by should take either 1 or 2 parameters"
|
276
289
|
end
|
277
290
|
end
|
278
|
-
|
291
|
+
|
279
292
|
def invoke_remote_model_find_by_for_local(local_resource, remote_attr, *values)
|
280
293
|
remote_set_timeout :pull
|
281
|
-
|
294
|
+
|
282
295
|
find_by_for_local = remote_model.method(:find_by_for_local)
|
283
296
|
case find_by_for_local.arity
|
284
297
|
when 2; find_by_for_local.call(local_resource, remote_path_for(remote_attr, *values))
|
@@ -287,57 +300,57 @@ module Remotable
|
|
287
300
|
raise InvalidRemoteModel, "#{remote_model}.find_by_for_local should take either 2 or 3 parameters"
|
288
301
|
end
|
289
302
|
end
|
290
|
-
|
291
|
-
|
292
|
-
|
303
|
+
|
304
|
+
|
305
|
+
|
293
306
|
def remote_path_for(remote_key, *values)
|
294
307
|
route = route_for(remote_key)
|
295
308
|
local_key = self.local_key(remote_key)
|
296
|
-
|
309
|
+
|
297
310
|
if remote_key.is_a?(Array)
|
298
311
|
remote_path_for_composite_key(route, local_key, values)
|
299
312
|
else
|
300
313
|
remote_path_for_simple_key(route, local_key, values.first)
|
301
314
|
end
|
302
315
|
end
|
303
|
-
|
316
|
+
|
304
317
|
def remote_path_for_simple_key(route, local_key, value)
|
305
|
-
route.gsub(/:#{local_key}/, value.to_s)
|
318
|
+
route.gsub(/:#{local_key}/, URI.escape(value.to_s))
|
306
319
|
end
|
307
|
-
|
320
|
+
|
308
321
|
def remote_path_for_composite_key(route, local_key, values)
|
309
322
|
values.flatten!
|
310
323
|
unless values.length == local_key.length
|
311
324
|
raise ArgumentError, "local_key has #{local_key.length} attributes but values has #{values.length}"
|
312
325
|
end
|
313
|
-
|
326
|
+
|
314
327
|
(0...values.length).inject(route) do |route, i|
|
315
|
-
route.gsub(/:#{local_key[i]}/, values[i].to_s)
|
328
|
+
route.gsub(/:#{local_key[i]}/, URI.escape(values[i].to_s))
|
316
329
|
end
|
317
330
|
end
|
318
|
-
|
319
|
-
|
320
|
-
|
331
|
+
|
332
|
+
|
333
|
+
|
321
334
|
def all_by_remote
|
322
335
|
find_by_remote_query(:all)
|
323
336
|
end
|
324
|
-
|
337
|
+
|
325
338
|
def find_by_remote_query(remote_method_name, *args)
|
326
339
|
remote_set_timeout :list
|
327
340
|
remote_resources = Array.wrap(remote_model.send(remote_method_name, *args))
|
328
341
|
map_remote_resources_to_local(remote_resources)
|
329
342
|
end
|
330
|
-
|
343
|
+
|
331
344
|
def map_remote_resources_to_local(remote_resources)
|
332
345
|
return [] if remote_resources.nil? || remote_resources.empty?
|
333
|
-
|
346
|
+
|
334
347
|
local_resources = nosync { fetch_corresponding_local_resources(remote_resources).to_a }
|
335
|
-
|
348
|
+
|
336
349
|
# Ensure a corresponding local resource for
|
337
350
|
# each remote resource; return the set of
|
338
351
|
# local resources.
|
339
352
|
remote_resources.map do |remote_resource|
|
340
|
-
|
353
|
+
|
341
354
|
# Get the specific local resource that
|
342
355
|
# corresponds to this remote one.
|
343
356
|
local_resource = local_resources.detect { |local_resource|
|
@@ -346,7 +359,7 @@ module Remotable
|
|
346
359
|
local_resource.send(local_attr) == remote_resource[remote_attr]
|
347
360
|
}
|
348
361
|
}
|
349
|
-
|
362
|
+
|
350
363
|
# If a local counterpart to this remote value
|
351
364
|
# exists, update the local resource and return it.
|
352
365
|
# If not, create a local counterpart and return it.
|
@@ -358,47 +371,47 @@ module Remotable
|
|
358
371
|
end
|
359
372
|
end
|
360
373
|
end
|
361
|
-
|
374
|
+
|
362
375
|
def fetch_corresponding_local_resources(remote_resources)
|
363
376
|
conditions = Array.wrap(remote_key).each_with_object({}) do |remote_attr, query|
|
364
377
|
local_attr = local_attribute_name(remote_attr)
|
365
378
|
query[local_attr] = remote_resources.map { |resource| resource[remote_attr] }
|
366
379
|
end
|
367
|
-
|
380
|
+
|
368
381
|
where(conditions)
|
369
382
|
end
|
370
|
-
|
371
|
-
|
372
|
-
|
383
|
+
|
384
|
+
|
385
|
+
|
373
386
|
private
|
374
|
-
|
387
|
+
|
375
388
|
def remote_set_timeout(mode)
|
376
389
|
remote_model.timeout = remote_timeout[mode] if remote_model.respond_to?(:timeout=)
|
377
390
|
end
|
378
|
-
|
379
|
-
|
391
|
+
|
392
|
+
|
380
393
|
def default_remote_attributes
|
381
394
|
column_names - %w{id created_at updated_at expires_at}
|
382
395
|
end
|
383
|
-
|
384
|
-
|
396
|
+
|
397
|
+
|
385
398
|
def generate_default_remote_key
|
386
399
|
return @remote_key if @remote_key
|
387
400
|
raise("No remote key supplied and :id is not a remote attribute") unless remote_attribute_names.member?(:id)
|
388
401
|
remote_key(:id)
|
389
402
|
end
|
390
|
-
|
391
|
-
|
403
|
+
|
404
|
+
|
392
405
|
def new_from_remote(remote_resource)
|
393
406
|
record = self.new
|
394
407
|
record.instance_variable_set(:@remote_resource, remote_resource)
|
395
408
|
record.pull_remote_data!
|
396
409
|
end
|
397
|
-
|
410
|
+
|
398
411
|
end
|
399
|
-
|
400
|
-
|
401
|
-
|
412
|
+
|
413
|
+
|
414
|
+
|
402
415
|
delegate :local_key,
|
403
416
|
:remote_key,
|
404
417
|
:remote_model,
|
@@ -409,23 +422,23 @@ module Remotable
|
|
409
422
|
:local_attribute_name,
|
410
423
|
:expires_after,
|
411
424
|
:to => "self.class"
|
412
|
-
|
425
|
+
|
413
426
|
def expired?
|
414
427
|
expires_at.nil? || expires_at < Time.now
|
415
428
|
end
|
416
|
-
|
429
|
+
|
417
430
|
def expired!
|
418
431
|
update_attribute(:expires_at, 1.day.ago)
|
419
432
|
end
|
420
|
-
|
421
|
-
|
422
|
-
|
433
|
+
|
434
|
+
|
435
|
+
|
423
436
|
def accepts_not_modified?
|
424
|
-
respond_to?(:
|
437
|
+
respond_to?(:remote_updated_at)
|
425
438
|
end
|
426
|
-
|
427
|
-
|
428
|
-
|
439
|
+
|
440
|
+
|
441
|
+
|
429
442
|
def pull_remote_data!
|
430
443
|
if remote_resource
|
431
444
|
merge_remote_data!(remote_resource)
|
@@ -433,19 +446,19 @@ module Remotable
|
|
433
446
|
remote_model_has_been_destroyed!
|
434
447
|
end
|
435
448
|
end
|
436
|
-
|
437
|
-
|
438
|
-
|
449
|
+
|
450
|
+
|
451
|
+
|
439
452
|
def remote_resource
|
440
453
|
@remote_resource ||= fetch_remote_resource
|
441
454
|
end
|
442
|
-
|
455
|
+
|
443
456
|
def any_remote_changes?
|
444
457
|
(changed.map(&:to_sym) & local_attribute_names).any?
|
445
458
|
end
|
446
|
-
|
447
|
-
|
448
|
-
|
459
|
+
|
460
|
+
|
461
|
+
|
449
462
|
def fetch_value
|
450
463
|
if local_key.is_a?(Array)
|
451
464
|
local_key.map(&method(:send))
|
@@ -453,42 +466,42 @@ module Remotable
|
|
453
466
|
send(local_key)
|
454
467
|
end
|
455
468
|
end
|
456
|
-
|
457
|
-
|
458
|
-
|
469
|
+
|
470
|
+
|
471
|
+
|
459
472
|
private
|
460
|
-
|
473
|
+
|
461
474
|
def fetch_remote_resource
|
462
475
|
fetch_value && find_remote_resource_by(remote_key, fetch_value)
|
463
476
|
end
|
464
|
-
|
477
|
+
|
465
478
|
def find_remote_resource_by(remote_key, fetch_value)
|
466
479
|
result = nil
|
467
480
|
ms = Benchmark.ms do
|
468
481
|
result = self.class.find_remote_resource_for_local_by(self, remote_key, fetch_value)
|
469
482
|
end
|
470
|
-
Remotable.logger.info "[remotable:#{self.class.name.underscore}:find_remote_resource_by](#{fetch_value.inspect}) %.1fms" % [ ms ]
|
483
|
+
Remotable.logger.info "[remotable:#{self.class.name.underscore}:find_remote_resource_by](#{fetch_value.inspect})" << " %.1fms" % [ ms ]
|
471
484
|
result
|
472
485
|
end
|
473
|
-
|
486
|
+
|
474
487
|
def merge_remote_data!(remote_resource)
|
475
488
|
merge_remote_data(remote_resource)
|
476
489
|
reset_expiration_date
|
477
490
|
nosync { save! }
|
478
491
|
self
|
479
492
|
end
|
480
|
-
|
493
|
+
|
481
494
|
def remote_model_has_been_destroyed!
|
482
495
|
Remotable.logger.info "[remotable:#{self.class.name.underscore}:remote_model_has_been_destroyed!](#{fetch_value.inspect})"
|
483
496
|
nosync { destroy }
|
484
497
|
end
|
485
|
-
|
486
|
-
|
487
|
-
|
498
|
+
|
499
|
+
|
500
|
+
|
488
501
|
def update_remote_resource
|
489
502
|
if any_remote_changes? && remote_resource
|
490
503
|
merge_local_data(remote_resource, true)
|
491
|
-
|
504
|
+
|
492
505
|
remote_set_timeout :update
|
493
506
|
if remote_resource.save
|
494
507
|
merge_remote_data!(remote_resource)
|
@@ -498,27 +511,27 @@ module Remotable
|
|
498
511
|
end
|
499
512
|
end
|
500
513
|
end
|
501
|
-
|
514
|
+
|
502
515
|
def create_remote_resource
|
503
516
|
@remote_resource = remote_model.new_resource
|
504
517
|
merge_local_data(@remote_resource)
|
505
|
-
|
518
|
+
|
506
519
|
remote_set_timeout :create
|
507
520
|
if @remote_resource.save
|
508
|
-
|
521
|
+
|
509
522
|
# This line is especially crucial if the primary key
|
510
523
|
# of the remote resource needs to be stored locally.
|
511
524
|
merge_remote_data(@remote_resource)
|
512
525
|
else
|
513
|
-
|
526
|
+
|
514
527
|
merge_remote_errors(remote_resource.errors)
|
515
528
|
raise ActiveRecord::RecordInvalid.new(self)
|
516
529
|
end
|
517
530
|
end
|
518
|
-
|
531
|
+
|
519
532
|
def destroy_remote_resource
|
520
533
|
return nil unless remote_resource
|
521
|
-
|
534
|
+
|
522
535
|
remote_set_timeout :destroy
|
523
536
|
if remote_resource.destroy
|
524
537
|
true
|
@@ -526,30 +539,36 @@ module Remotable
|
|
526
539
|
merge_remote_errors(remote_resource.errors)
|
527
540
|
false
|
528
541
|
end
|
542
|
+
rescue Remotable::NotFound
|
543
|
+
report_ignored_404_on_destroy $!
|
529
544
|
end
|
530
|
-
|
531
|
-
|
532
|
-
|
545
|
+
|
546
|
+
|
547
|
+
|
533
548
|
def initialize_expiration_date
|
534
549
|
reset_expiration_date unless self.expires_at
|
535
550
|
end
|
536
|
-
|
551
|
+
|
537
552
|
def reset_expiration_date
|
538
553
|
self.expires_at = expires_after.from_now
|
539
554
|
end
|
540
|
-
|
541
|
-
|
542
|
-
|
555
|
+
|
556
|
+
|
557
|
+
|
543
558
|
def local_attribute_changed?(name)
|
544
559
|
changed.member?(name.to_s)
|
545
560
|
end
|
546
|
-
|
547
|
-
|
548
|
-
|
561
|
+
|
562
|
+
|
563
|
+
|
549
564
|
protected
|
550
|
-
|
551
|
-
|
552
|
-
|
565
|
+
|
566
|
+
|
567
|
+
|
568
|
+
def report_ignored_404_on_destroy(error)
|
569
|
+
Remotable.logger.error "[remotable:#{self.class.name.underscore}:destroy] #{error.message}"
|
570
|
+
end
|
571
|
+
|
553
572
|
def merge_remote_errors(errors)
|
554
573
|
Remotable.logger.debug "[remotable:#{self.class.name.underscore}:merge_remote_errors](#{fetch_value.inspect}) #{errors.inspect}"
|
555
574
|
errors.each do |attribute, messages|
|
@@ -559,7 +578,7 @@ module Remotable
|
|
559
578
|
end
|
560
579
|
self
|
561
580
|
end
|
562
|
-
|
581
|
+
|
563
582
|
def merge_remote_data(remote_resource)
|
564
583
|
remote_attribute_map.each do |remote_attr, local_attr|
|
565
584
|
if remote_resource.key?(remote_attr)
|
@@ -568,9 +587,10 @@ module Remotable
|
|
568
587
|
send("#{local_attr}=", remote_value)
|
569
588
|
end
|
570
589
|
end
|
590
|
+
self.remote_updated_at = Time.now if respond_to?(:remote_updated_at=)
|
571
591
|
self
|
572
592
|
end
|
573
|
-
|
593
|
+
|
574
594
|
def merge_local_data(remote_resource, changes_only=false)
|
575
595
|
remote_attribute_map.each do |remote_attr, local_attr|
|
576
596
|
if !changes_only || local_attribute_changed?(local_attr)
|
@@ -581,14 +601,14 @@ module Remotable
|
|
581
601
|
end
|
582
602
|
self
|
583
603
|
end
|
584
|
-
|
585
|
-
|
586
|
-
|
604
|
+
|
605
|
+
|
606
|
+
|
587
607
|
private
|
588
|
-
|
608
|
+
|
589
609
|
def remote_set_timeout(mode)
|
590
610
|
self.class.send :remote_set_timeout, mode
|
591
611
|
end
|
592
|
-
|
612
|
+
|
593
613
|
end
|
594
614
|
end
|