alessandro1997/panther

View on GitHub
lib/panther/representer/association/reflection.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true
module Panther
  module Representer
    module Association
      # An association reflection holds all the information about an association.
      #
      # It is instantiated by {ClassMethods#association}.
      #
      # @author Alessandro Desantis <desa.alessandro@gmail.com>
      class Reflection
        # @!attribute [r] name
        #   @return [Symbol] the association name
        #
        # @!attribute [r] options
        #   @return [Hash] the association's options
        attr_reader :name, :options

        # Initializes the reflection. The +type+ and +source_klass+ options are mandatory.
        #
        # @param name [String|Symbol] the association's name
        # @param options [Hash] the association's options
        #
        # @options options [Symbol] :type the type of the association (+has_one+, +has_many+, ...)
        # @options options [Class] :source_klass the representer defining the association
        # @options options [Symbol] :resource_module the module containing the resource (by default,
        #   this is computed from the name and +source_klass+)
        # @option options [TrueClass|FalseClass] :expose_id whether to expose the IDs of the
        #   associated records when they are not being sideloaded
        # @options options [Proc] :if a condition that must be +true+ to show the association
        # @options options [Proc] :unless a condition that must be +false+ to show the association
        # @options options [Proc] :page_proc collections only: a proc accepting the params hash and
        #   returning the current page for this association (the default proc returns the
        #   +[association_name]_page+ param)
        # @options options [Proc] :per_page_proc collections only: a proc accepting the params hash
        #   and returning the number of records to show on each page (the default proc returns the
        #   +[association_name]_per_page+ param or 10 if it isn't present)
        #
        # @raise [ArgumentError] if the options are malformed
        def initialize(name, options = {})
          validate_options(options)

          @name = name.to_sym
          @namer = Namer.new(options[:source_klass])
          @options = compute_options_from(options)
        end

        # Returns the representer to use for this association (resource or collection representer,
        # depending on the association type).
        #
        # @return [Representer::Base] the representer
        def representer_klass
          representer_module = "#{options[:resource_module]}::Representer".constantize

          if collection?
            "#{representer_module}::Collection"
          else
            "#{representer_module}::Resource"
          end.constantize
        end

        # Returns whether this is a single association (+has_one+ or +belongs_to+).
        #
        # @return [Boolean]
        def single?
          single_type?(options[:type])
        end

        # Returns whether this is a collection association (+has_many+ or
        # +has_and_belongs_to_many+).
        #
        # @return [Boolean]
        def collection?
          collection_type?(options[:type])
        end

        # Evaluates the +if+ and +unless+ options of the associations (if present), in the
        # given context and passing the provided arguments.
        #
        # Internally, uses +instance_exec+ to change the context.
        #
        # @param context [Object] the object in which to evaluate the procs
        # @param **args [Hash] arguments to delegate to the procs
        #
        # @return [Boolean] the result of the evaluation
        def evaluate_conditions(context:, **args)
          return false if options[:if] && !context.instance_exec(args, &options[:if])
          return false if options[:unless] && context.instance_exec(args, &options[:unless])

          true
        end

        private

        def compute_options_from(base_options)
          base_options[:expose_id] = true unless base_options.key?(:expose_id)

          if collection_type?(base_options[:type])
            base_options[:page_proc] = proc do |params|
              params["#{name}_page"]
            end unless base_options[:page_proc]

            base_options[:per_page_proc] = proc do |params|
              params["#{name}_per_page"] || 10
            end unless base_options[:per_page_proc]
          end

          unless base_options[:resource_module]
            resource_module = "::#{@namer.namespace_module}::#{name.to_s.singularize.camelize}"
            base_options[:resource_module] = resource_module.constantize
          end

          base_options
        end

        def validate_options(input_options)
          missing_options = [:source_klass, :type].select do |option|
            !input_options.key?(option)
          end

          fail(
            ArgumentError,
            "Missing required options #{missing_options.join(', ')}"
          ) if missing_options.any?

          fail(
            ArgumentError,
            "#{input_options[:type]} is an invalid association type"
          ) unless single_type?(input_options[:type]) || collection_type?(input_options[:type])
        end

        def single_type?(type)
          type.to_sym.in?([:has_one, :belongs_to])
        end

        def collection_type?(type)
          type.to_sym.in?([:has_many, :has_and_belongs_to_many])
        end
      end
    end
  end
end