martinandert/gql

View on GitHub
lib/gql/mixins/has_calls.rb

Summary

Maintainability
A
35 mins
Test Coverage
require 'active_support/concern'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/class/subclasses'
require 'active_support/core_ext/object/try'
require 'active_support/core_ext/string/inflections'

module GQL
  module Mixins
    module HasCalls
      extend ActiveSupport::Concern

      included do
        class_attribute :calls, :call_proc, instance_accessor: false, instance_predicate: false
        self.calls = {}
      end

      module ClassMethods
        def add_call(id, *args, &block)
          remove_call id

          id            = id.to_sym
          options       = args.extract_options!
          call_spec     = args.shift || block || proc_for_call(id)
          result_spec   = options[:returns] || call_spec.try(:result_class)
          result_class  = result_class_from_spec(result_spec)

          build_call_class(call_spec, id, result_class).tap do |call_class|
            propagate :call, id, call_class
          end
        end

        alias :call :add_call

        def remove_call(id)
          shutdown :call, id.to_sym
        end

        def has_call?(id)
          calls.has_key? id.to_sym
        end

        private
          def build_call_class(spec, id, result_class)
            call_class_from_spec(spec).tap do |call_class|
              call_class.id = id
              call_class.result_class = result_class

              if result_class && result_class.name.nil?
                call_class.const_set :Result, result_class
              end
            end
          end

          def call_class_from_spec(spec)
            return LazyCall.new(spec) if spec.is_a?(::String)
            return Class.new(spec) unless spec.is_a?(::Proc)

            Class.new(Call).tap do |call_class|
              call_class.class_eval do
                self.proc = spec

                def execute(*args)
                  instance_exec(*args, &self.class.proc)
                end
              end
            end
          end

          def const_name_for_call(id)
            :"#{id.to_s.camelize}Call"
          end

          def proc_for_call(id)
            instance_exec id, &(call_proc || GQL.default_call_proc)
          end

          def result_class_from_spec(spec)
            result_class =
              case spec
              when ::Array
                result_class_from_connection_spec spec.dup
              when ::Hash, ::Proc
                result_class_from_object_spec spec.dup
              else
                spec
              end

            result_class && Registry.fetch(result_class)
          end

          def result_class_from_connection_spec(spec)
            if spec.size == 1
              spec.unshift GQL.default_list_class
            end

            options = {
              list_class: spec.first,
              item_class: spec.last
            }

            Connection.build_class :result, nil, options
          end

          def result_class_from_object_spec(spec)
            Object.build_class :result, nil, object_class: spec
          end
      end

      private
        def value_of_call(ast_call)
          call_class = call_class_for_id(ast_call.id)
          call_class.execute self.class, ast_call, target, variables, context
        end

        def call_class_for_id(id)
          self.class.calls[id] or raise Errors::CallNotFound.new(id, self.class)
        end

        class LazyCall
          attr_reader :call_class_name
          attr_accessor :id, :result_class

          def initialize(call_class_name)
            @call_class_name = call_class_name
          end

          def execute(caller_class, ast_node, target, variables, context)
            spurred = spur_call_class(caller_class, id)
            spurred.execute caller_class, ast_node, target, variables, context
          end

          def parameters
            call_class.parameters
          end

          private
            def call_class
              @call_class ||= Registry.fetch(call_class_name, Call)
            end

            def spur_call_class(caller_class, id)
              caller_class.add_call id, call_class, returns: result_class
            end
        end
    end
  end
end