lib/zendesk_api/associations.rb
require 'zendesk_api/helpers'
module ZendeskAPI
# This module holds association method for resources.
# Associations can be loaded in three ways:
# * Commonly used resources are automatically side-loaded server side and sent along with their parent object.
# * Associated resource ids are sent and are then loaded one-by-one into the parent collection.
# * The association is represented with Rails' nested association urls (such as tickets/:id/groups) and are loaded that way.
#
# @private
module Associations
def self.included(base)
base.extend ClassMethods
end
def wrap_resource(resource, class_level_association, options = {})
instance_association = Association.new(class_level_association.merge(:parent => self))
klass = class_level_association[:class]
case resource
when Hash
klass.new(@client, resource.merge(:association => instance_association))
when String, Integer
klass.new(@client, (options[:include_key] || :id) => resource, :association => instance_association)
else
resource.association = instance_association
resource
end
end
# @private
module ClassMethods
def self.extended(klass)
klass.extend Has
klass.extend HasMany
end
def associations
@associations ||= []
end
def associated_with(name)
associations.inject([]) do |associated_with, association|
if association[:include] == name.to_s
associated_with.push(Association.new(association))
end
associated_with
end
end
private
def build_association(klass, resource_name, options)
{
:class => klass,
:name => resource_name,
:inline => options.delete(:inline),
:path => options.delete(:path),
:include => (options.delete(:include) || klass.resource_name).to_s,
:include_key => (options.delete(:include_key) || :id).to_s,
:singular => options.delete(:singular),
:extensions => Array(options.delete(:extend))
}
end
def define_used(association)
define_method "#{association[:name]}_used?" do
!!instance_variable_get("@#{association[:name]}")
end
end
module Has
# Represents a parent-to-child association between resources. Options to pass in are: class, path.
# @param [Symbol] resource_name_or_class The underlying resource name or a class to get it from
# @param [Hash] class_level_options The options to pass to the method definition.
def has(resource_name_or_class, class_level_options = {})
if klass = class_level_options.delete(:class)
resource_name = resource_name_or_class
else
klass = resource_name_or_class
resource_name = klass.singular_resource_name
end
class_level_association = build_association(klass, resource_name, class_level_options)
class_level_association.merge!(:singular => true, :id_column => "#{resource_name}_id")
associations << class_level_association
define_used(class_level_association)
define_has_getter(class_level_association)
define_has_setter(class_level_association)
end
private
def define_has_getter(association)
klass = association[:class] # shorthand
define_method association[:name] do |*args|
instance_options = args.last.is_a?(Hash) ? args.pop : {}
# return if cached
cached = instance_variable_get("@#{association[:name]}")
return cached if cached && !instance_options[:reload]
# find and cache association
instance_association = Association.new(association.merge(:parent => self))
resource = if klass.respond_to?(:find) && resource_id = method_missing(association[:id_column])
klass.find(@client, :id => resource_id, :association => instance_association)
elsif found = method_missing(association[:name].to_sym)
wrap_resource(found, association, :include_key => association[:include_key])
elsif klass.superclass == DataResource && !association[:inline]
response = @client.connection.get(instance_association.generate_path(:with_parent => true))
klass.new(@client, response.body[klass.singular_resource_name].merge(:association => instance_association))
end
send("#{association[:id_column]}=", resource.id) if resource && has_key?(association[:id_column])
instance_variable_set("@#{association[:name]}", resource)
end
end
def define_has_setter(association)
define_method "#{association[:name]}=" do |resource|
resource = wrap_resource(resource, association)
send("#{association[:id_column]}=", resource.id) if has_key?(association[:id_column])
instance_variable_set("@#{association[:name]}", resource)
end
end
end
module HasMany
# Represents a parent-to-children association between resources. Options to pass in are: class, path.
# @param [Symbol] resource_name_or_class The underlying resource name or class to get it from
# @param [Hash] class_level_options The options to pass to the method definition.
def has_many(resource_name_or_class, class_level_options = {})
if klass = class_level_options.delete(:class)
resource_name = resource_name_or_class
else
klass = resource_name_or_class
resource_name = klass.resource_name
end
class_level_association = build_association(klass, resource_name, class_level_options)
class_level_association.merge!(:singular => false, :id_column => "#{resource_name}_ids")
associations << class_level_association
define_used(class_level_association)
define_has_many_getter(class_level_association)
define_has_many_setter(class_level_association)
end
private
def define_has_many_getter(association)
klass = association[:class]
define_method association[:name] do |*args|
instance_opts = args.last.is_a?(Hash) ? args.pop : {}
# return if cached
cached = instance_variable_get("@#{association[:name]}")
return cached if cached && !instance_opts[:reload]
# find and cache association
instance_association = Association.new(association.merge(:parent => self))
singular_resource_name = Inflection.singular(association[:name].to_s)
resources = if (ids = method_missing("#{singular_resource_name}_ids")) && ids.any?
ids.map do |id|
klass.find(@client, :id => id, :association => instance_association)
end.compact
elsif (resources = method_missing(association[:name].to_sym)) && resources.any?
resources.map { |res| wrap_resource(res, association) }
else
[]
end
collection = ZendeskAPI::Collection.new(@client, klass, instance_opts.merge(:association => instance_association))
if association[:extensions].any?
collection.extend(*association[:extensions])
end
if resources.any?
collection.replace(resources)
end
send("#{association[:id_column]}=", resources.map(&:id)) if has_key?(association[:id_column])
instance_variable_set("@#{association[:name]}", collection)
end
end
def define_has_many_setter(association)
define_method "#{association[:name]}=" do |resources|
if resources.is_a?(Array)
wrapped = resources.map { |attr| wrap_resource(attr, association) }
send(association[:name]).replace(wrapped)
else
resources.association = Association.new(association.merge(:parent => self))
instance_variable_set("@#{association[:name]}", resources)
end
send("#{association[:id_column]}=", resources.map(&:id)) if resources && has_key?(association[:id_column])
resource
end
end
end
end
end
end