lib/graphql/schema/directive.rb



# frozen_string_literal: true

module GraphQL
  class Schema
    # Subclasses of this can influence how {GraphQL::Execution::Interpreter} runs queries.
    #
    # - {.include?}: if it returns `false`, the field or fragment will be skipped altogether, as if it were absent
    # - {.resolve}: Wraps field resolution (so it should call `yield` to continue)
    class Directive < GraphQL::Schema::Member
      extend GraphQL::Schema::Member::HasArguments
      extend GraphQL::Schema::Member::HasArguments::HasDirectiveArguments

      class << self
        # Directives aren't types, they don't have kinds.
        undef_method :kind

        def path
          "@#{super}"
        end

        # Return a name based on the class name,
        # but downcase the first letter.
        def default_graphql_name
          @default_graphql_name ||= begin
            camelized_name = super.dup
            camelized_name[0] = camelized_name[0].downcase
            -camelized_name
          end
        end

        def locations(*new_locations)
          if new_locations.any?
            new_locations.each do |new_loc|
              if !LOCATIONS.include?(new_loc.to_sym)
                raise ArgumentError, "#{self} (#{self.graphql_name}) has an invalid directive location: `locations #{new_loc}` "
              end
            end
            @locations = new_locations
          else
            @locations ||= (superclass.respond_to?(:locations) ? superclass.locations : [])
          end
        end

        def default_directive(new_default_directive = nil)
          if new_default_directive != nil
            @default_directive = new_default_directive
          elsif @default_directive.nil?
            @default_directive = (superclass.respond_to?(:default_directive) ? superclass.default_directive : false)
          else
            !!@default_directive
          end
        end

        def default_directive?
          default_directive
        end

        # If false, this part of the query won't be evaluated
        def include?(_object, arguments, context)
          static_include?(arguments, context)
        end

        # Determines whether {Execution::Lookahead} considers the field to be selected
        def static_include?(_arguments, _context)
          true
        end

        # Continuing is passed as a block; `yield` to continue
        def resolve(object, arguments, context)
          yield
        end

        # Continuing is passed as a block, yield to continue.
        def resolve_each(object, arguments, context)
          yield
        end

        def on_field?
          locations.include?(FIELD)
        end

        def on_fragment?
          locations.include?(FRAGMENT_SPREAD) && locations.include?(INLINE_FRAGMENT)
        end

        def on_operation?
          locations.include?(QUERY) && locations.include?(MUTATION) && locations.include?(SUBSCRIPTION)
        end

        def repeatable?
          !!@repeatable
        end

        def repeatable(new_value)
          @repeatable = new_value
        end

        private

        def inherited(subclass)
          super
          subclass.class_eval do
            @default_graphql_name ||= nil
          end
        end
      end

      # @return [GraphQL::Schema::Field, GraphQL::Schema::Argument, Class, Module]
      attr_reader :owner

      # @return [GraphQL::Interpreter::Arguments]
      attr_reader :arguments

      def initialize(owner, **arguments)
        @owner = owner
        assert_valid_owner
        # It's be nice if we had the real context here, but we don't. What we _would_ get is:
        # - error handling
        # - lazy resolution
        # Probably, those won't be needed here, since these are configuration arguments,
        # not runtime arguments.
        @arguments = self.class.coerce_arguments(nil, arguments, Query::NullContext)
      end

      def graphql_name
        self.class.graphql_name
      end

      LOCATIONS = [
        QUERY =                  :QUERY,
        MUTATION =               :MUTATION,
        SUBSCRIPTION =           :SUBSCRIPTION,
        FIELD =                  :FIELD,
        FRAGMENT_DEFINITION =    :FRAGMENT_DEFINITION,
        FRAGMENT_SPREAD =        :FRAGMENT_SPREAD,
        INLINE_FRAGMENT =        :INLINE_FRAGMENT,
        SCHEMA =                 :SCHEMA,
        SCALAR =                 :SCALAR,
        OBJECT =                 :OBJECT,
        FIELD_DEFINITION =       :FIELD_DEFINITION,
        ARGUMENT_DEFINITION =    :ARGUMENT_DEFINITION,
        INTERFACE =              :INTERFACE,
        UNION =                  :UNION,
        ENUM =                   :ENUM,
        ENUM_VALUE =             :ENUM_VALUE,
        INPUT_OBJECT =           :INPUT_OBJECT,
        INPUT_FIELD_DEFINITION = :INPUT_FIELD_DEFINITION,
        VARIABLE_DEFINITION =    :VARIABLE_DEFINITION,
      ]

      DEFAULT_DEPRECATION_REASON = 'No longer supported'
      LOCATION_DESCRIPTIONS = {
        QUERY:                    'Location adjacent to a query operation.',
        MUTATION:                 'Location adjacent to a mutation operation.',
        SUBSCRIPTION:             'Location adjacent to a subscription operation.',
        FIELD:                    'Location adjacent to a field.',
        FRAGMENT_DEFINITION:      'Location adjacent to a fragment definition.',
        FRAGMENT_SPREAD:          'Location adjacent to a fragment spread.',
        INLINE_FRAGMENT:          'Location adjacent to an inline fragment.',
        SCHEMA:                   'Location adjacent to a schema definition.',
        SCALAR:                   'Location adjacent to a scalar definition.',
        OBJECT:                   'Location adjacent to an object type definition.',
        FIELD_DEFINITION:         'Location adjacent to a field definition.',
        ARGUMENT_DEFINITION:      'Location adjacent to an argument definition.',
        INTERFACE:                'Location adjacent to an interface definition.',
        UNION:                    'Location adjacent to a union definition.',
        ENUM:                     'Location adjacent to an enum definition.',
        ENUM_VALUE:               'Location adjacent to an enum value definition.',
        INPUT_OBJECT:             'Location adjacent to an input object type definition.',
        INPUT_FIELD_DEFINITION:   'Location adjacent to an input object field definition.',
        VARIABLE_DEFINITION:      'Location adjacent to a variable definition.',
      }

      private

      def assert_valid_owner
        case @owner
        when Class
          if @owner < GraphQL::Schema::Object
            assert_has_location(OBJECT)
          elsif @owner < GraphQL::Schema::Union
            assert_has_location(UNION)
          elsif @owner < GraphQL::Schema::Enum
            assert_has_location(ENUM)
          elsif @owner < GraphQL::Schema::InputObject
            assert_has_location(INPUT_OBJECT)
          elsif @owner < GraphQL::Schema::Scalar
            assert_has_location(SCALAR)
          elsif @owner < GraphQL::Schema
            assert_has_location(SCHEMA)
          else
            raise "Unexpected directive owner class: #{@owner}"
          end
        when Module
          assert_has_location(INTERFACE)
        when GraphQL::Schema::Argument
          if @owner.owner.is_a?(GraphQL::Schema::Field)
            assert_has_location(ARGUMENT_DEFINITION)
          else
            assert_has_location(INPUT_FIELD_DEFINITION)
          end
        when GraphQL::Schema::Field
          assert_has_location(FIELD_DEFINITION)
        when GraphQL::Schema::EnumValue
          assert_has_location(ENUM_VALUE)
        else
          raise "Unexpected directive owner: #{@owner.inspect}"
        end
      end

      def assert_has_location(location)
        if !self.class.locations.include?(location)
          raise ArgumentError, <<-MD
Directive `@#{self.class.graphql_name}` can't be attached to #{@owner.graphql_name} because #{location} isn't included in its locations (#{self.class.locations.join(", ")}).

Use `locations(#{location})` to update this directive's definition, or remove it from #{@owner.graphql_name}.
MD
        end
      end
    end
  end
end