Module: Arkenstone::Associations::ClassMethods

Defined in:
lib/arkenstone/associations.rb

Instance Method Summary collapse

Instance Method Details

#add_association_method(method_name, &method_definition) ⇒ Object

Adds a method to a class unless that method is already defined.



308
309
310
# File 'lib/arkenstone/associations.rb', line 308

def add_association_method(method_name, &method_definition)
  define_method method_name, method_definition unless method_defined? method_name
end

#belongs_to(parent_model_name) ⇒ Object

The opposite of a has_X relationship. Allows you to go back up the association tree. Example:

class Hat
  belongs_to :llama
end

class Llama
end

Once ‘belongs_to` has been evaluated, the structure of `Hat` will look like this:

class Hat
  def llama
    #snip
  end
end


196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/arkenstone/associations.rb', line 196

def belongs_to(parent_model_name)
  setup_arkenstone_data

  parent_model_field = "#{parent_model_name}_id"

  self.arkenstone_attributes = [] unless arkenstone_attributes
  arkenstone_attributes << parent_model_field.to_sym
  class_eval("attr_accessor :#{parent_model_field}", __FILE__, __LINE__)

  # The method for accessing the cached data is `cached_[name]`. If the cache is empty it creates a request to repopulate it from the server.
  cached_parent_model_name = "cached_#{parent_model_name}"
  add_association_method cached_parent_model_name do
    cache = arkenstone_data
    cache[parent_model_name] = fetch_parent parent_model_name if cache[parent_model_name].nil?
    cache[parent_model_name]
  end

  # The uncached version is the name supplied to belongs_to. It wipes the cache for the association and refetches it.
  add_association_method parent_model_name.to_s do
    arkenstone_data[parent_model_name] = nil
    send cached_parent_model_name
  end

  define_method("#{parent_model_name}=") do |parent_instance|
    send "#{parent_model_field}=".to_sym, parent_instance.id
  end
end

#has_and_belongs_to_many(model_klass_name) ⇒ Object

Support for ‘has_and_belongs_to_many` relationship



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/arkenstone/associations.rb', line 225

def has_and_belongs_to_many(model_klass_name)
  # Gather the namespace
  namespace             = to_s.split(/::/)
  model_klass_name      = model_klass_name.to_s.singularize.underscore.to_sym
  current_klass_name    = namespace.pop.underscore.to_sym

  # Build join class needs
  join_klass_name       = [model_klass_name, current_klass_name].sort.join('_')
  join_klass_classified = join_klass_name.classify.to_sym
  join_klass_pluralized = join_klass_name.pluralize
  namespace             = Kernel.const_get(namespace.join('::'))

  # Create the join class if it doesn't exist already
  unless namespace.constants.include?(join_klass_classified)
    join_klass = namespace.const_set(join_klass_classified, Class.new)
    join_klass.instance_eval { include Arkenstone::Document }

    # The join class should belong to both foreign sides of the relationship
    join_klass.send :belongs_to, model_klass_name
    join_klass.send :belongs_to, current_klass_name
  end

  # This class should belong to the join table
  send(:has_many, join_klass_pluralized.to_sym) unless respond_to?(join_klass_pluralized.to_sym)

  # These are helper variables for the cached and uncached join `:through` instances
  model_klass_pluralized = model_klass_name.to_s.pluralize
  cached_instances_field = "cached_#{model_klass_pluralized}"

  send :attr_accessor, cached_instances_field.to_sym

  # Creates a `self.join_through_instances` helper method
  #
  # This actually pulls instances of the join model and then maps on the
  # complimenting foreign key to gather all the foreign join instances
  #
  define_method model_klass_pluralized.to_s do
    current_klass_instance   = self # The instance calling this method
    current_klass_pluralized = current_klass_name.to_s.pluralize

    # Check for cached joined instances
    cached_instances = current_klass_instance.send cached_instances_field
    return cached_instances if cached_instances

    # Get from joined instances
    model_klass_instances = send("cached_#{join_klass_pluralized}".to_sym).map(&:"#{model_klass_pluralized}")

    # Redefine `<<` so that you can something like `beer.tags << new_tag`
    model_klass_instances.define_singleton_method :<< do |element|
      # Use built in `push` for `Array.new`
      push element

      # Cache the result
      current_klass_instance.send "#{cached_instances_field}=", self

      # Add the current class instance in the other side of the join
      # The equivelant of doing `beer.tags << tag` then `tag.beers << beer`
      #
      # Grab the current_klass_instances from element
      element_current_klass_instances = element.send(current_klass_pluralized)

      # Push the current_klass_instance to what element currently has
      element_current_klass_instances.push(current_klass_instance)

      # Save the new stack of current_klass_instances with element
      element.send "#{current_klass_pluralized}=", element_current_klass_instances

      # Return the new array
      self
    end
    model_klass_instances
  end

  # This creates a setter helper to set all joined instances on the
  # opposite side of the foreign join
  #
  define_method "#{model_klass_pluralized}=" do |elements|
    current_klass_instance = self
    current_klass_instance.send "#{cached_instances_field}=", elements
  end
end

#has_many(child_model_name, options = {}) ⇒ Object

Creates a One to Many association with the supplied ‘child_model_name`. Example:

class Flea
end

class Llama
  has_many :fleas

end

Once ‘has_many` has evaluated, the structure of `Llama` will look like this:

class Llama
  url 'http://example.com/llamas'

  def cached_fleas
    #snip
  end

  def fleas
    #snip
  end

  def flea_ids
    [...] # all the ids of the fleas
  end

  def add_flea(new_flea)
    #snip
  end

  def remove_flea(flea_to_remove)
    #snip
  end
end

You can override the url of the association by passing in model_name: ‘something’. This will change the URL it fetches from to use the ‘model_name` instead:

has_many :fleas, model_name: 'bugs'

Will fetch ‘fleas` from `example.com/llamas/:id/bugs.



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/arkenstone/associations.rb', line 75

def has_many(child_model_name, options = {})
  setup_arkenstone_data
  child_url_fragment = options[:model_name] || child_model_name
  child_class_name = options[:class_name] || child_model_name

  # The method for accessing the cached data is `cached_[name]`. If the cache is empty it creates a request to repopulate it from the server.
  cached_child_name = "cached_#{child_model_name}"
  add_association_method cached_child_name do
    cache = arkenstone_data
    if cache[child_model_name].nil?
      child_instances = fetch_children child_class_name, child_url_fragment
      attach_nested_has_many_resource_methods(child_instances, child_model_name, child_class_name)
      cache[child_model_name] = child_instances
    end
    cache[child_model_name]
  end

  # The uncached version is the name supplied to has_many. It wipes the cache for the association and refetches it.
  add_association_method child_model_name do
    wipe_arkenstone_cache child_model_name
    send cached_child_name
  end

  # Creates an array of the ids of the child models for quick access.
  singular = child_model_name.to_s.singularize
  add_association_method "#{singular}_ids" do
    (send cached_child_name).map(&:id)
  end

  # Add a model to the association with add_[child_model_name]. It performs two network calls, one to add it, then another to refetch the association.
  add_child_method_name = "add_#{singular}"
  add_association_method add_child_method_name do |new_child|
    add_child child_model_name, new_child.id
    wipe_arkenstone_cache child_model_name
    send cached_child_name
  end

  # Remove a model from the association with remove_[child_model_name]. It performs two network calls, one to add it, then another to refetch the association.
  remove_child_method_name = "remove_#{singular}"
  add_association_method remove_child_method_name do |child_to_remove|
    remove_child child_model_name, child_to_remove.id
    wipe_arkenstone_cache child_model_name
    send cached_child_name
  end
end

#has_one(child_model_name, options = {}) ⇒ Object

Similar to ‘has_many` but for a One to One association. Example:

class Hat
end

class Llama
  has_one :hat
end

Once ‘has_one` has evaluated, the structure of `Llama` will look like this:

class Llama
  def cached_hat
    #snip
  end

  def hat
    #snip
  end

  def hat=(new_value)
    #snip
  end
end

If nil is passed into the setter method (‘hat=` in the above example), the association is removed.



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/arkenstone/associations.rb', line 147

def has_one(child_model_name, options = {})
  setup_arkenstone_data
  child_url_fragment = options[:model_name] || child_model_name

  # The method for accessing the cached single resource is `cached_[name]`. If the value is nil it creates a request to pull the value from the server.
  cached_child_name = "cached_#{child_model_name}"
  add_association_method cached_child_name do
    cache = arkenstone_data
    cache[child_model_name] = fetch_child child_model_name, child_url_fragment if cache[child_model_name].nil?
    cache[child_model_name]
  end

  # The uncached version is retrieved by wiping the cache for the association, and then re-getting it.
  add_association_method child_model_name do
    arkenstone_data[child_model_name] = nil
    send cached_child_name
  end

  # A single association is updated or removed with a setter method.
  setter_method_name = "#{child_model_name}="
  add_association_method setter_method_name do |new_value|
    if new_value.nil?
      old_model = send child_model_name
      remove_child child_model_name, old_model.id
      wipe_arkenstone_cache child_model_name
    else
      add_child child_model_name, new_value.id
      wipe_arkenstone_cache child_model_name
      send cached_child_name
    end
  end
end

#setup_arkenstone_dataObject

All association data is stored in a hash (@arkenstone_data) on the instance of the class. Each entry in the hash is keyed off the association name. The value of the hash key is a basic array. This can be wrapped up and extended if (when) more functionality is needed. ‘setup_arkenstone_data` creates the following instance methods on the class:

‘arkenstone_data` - the hash for the association data. Only use this if you’re absolutely 100% sure that you don’t need to get up to date data.

‘wipe_arkenstone_cache` - clears the cache for the association provided



23
24
25
26
27
28
29
30
31
32
# File 'lib/arkenstone/associations.rb', line 23

def setup_arkenstone_data
  define_method('arkenstone_data') do
    @arkenstone_data = {} if @arkenstone_data.nil?
    @arkenstone_data
  end

  define_method('wipe_arkenstone_cache') do |model_name|
    arkenstone_data[model_name] = nil
  end
end