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

#nosync, #nosync!, #nosync=, #nosync_value?

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

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



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/remotable/active_record_extender.rb', line 193

def method_missing(method_sym, *args, &block)
  method_details = recognize_remote_finder_method(method_sym)
  if method_details
    local_attributes = method_details[:local_attributes]
    values = args
    
    unless values.length == local_attributes.length
      raise ArgumentError, "#{method_sym} was called with #{values.length} but #{local_attributes.length} was expected"
    end
    
    local_resource = ((0...local_attributes.length).inject(self) do |scope, i|
      scope.where(local_attributes[i] => values[i])
    end).first || fetch_by(method_details[:remote_key], *values)
    
    raise ActiveRecord::RecordNotFound if local_resource.nil? && (method_sym =~ /!$/)
    local_resource
  else
    super(method_sym, *args, &block)
  end
end

Instance Attribute Details

#local_attribute_routesObject (readonly)

Returns the value of attribute local_attribute_routes.



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

def local_attribute_routes
  @local_attribute_routes
end

#remote_attribute_mapObject (readonly)

Returns the value of attribute remote_attribute_map.



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

def remote_attribute_map
  @remote_attribute_map
end

Instance Method Details

#all_by_remoteObject



321
322
323
# File 'lib/remotable/active_record_extender.rb', line 321

def all_by_remote
  find_by_remote_query(:all)
end

#attr_remote(*attrs) ⇒ Object



92
93
94
95
96
97
# File 'lib/remotable/active_record_extender.rb', line 92

def attr_remote(*attrs)
  map = attrs.extract_options!
  map = attrs.map_to_self.merge(map)
  @remote_attribute_map = map
  @local_attribute_routes = {} # reset routes
end

#default_route_for(local_key, remote_key = nil) ⇒ Object



147
148
149
150
151
152
153
154
# File 'lib/remotable/active_record_extender.rb', line 147

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



241
242
243
# File 'lib/remotable/active_record_extender.rb', line 241

def expire_all!
  update_all(["expires_at=?", 1.day.ago])
end

#expires_after(*args) ⇒ Object



87
88
89
90
# File 'lib/remotable/active_record_extender.rb', line 87

def expires_after(*args)
  @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.



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

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



362
363
364
365
366
367
368
369
# File 'lib/remotable/active_record_extender.rb', line 362

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 Also known as: find_by



107
108
109
# File 'lib/remotable/active_record_extender.rb', line 107

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

#find_by_remote_query(remote_method_name, *args) ⇒ Object



325
326
327
328
329
# File 'lib/remotable/active_record_extender.rb', line 325

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



255
256
257
# File 'lib/remotable/active_record_extender.rb', line 255

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



259
260
261
262
263
264
265
# File 'lib/remotable/active_record_extender.rb', line 259

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.



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/remotable/active_record_extender.rb', line 160

def instantiate(*args)
  record = super
  if record.expired? && !record.nosync?
    begin
      Remotable.logger.debug "[remotable:#{name.underscore}:instantiate](#{record.fetch_value.inspect}) expired #{record.expires_at}"
      record.pull_remote_data!
      record = nil if record.destroyed?
    rescue Remotable::TimeoutError
      report_ignored_timeout_error($!)
    rescue Remotable::ServiceUnavailableError
      report_ignored_503_error($!)
    end
  end
  record
end

#invoke_remote_model_find_by(remote_attr, *values) ⇒ Object



267
268
269
270
271
272
273
274
275
276
277
# File 'lib/remotable/active_record_extender.rb', line 267

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



279
280
281
282
283
284
285
286
287
288
289
# File 'lib/remotable/active_record_extender.rb', line 279

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



138
139
140
# File 'lib/remotable/active_record_extender.rb', line 138

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

#local_attribute_namesObject



130
131
132
# File 'lib/remotable/active_record_extender.rb', line 130

def local_attribute_names
  remote_attribute_map.values
end

#local_key(remote_key = nil) ⇒ Object



117
118
119
120
121
122
123
124
# File 'lib/remotable/active_record_extender.rb', line 117

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



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/remotable/active_record_extender.rb', line 331

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)


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

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.



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/remotable/active_record_extender.rb', line 217

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

#remote_attribute_name(local_attr) ⇒ Object



134
135
136
# File 'lib/remotable/active_record_extender.rb', line 134

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

#remote_attribute_namesObject



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

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"


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

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



293
294
295
296
297
298
299
300
301
302
# File 'lib/remotable/active_record_extender.rb', line 293

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



308
309
310
311
312
313
314
315
316
317
# File 'lib/remotable/active_record_extender.rb', line 308

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]}/, values[i].to_s)
  end
end

#remote_path_for_simple_key(route, local_key, value) ⇒ Object



304
305
306
# File 'lib/remotable/active_record_extender.rb', line 304

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

#remote_timeout(*args) ⇒ Object



99
100
101
102
103
104
105
# File 'lib/remotable/active_record_extender.rb', line 99

def remote_timeout(*args)
  if args.any?
    @remote_timeout = n = args.first
    @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



180
181
182
# File 'lib/remotable/active_record_extender.rb', line 180

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

#report_ignored_timeout_error(error) ⇒ Object



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

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)


188
189
190
191
# File 'lib/remotable/active_record_extender.rb', line 188

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



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

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