Class: ReactiveResource::Base

Inherits:
ActiveResource::Base
  • Object
show all
Extended by:
Extensions::RelativeConstGet
Defined in:
lib/reactive_resource/base.rb

Overview

The class that all ReactiveResourse resources should inherit from. This class fixes and patches over a lot of the broken stuff in Active Resource, and smoothes out the differences between the client-side Rails REST stuff and the server-side Rails REST stuff. It also adds support for ActiveRecord-like associations.

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Extensions::RelativeConstGet

relative_const_get

Class Attribute Details

.associationsObject

Holds all the associations that have been declared for this class



179
180
181
# File 'lib/reactive_resource/base.rb', line 179

def associations
  @associations
end

Class Method Details

.association_prefix(options) ⇒ Object

Generates the URL prefix that the belongs_to parameters and associations refer to. For example, a license with params :lawyer_id => 2 will return ‘lawyers/2/’ and a phone with params :address_id => 2, :lawyer_id => 3 will return ‘lawyers/3/addresses/2/’.



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/reactive_resource/base.rb', line 153

def self.association_prefix(options)
  options = options.dup
  association_prefix = ''

  if belongs_to_associations

    used_associations = prefix_associations(options)

    association_prefix = used_associations.map do |association|
      collection_name = association.associated_class.collection_name
      if association.associated_class.singleton?
        collection_name
      else
        value = options.delete("#{association.attribute}_id".intern)
        "#{collection_name}/#{value}"
      end
    end.join('/')

    # add trailing slash
    association_prefix << '/' unless used_associations.empty?
  end
  association_prefix
end

.belongs_to(attribute, options = {}) ⇒ Object

Add a parent-child relationship between attribute and this class. This allows parameters like attribute_id to contribute to generating nested urls. options is a hash of extra parameters:

:class_name

Override the class name of the target of the association. By default, this is based on the attribute name.



211
212
213
# File 'lib/reactive_resource/base.rb', line 211

def self.belongs_to(attribute, options = {})
  self.associations << Association::BelongsToAssociation.new(self, attribute, options)
end

.belongs_to_associationsObject

Returns all of the belongs_to associations this class has.



216
217
218
# File 'lib/reactive_resource/base.rb', line 216

def self.belongs_to_associations
  self.associations.select {|assoc| assoc.kind_of?(Association::BelongsToAssociation) }
end

.belongs_to_with_parentsObject

belongs_to in ReactiveResource works a little differently than ActiveRecord. Because we have to deal with full class hierachies in order to generate the full URL (as mentioned in association_prefix), we have to treat the belongs_to associations on objects that this object belongs_to as if they exist on this object itself. This method merges in all of this class’ associated classes’ belongs_to associations, so we can handle deeply nested routes. So, for instance, if we have phone => address => lawyer, phone will look for address’ belongs_to associations and merge them in. This allows us to have both lawyer_id and address_id at url generation time.



286
287
288
289
290
291
292
# File 'lib/reactive_resource/base.rb', line 286

def self.belongs_to_with_parents
  @belongs_to_with_parents ||= begin
    ret = belongs_to_associations.map(&:associated_attributes)
    ret += parents.map(&:belongs_to_with_parents)
    ret.flatten.uniq
  end
end

.collection_nameObject

Override ActiveResource’s collection_name to support singular names for singleton resources.



46
47
48
49
50
51
52
# File 'lib/reactive_resource/base.rb', line 46

def self.collection_name
  if singleton?
    element_name
  else
    super
  end
end

.collection_path(prefix_options = {}, query_options = nil) ⇒ Object

This method differs from its parent by adding association_prefix into the generated url. This is needed to support belongs_to associations.



64
65
66
67
# File 'lib/reactive_resource/base.rb', line 64

def self.collection_path(prefix_options = {}, query_options = nil)
  prefix_options, query_options = split_options(prefix_options) if query_options.nil?
  "#{prefix(prefix_options)}#{association_prefix(prefix_options)}#{collection_name}#{extension}#{query_string(query_options)}"
end

.custom_method_collection_url(method_name, options = {}) ⇒ Object

Same as collection_path, except with an extra method_name on the end to support custom methods



71
72
73
74
# File 'lib/reactive_resource/base.rb', line 71

def self.custom_method_collection_url(method_name, options = {})
  prefix_options, query_options = split_options(options)
  "#{prefix(prefix_options)}#{association_prefix(prefix_options)}#{collection_name}/#{method_name}#{extension}#{query_string(query_options)}"
end

.element_path(id, prefix_options = {}, query_options = nil) ⇒ Object

Same as collection_path, except it adds the ID to the end of the path (unless it’s a singleton resource)



78
79
80
81
82
83
84
85
86
87
88
# File 'lib/reactive_resource/base.rb', line 78

def self.element_path(id, prefix_options = {}, query_options = nil)
  prefix_options, query_options = split_options(prefix_options) if query_options.nil?
  element_path = "#{prefix(prefix_options)}#{association_prefix(prefix_options)}#{collection_name}"

  # singleton resources don't have an ID
  if id.present? || !singleton?
    element_path += "/#{id}"
  end
  element_path += "#{extension}#{query_string(query_options)}"
  element_path
end

.extensionObject

Returns the extension based on the format (‘.json’, for example), or the empty string if format doesn’t specify an extension



57
58
59
# File 'lib/reactive_resource/base.rb', line 57

def self.extension
  format.extension.blank? ? "" : ".#{format.extension}"
end

.find_one(options) ⇒ Object

Active Resource’s find_one does nothing if you don’t pass a :from parameter. This doesn’t make sense if you’re dealing with a singleton resource, so if we don’t get anything back from find_one, try hitting the element path directly



34
35
36
37
38
39
40
41
42
# File 'lib/reactive_resource/base.rb', line 34

def self.find_one(options)
  found_object = super(options)
  if !found_object && singleton?
    prefix_options, query_options = split_options(options[:params])
    path = element_path(nil, prefix_options, query_options)
    found_object = instantiate_record(format.decode(connection.get(path, headers).body), prefix_options)
  end
  found_object
end

.has_many(attribute, options = {}) ⇒ Object

Add a has_many relationship to another class. options is a hash of extra parameters:

:class_name

Override the class name of the target of the association. By default, this is based on the attribute name.



199
200
201
# File 'lib/reactive_resource/base.rb', line 199

def self.has_many(attribute, options = {})
  self.associations << Association::HasManyAssociation.new(self, attribute, options)
end

.has_one(attribute, options = {}) ⇒ Object

Add a has_one relationship to another class. options is a hash of extra parameters:

:class_name

Override the class name of the target of the association. By default, this is based on the attribute name.



189
190
191
# File 'lib/reactive_resource/base.rb', line 189

def self.has_one(attribute, options = {})
  self.associations << Association::HasOneAssociation.new(self, attribute, options)
end

.inherited(child) ⇒ Object

Fix up the klass attribute of all of our associations to point to the new child class instead of the parent class, so class lookup works as expected



223
224
225
226
227
228
229
230
231
232
233
# File 'lib/reactive_resource/base.rb', line 223

def self.inherited(child)
  super(child)
  child.associations = []
  associations.each do |association|
    begin
      child.associations << association.class.new(child, association.attribute, association.options)
    rescue NameError
      # assume that they'll fix the association later by manually specifying :class_name in the belongs_to
    end
  end
end

.parentsObject

All the classes that this class references in its belongs_to, along with their parents, and so on.



296
297
298
# File 'lib/reactive_resource/base.rb', line 296

def self.parents
  @parents ||= belongs_to_associations.map(&:associated_class)
end

.prefix_associations(options) ⇒ Object

Returns a list of the belongs_to associations we will use to generate the full path for this resource.



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/reactive_resource/base.rb', line 123

def self.prefix_associations(options)
  options = options.dup
  used_associations = []
  parent_associations = []

  # Recurse to add the parent resource hierarchy. For Phone, for
  # instance, this will add the '/lawyers/:id' part of the URL,
  # which it knows about from the Address class.
  parents.each do |parent|
    parent_associations = parent.prefix_associations(options)
    break unless parent_associations.empty?
  end

  # The association chain we're following
  used_association = nil

  belongs_to_associations.each do |association|
    if !used_association && (association.associated_class.singleton? || param_value = options.delete("#{association.attribute}_id".intern)) # only take the first one
      used_associations << association
      break
    end
  end
  parent_associations + used_associations
end

.prefix_parametersObject

Add all of the belongs_to attributes as prefix parameters. This is necessary to support nested url generation on our polymorphic associations, because we need some way of getting the attributes at the point where we need to generate the url, and only the prefix options are available for both finds and creates.



112
113
114
115
116
117
118
119
# File 'lib/reactive_resource/base.rb', line 112

def self.prefix_parameters
  if !@prefix_parameters
    @prefix_parameters = super

    @prefix_parameters.merge(belongs_to_with_parents.map {|p| "#{p}_id".to_sym})
  end
  @prefix_parameters
end

.singletonObject

Call this method to transform a resource into a ‘singleton’ resource. This will fix the paths Active Resource generates for singleton resources. See rails.lighthouseapp.com/projects/8994/tickets/4348-supporting-singleton-resources-in-activeresource for more info.



20
21
22
# File 'lib/reactive_resource/base.rb', line 20

def self.singleton
  self.singleton_resource = true
end

.singleton?Boolean

true if this resource is a singleton resource, false otherwise

Returns:

  • (Boolean)


26
27
28
# File 'lib/reactive_resource/base.rb', line 26

def self.singleton?
  self.singleton_resource?
end

Instance Method Details

#encode(options = {}) ⇒ Object

ActiveResource (as of 3.0) assumes that you have a to_x method on ActiveResource::Base for any format ‘x’ that is assigned to the record. This seems weird – the format should take care of encoding, not the object itself. To keep this as stable as possible, we should use to_x if it’s defined, otherwise just delegate to the format’s ‘encode’ function. This is how things worked as of Rails 2.3.



242
243
244
245
246
247
248
249
250
251
252
# File 'lib/reactive_resource/base.rb', line 242

def encode(options = {})
  if defined?(ActiveResource::VERSION) && ActiveResource::VERSION::MAJOR == 3
    if respond_to?("to_#{self.class.format.extension}")
      super(options)
    else
      self.class.format.encode(attributes, options)
    end
  else
    super(options)
  end
end

#load(attributes, remove_root = false) ⇒ Object

It’s kind of redundant to have the server return the foreign keys corresponding to the belongs_to associations (since they’ll be in the URL anyway), so we’ll try to inject them based on the attributes of the object we just used.



94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/reactive_resource/base.rb', line 94

def load(attributes, remove_root=false)
  attributes = attributes.stringify_keys
  self.class.belongs_to_with_parents.each do |belongs_to_param|
    attributes["#{belongs_to_param}_id"] ||= prefix_options["#{belongs_to_param}_id".intern]

    # also set prefix attributes as real attributes. Otherwise,
    # belongs_to attributes will be stripped out of the response
    # even if we aren't actually using the association.
    @attributes["#{belongs_to_param}_id"] = attributes["#{belongs_to_param}_id"]
  end
  super(attributes, remove_root)
end

#saveObject

In order to support two-way belongs_to associations in a reasonable way, we duplicate all of the prefix options as real attributes (so license.lawyer_id will set both the lawyer_id attribute and the lawyer_id prefix option). When we’re ready to save, we should take all the parameters that we’re already sending as prefix options and remove them from the model’s attributes so we don’t send duplicates down the wire. This way, if you have an object that belongs_to :lawyer and belongs_to :phone (in that order), setting lawyer_id and phone_id will treat lawyer_id as a prefix option and phone_id as a normal attribute.



265
266
267
268
269
270
271
272
273
# File 'lib/reactive_resource/base.rb', line 265

def save
  if self.class.belongs_to_associations
    used_attributes = self.class.prefix_associations(prefix_options).map {|association| association.attribute}
    used_attributes.each do |attribute|
      attributes.delete("#{attribute}_id".intern)
    end
  end
  super
end