require 'zendesk_api/helpers' module ZendeskAPI # Represents an association between two resources # @private class Association class << self def namespaces [ZendeskAPI] + ZendeskAPI::DataNamespace.descendants end def class_from_namespace(klass_as_string) namespaces.each do |ns| if module_defines_class?(ns, klass_as_string) return ns.const_get(klass_as_string) end end nil end def module_defines_class?(mod, klass_as_string) mod.const_defined?(klass_as_string, false) end end # @return [Hash] Options passed into the association attr_reader :options # Options to pass in # * class - Required # * parent - Parent instance # * path - Optional path instead of resource name def initialize(options = {}) @options = SilentMash.new(options) end # Generate a path to the resource. # id and _id attributes will be deleted from passed in options hash if they are used in the built path. # Arguments that can be passed in: # An instance, any resource instance # Hash Options: # * with_parent - Include the parent path (false by default) # * with_id - Include the instance id, if possible (true) def generate_path(*args) options = SilentMash.new(:with_id => true) if args.last.is_a?(Hash) original_options = args.pop options.merge!(original_options) end instance = args.first namespace = @options[:class].to_s.split("::") namespace[-1] = @options[:class].resource_path # Remove components without path information ignorable_namespace_strings.each { |ns| namespace.delete(ns) } has_parent = namespace.size > 1 || (options[:with_parent] && @options.parent) if has_parent parent_class = @options.parent ? @options.parent.class : Association.class_from_namespace(ZendeskAPI::Helpers.modulize_string(namespace[0])) parent_namespace = build_parent_namespace(parent_class, instance, options, original_options) namespace[1..1] = parent_namespace if parent_namespace namespace[0] = parent_class.resource_path else namespace[0] = @options.path || @options[:class].resource_path end if id = extract_id(instance, options, original_options) namespace << id end namespace.join("/") end # Tries to place side loads onto given resources. def side_load(resources, side_loads) key = "#{options.name}_id" plural_key = "#{Inflection.singular options.name.to_s}_ids" resources.each do |resource| if resource.key?(plural_key) # Grab associations from child_ids field on resource side_load_from_child_ids(resource, side_loads, plural_key) elsif resource.key?(key) || options.singular side_load_from_child_or_parent_id(resource, side_loads, key) else # Grab associations from parent_id field from multiple child resources side_load_from_parent_id(resource, side_loads, key) end end end private # @return [Array] ['ZendeskAPI', 'Voice', etc.. ] def ignorable_namespace_strings ZendeskAPI::DataNamespace.descendants.map { |klass| klass.to_s.split('::') }.flatten.uniq end def _side_load(resource, side_loads) side_loads.map! do |side_load| resource.send(:wrap_resource, side_load, options) end ZendeskAPI::Collection.new(resource.client, options[:class]).tap do |collection| collection.replace(side_loads) end end def side_load_from_parent_id(resource, side_loads, key) key = "#{resource.class.singular_resource_name}_id" resource.send("#{options.name}=", _side_load(resource, side_loads.select {|side_load| side_load[key] == resource.id })) end def side_load_from_child_ids(resource, side_loads, plural_key) ids = resource.send(plural_key) resource.send("#{options.name}=", _side_load(resource, side_loads.select {|side_load| ids.include?(side_load[options.include_key]) })) end def side_load_from_child_or_parent_id(resource, side_loads, key) # Either grab association from child_id field on resource or parent_id on child resource if resource.key?(key) id = resource.send(key) include_key = options.include_key else id = resource.id include_key = "#{resource.class.singular_resource_name}_id" end return unless id side_load = side_loads.detect do |side_load| id == side_load[include_key] end resource.send("#{options.name}=", side_load) if side_load end def build_parent_namespace(parent_class, instance, options, original_options) path = @options.path association_on_parent = parent_class.associations.detect { |a| a[:name] == @options[:name] } association_on_parent ||= parent_class.associations.detect do |a| !a[:inline] && a[:class] == @options[:class] end if association_on_parent path ||= association_on_parent[:path] path ||= association_on_parent[:name].to_s end path ||= @options[:class].resource_path [ extract_parent_id(parent_class, instance, options, original_options), path ] end def extract_parent_id(parent_class, instance, options, original_options) parent_id_column = "#{parent_class.singular_resource_name}_id" if @options.parent @options.parent.id elsif instance instance.send(parent_id_column) elsif options[parent_id_column] original_options.delete(parent_id_column) || original_options.delete(parent_id_column.to_sym) else raise ArgumentError.new("#{@options[:class].resource_name} requires #{parent_id_column} or parent") end end def extract_id(instance, options, original_options) if options[:with_id] && !@options[:class].ancestors.include?(SingularResource) if instance && instance.id instance.id elsif options[:id] original_options.delete(:id) || original_options.delete("id") end end end end end