Module: Remotable::ActiveRecordExtender::ClassMethods

Includes:
Nosync
Defined in:
lib/remotable/active_record_extender.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Nosync

extended, included

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_sym, *values, &block) ⇒ Object

Raises:

  • (ArgumentError)


230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/remotable/active_record_extender.rb', line 230

def method_missing(method_sym, *values, &block)
  method_details = recognize_remote_finder_method(method_sym)
  return super(method_sym, *values, &block) unless method_details

  local_attributes = method_details[:local_attributes]
  raise ArgumentError, "#{method_sym} was called with #{values.length} but #{local_attributes.length} was expected" unless values.length == local_attributes.length

  local_resource = __remotable_lookup(method_details[:remote_key], local_attributes, values)
  local_resource = nil if local_resource && local_resource.destroyed?
  raise ActiveRecord::RecordNotFound if local_resource.nil? && (method_sym =~ /!$/)
  local_resource
end

Instance Attribute Details

#_expires_afterObject

Returns the value of attribute _expires_after.



46
47
48
# File 'lib/remotable/active_record_extender.rb', line 46

def _expires_after
  @_expires_after
end

#_local_attribute_routesObject

Returns the value of attribute _local_attribute_routes.



46
47
48
# File 'lib/remotable/active_record_extender.rb', line 46

def _local_attribute_routes
  @_local_attribute_routes
end

#_remote_attribute_mapObject

Returns the value of attribute _remote_attribute_map.



46
47
48
# File 'lib/remotable/active_record_extender.rb', line 46

def _remote_attribute_map
  @_remote_attribute_map
end

#_remote_timeoutObject

Returns the value of attribute _remote_timeout.



46
47
48
# File 'lib/remotable/active_record_extender.rb', line 46

def _remote_timeout
  @_remote_timeout
end

#remotable_skip_validation_on_syncObject

Returns the value of attribute remotable_skip_validation_on_sync.



46
47
48
# File 'lib/remotable/active_record_extender.rb', line 46

def remotable_skip_validation_on_sync
  @remotable_skip_validation_on_sync
end

Instance Method Details

#__remotable_local_lookup(local_attributes, values) ⇒ Object



249
250
251
252
253
# File 'lib/remotable/active_record_extender.rb', line 249

def __remotable_local_lookup(local_attributes, values)
  (0...local_attributes.length)
    .inject(self) { |scope, i| scope.where(local_attributes[i] => values[i]) }
    .limit(1).first
end

#__remotable_lookup(key, local_attributes, values) ⇒ Object



243
244
245
246
247
# File 'lib/remotable/active_record_extender.rb', line 243

def __remotable_lookup(key, local_attributes, values)
  __remotable_local_lookup(local_attributes, values) || fetch_by(key, *values)
rescue ActiveRecord::RecordNotUnique
  __remotable_local_lookup(local_attributes, values)
end

#all_by_remoteObject



366
367
368
# File 'lib/remotable/active_record_extender.rb', line 366

def all_by_remote
  find_by_remote_query(:all)
end

#attr_remote(*attrs) ⇒ Object



95
96
97
98
99
100
# File 'lib/remotable/active_record_extender.rb', line 95

def attr_remote(*attrs)
  map = attrs.extract_options!
  map = attrs.map_to_self.merge(map)
  self._remote_attribute_map = map
  self._local_attribute_routes = {} # reset routes
end

#default_route_for(local_key, remote_key = nil) ⇒ Object



161
162
163
164
165
166
167
168
# File 'lib/remotable/active_record_extender.rb', line 161

def default_route_for(local_key, remote_key=nil)
  remote_key ||= remote_attribute_name(local_key)
  if remote_key.to_s == primary_key
    ":#{local_key}"
  else
    "by_#{local_key}/:#{local_key}"
  end
end

#expire_all!Object



282
283
284
# File 'lib/remotable/active_record_extender.rb', line 282

def expire_all!
  update_all(expires_at: 1.day.ago)
end

#expires_after(*args) ⇒ Object



90
91
92
93
# File 'lib/remotable/active_record_extender.rb', line 90

def expires_after(*args)
  self._expires_after = args.first if args.any?
  _expires_after
end

#fetch_by(remote_attr, *values) ⇒ Object

Looks the resource up remotely, by the given attribute If the resource is found, wraps it in a new local resource and returns that.



296
297
298
299
# File 'lib/remotable/active_record_extender.rb', line 296

def fetch_by(remote_attr, *values)
  remote_resource = find_remote_resource_by(remote_attr, *values)
  remote_resource && new_from_remote(remote_resource)
end

#fetch_corresponding_local_resources(remote_resources) ⇒ Object



408
409
410
411
412
413
414
415
# File 'lib/remotable/active_record_extender.rb', line 408

def fetch_corresponding_local_resources(remote_resources)
  conditions = Array.wrap(remote_key).each_with_object({}) do |remote_attr, query|
    local_attr = local_attribute_name(remote_attr)
    query[local_attr] = remote_resources.map { |resource| resource[remote_attr] }
  end

  where(conditions)
end

#fetch_with(local_key, options = {}) ⇒ Object



118
119
120
# File 'lib/remotable/active_record_extender.rb', line 118

def fetch_with(local_key, options={})
  self._local_attribute_routes.merge!(local_key => options[:path])
end

#find_by_remote_query(remote_method_name, *args) ⇒ Object



370
371
372
373
374
375
# File 'lib/remotable/active_record_extender.rb', line 370

def find_by_remote_query(remote_method_name, *args)
  remote_set_timeout :list
  remote_resources = Array.wrap(remote_model.send(remote_method_name, *args))

  map_remote_resources_to_local(remote_resources)
end

#find_remote_resource_by(remote_attr, *values) ⇒ Object



301
302
303
# File 'lib/remotable/active_record_extender.rb', line 301

def find_remote_resource_by(remote_attr, *values)
  invoke_remote_model_find_by(remote_attr, *values)
end

#find_remote_resource_for_local_by(local_resource, remote_attr, *values) ⇒ Object



305
306
307
308
309
310
311
# File 'lib/remotable/active_record_extender.rb', line 305

def find_remote_resource_for_local_by(local_resource, remote_attr, *values)
  if remote_model.respond_to?(:find_by_for_local)
    invoke_remote_model_find_by_for_local(local_resource, remote_attr, *values)
  else
    invoke_remote_model_find_by(remote_attr, *values)
  end
end

#instantiate(*args) ⇒ Object

!nb: this method is called when associations are loaded so you can use the remoted record in associations.



174
175
176
177
178
# File 'lib/remotable/active_record_extender.rb', line 174

def instantiate(*args)
  super.tap do |record|
    sync_on_instantiate(record) unless ActiveRecord.version.segments.first > 5
  end
end

#instantiate_instance_of(*args) ⇒ Object

!nb: In Rails 6+, this has been extracted from instantiate and can be called to instantiate homogenous sets of records without calling instantiate



182
183
184
185
186
# File 'lib/remotable/active_record_extender.rb', line 182

def instantiate_instance_of(*args)
  super.tap do |record|
    sync_on_instantiate(record)
  end
end

#invoke_remote_model_find_by(remote_attr, *values) ⇒ Object



313
314
315
316
317
318
319
320
321
322
323
# File 'lib/remotable/active_record_extender.rb', line 313

def invoke_remote_model_find_by(remote_attr, *values)
  remote_set_timeout :fetch

  find_by = remote_model.method(:find_by)
  case find_by.arity
  when 1; find_by.call(remote_path_for(remote_attr, *values))
  when 2; find_by.call(remote_attr, *values)
  else
    raise InvalidRemoteModel, "#{remote_model}.find_by should take either 1 or 2 parameters"
  end
end

#invoke_remote_model_find_by_for_local(local_resource, remote_attr, *values) ⇒ Object



325
326
327
328
329
330
331
332
333
334
335
# File 'lib/remotable/active_record_extender.rb', line 325

def invoke_remote_model_find_by_for_local(local_resource, remote_attr, *values)
  remote_set_timeout :pull

  find_by_for_local = remote_model.method(:find_by_for_local)
  case find_by_for_local.arity
  when 2; find_by_for_local.call(local_resource, remote_path_for(remote_attr, *values))
  when 3; find_by_for_local.call(local_resource, remote_attr, *values)
  else
    raise InvalidRemoteModel, "#{remote_model}.find_by_for_local should take either 2 or 3 parameters"
  end
end

#local_attribute_name(remote_attr) ⇒ Object



152
153
154
# File 'lib/remotable/active_record_extender.rb', line 152

def local_attribute_name(remote_attr)
  _remote_attribute_map[remote_attr] || remote_attr
end

#local_attribute_namesObject



144
145
146
# File 'lib/remotable/active_record_extender.rb', line 144

def local_attribute_names
  _remote_attribute_map.values
end

#local_attribute_routesObject



114
115
116
# File 'lib/remotable/active_record_extender.rb', line 114

def local_attribute_routes
  self._local_attribute_routes
end

#local_key(remote_key = nil) ⇒ Object



131
132
133
134
135
136
137
138
# File 'lib/remotable/active_record_extender.rb', line 131

def local_key(remote_key=nil)
  remote_key ||= self.remote_key
  if remote_key.is_a?(Array)
    remote_key.map(&method(:local_attribute_name))
  else
    local_attribute_name(remote_key)
  end
end

#map_remote_resources_to_local(remote_resources) ⇒ Object



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/remotable/active_record_extender.rb', line 377

def map_remote_resources_to_local(remote_resources)
  return [] if remote_resources.nil? || remote_resources.empty?

  local_resources = nosync { fetch_corresponding_local_resources(remote_resources).to_a }

  # Ensure a corresponding local resource for
  # each remote resource; return the set of
  # local resources.
  remote_resources.map do |remote_resource|

    # Get the specific local resource that
    # corresponds to this remote one.
    local_resource = local_resources.detect { |local_resource|
      Array.wrap(remote_key).all? { |remote_attr|
        local_attr = local_attribute_name(remote_attr)
        local_resource.send(local_attr) == remote_resource[remote_attr]
      }
    }

    # If a local counterpart to this remote value
    # exists, update the local resource and return it.
    # If not, create a local counterpart and return it.
    if local_resource
      local_resource.instance_variable_set :@remote_resource, remote_resource
      local_resource.pull_remote_data!
    else
      new_from_remote(remote_resource)
    end
  end
end

#nosync?Boolean

Returns:

  • (Boolean)


49
50
51
52
53
# File 'lib/remotable/active_record_extender.rb', line 49

def nosync?
  return true if remote_model.nil?
  return super if nosync_value?
  Remotable.nosync?
end

#recognize_remote_finder_method(method_sym) ⇒ Object

If the missing method IS a Remotable finder method, returns the remote key (may be a composite key). Otherwise, returns false.



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/remotable/active_record_extender.rb', line 258

def recognize_remote_finder_method(method_sym)
  method_name = method_sym.to_s
  return false unless method_name =~ /find_by_([^!]*)(!?)/

  local_attributes = $1.split("_and_").map(&:to_sym)
  remote_attributes = local_attributes.map(&method(:remote_attribute_name))

  local_key, remote_key = if local_attributes.length == 1
    [local_attributes[0], remote_attributes[0]]
  else
    [local_attributes, remote_attributes]
  end

  generate_default_remote_key # <- Make sure we've figured out the remote
                              #    primary key if we're evaluating a finder

  return false unless _local_attribute_routes.key?(local_key)

  { :local_attributes => local_attributes,
    :remote_key => remote_key }
end

#remotable_skip_validation!Object



122
123
124
# File 'lib/remotable/active_record_extender.rb', line 122

def remotable_skip_validation!
  self.remotable_skip_validation_on_sync = true
end

#remotable_skip_validation_on_sync?Boolean

Returns:

  • (Boolean)


126
127
128
# File 'lib/remotable/active_record_extender.rb', line 126

def remotable_skip_validation_on_sync?
  self.remotable_skip_validation_on_sync
end

#remote_attribute_mapObject



110
111
112
# File 'lib/remotable/active_record_extender.rb', line 110

def remote_attribute_map
  self._remote_attribute_map
end

#remote_attribute_name(local_attr) ⇒ Object



148
149
150
# File 'lib/remotable/active_record_extender.rb', line 148

def remote_attribute_name(local_attr)
  _remote_attribute_map.key(local_attr) || local_attr
end

#remote_attribute_namesObject



140
141
142
# File 'lib/remotable/active_record_extender.rb', line 140

def remote_attribute_names
  _remote_attribute_map.keys
end

#remote_key(*args) ⇒ Object

Sets the key with which a resource is identified remotely. If no remote key is set, the remote key is assumed to be :id. Which could be explicitly set like this:

remote_key :id

It can can be a composite key:

remote_key [:calendar_id, :id]

You can also supply a path for the remote key which will be passed to fetch_with:

remote_key [:calendar_id, :id], :path => "calendars/:calendar_id/events/:id"


70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/remotable/active_record_extender.rb', line 70

def remote_key(*args)
  if args.any?
    remote_key = args.shift
    options = args.shift || {}

    # remote_key may be a composite of several attributes
    # ensure that all of the attributs have been defined
    Array.wrap(remote_key).each do |attribute|
      raise(":#{attribute} is not the name of a remote attribute") unless remote_attribute_names.member?(attribute)
    end

    # Set up a finder method for the remote_key
    fetch_with(local_key(remote_key), options)

    @remote_key = remote_key
  else
    @remote_key || generate_default_remote_key
  end
end

#remote_path_for(remote_key, *values) ⇒ Object



339
340
341
342
343
344
345
346
347
348
# File 'lib/remotable/active_record_extender.rb', line 339

def remote_path_for(remote_key, *values)
  route = route_for(remote_key)
  local_key = self.local_key(remote_key)

  if remote_key.is_a?(Array)
    remote_path_for_composite_key(route, local_key, values)
  else
    remote_path_for_simple_key(route, local_key, values.first)
  end
end

#remote_path_for_composite_key(route, local_key, values) ⇒ Object



354
355
356
357
358
359
360
361
362
363
# File 'lib/remotable/active_record_extender.rb', line 354

def remote_path_for_composite_key(route, local_key, values)
  values.flatten!
  unless values.length == local_key.length
    raise ArgumentError, "local_key has #{local_key.length} attributes but values has #{values.length}"
  end

  (0...values.length).inject(route) do |route, i|
    route.gsub(/:#{local_key[i]}/, ERB::Util.url_encode(values[i].to_s))
  end
end

#remote_path_for_simple_key(route, local_key, value) ⇒ Object



350
351
352
# File 'lib/remotable/active_record_extender.rb', line 350

def remote_path_for_simple_key(route, local_key, value)
  route.gsub(/:#{local_key}/, ERB::Util.url_encode(value.to_s))
end

#remote_timeout(*args) ⇒ Object



102
103
104
105
106
107
108
# File 'lib/remotable/active_record_extender.rb', line 102

def remote_timeout(*args)
  if args.any?
    self._remote_timeout = n = args.first
    self._remote_timeout = {:list => n, :fetch => n, :pull => n, :create => n, :update => n, :destroy => n} if n.is_a?(Numeric)
  end
  _remote_timeout
end

#report_ignored_503_error(error) ⇒ Object



213
214
215
# File 'lib/remotable/active_record_extender.rb', line 213

def report_ignored_503_error(error)
  Remotable.logger.error "[remotable:#{name.underscore}:instantiate] #{error.message}"
end

#report_ignored_network_error(error) ⇒ Object



209
210
211
# File 'lib/remotable/active_record_extender.rb', line 209

def report_ignored_network_error(error)
  Remotable.logger.error "[remotable:#{name.underscore}:instantiate] #{error.message}"
end

#report_ignored_ssl_error(error) ⇒ Object



217
218
219
# File 'lib/remotable/active_record_extender.rb', line 217

def report_ignored_ssl_error(error)
  Remotable.logger.error "[remotable:#{name.underscore}:instantiate] #{error.message}"
end

#report_ignored_timeout_error(error) ⇒ Object



205
206
207
# File 'lib/remotable/active_record_extender.rb', line 205

def report_ignored_timeout_error(error)
  Remotable.logger.error "[remotable:#{name.underscore}:instantiate] #{error.message}"
end

#respond_to?(method_sym, include_private = false) ⇒ Boolean

!todo: create these methods on an anonymous module and mix it in

Returns:

  • (Boolean)


225
226
227
228
# File 'lib/remotable/active_record_extender.rb', line 225

def respond_to?(method_sym, include_private=false)
  return true if recognize_remote_finder_method(method_sym)
  super(method_sym, include_private)
end

#route_for(remote_key) ⇒ Object



156
157
158
159
# File 'lib/remotable/active_record_extender.rb', line 156

def route_for(remote_key)
  local_key = self.local_key(remote_key)
  _local_attribute_routes[local_key] || default_route_for(local_key, remote_key)
end

#sync_all!Object



286
287
288
289
# File 'lib/remotable/active_record_extender.rb', line 286

def sync_all!
  expire_all!
  all.to_a
end

#sync_on_instantiate(record) ⇒ Object



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/remotable/active_record_extender.rb', line 188

def sync_on_instantiate(record)
  if record.expired? && !record.nosync?
    begin
      Remotable.logger.debug "[remotable:#{name.underscore}:sync_on_instantiate](#{record.fetch_value.inspect}) expired #{record.expires_at}"
      record.pull_remote_data!
    rescue Remotable::TimeoutError
      report_ignored_timeout_error($!)
    rescue Remotable::NetworkError
      report_ignored_network_error($!)
    rescue Remotable::ServiceUnavailableError
      report_ignored_503_error($!)
    rescue Remotable::SSLError
      report_ignored_ssl_error($!)
    end
  end
end