remotable 0.3.0 → 0.4.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 55fd48684b48f738a160bce22e85c08a8bf206be
4
- data.tar.gz: 8b157f2ee0ea3e3baf10de49391a22ceebe3459e
3
+ metadata.gz: 50bbb2463866bea7de985bdc5375aaac0b7c715b
4
+ data.tar.gz: ee16e4675c4c3c8a7cd4420e9180a0d532575a80
5
5
  SHA512:
6
- metadata.gz: 08d450f3c0efde8b23c84a75135576293715df063657d608730ad6ad3f175082f1edf8c116ff8a92f9e5c50ee6693bee9fcf72b6854a6f3d4db63d6dabcb1ead
7
- data.tar.gz: 5c89a432fa32638f8e28fc1e08e784dd059fb7d506bed8dec03d104be0c166f3b26df528804023f278f3d1550c1437d200f0864fa04904663b9a1cd6cdfeecb9
6
+ metadata.gz: 32767f6332fe922693bf2265c7dff1b73b916c491800e792b44e4d4104050335fcf562a5c729bf1ba9f1306d3bf14d2a0590e4211c98de7bf22a0f4a7865d26f
7
+ data.tar.gz: 46c8039a5195b3a629761798b359c228ce8448caa72b59686402182f17ea70773fe8191ee5c80fcd13a3cd8af9ebbf73947ae4c53317ceef78fa4fa9ae730d46
data/.gitignore CHANGED
@@ -2,4 +2,5 @@
2
2
  .bundle
3
3
  pkg/*
4
4
  coverage
5
+ tmp/*
5
6
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- remotable (0.3.0)
4
+ remotable (0.4.0.beta2)
5
5
  activerecord
6
6
  activeresource
7
7
  activesupport
@@ -123,3 +123,6 @@ DEPENDENCIES
123
123
  simplecov
124
124
  sqlite3
125
125
  turn
126
+
127
+ BUNDLED WITH
128
+ 1.10.6
@@ -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 `find_by` method:
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
- find_by :slug
82
- find_by :name
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 `find_by` method:
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
- find_by :name, :path => "by_nombre/:name"
100
+ fetch_with :name, :path => "by_nombre/:name"
101
101
  end
102
102
 
103
103
 
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.
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
 
@@ -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
- alias :find_by :fetch_with
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, *args, &block)
196
+
197
+ def method_missing(method_sym, *values, &block)
194
198
  method_details = recognize_remote_finder_method(method_sym)
195
- if method_details
196
- local_attributes = method_details[:local_attributes]
197
- values = args
198
-
199
- unless values.length == local_attributes.length
200
- raise ArgumentError, "#{method_sym} was called with #{values.length} but #{local_attributes.length} was expected"
201
- end
202
-
203
- local_resource = ((0...local_attributes.length).inject(self) do |scope, i|
204
- scope.where(local_attributes[i] => values[i])
205
- end).first || fetch_by(method_details[:remote_key], *values)
206
-
207
- raise ActiveRecord::RecordNotFound if local_resource.nil? && (method_sym =~ /!$/)
208
- local_resource
209
- else
210
- super(method_sym, *args, &block)
211
- end
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(["expires_at=?", 1.day.ago])
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?(:updated_at)
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