# frozen_string_literal: true module Rails # :nodoc: module GraphQL # :nodoc: # = GraphQL Field # # A field has multiple purposes, which is defined by the specific subclass # used. They are also, in various ways, similar to arguments, since they # tend to have the same structure. # array as input. # # ==== Options # # * :owner - The main object that this field belongs to. # * :null - Marks if the overall type can be null # (defaults to true). # * :array - Marks if the type should be wrapped as an array # (defaults to false). # * :nullable - Marks if the internal values of an array can be null # (defaults to true). # * :full - Shortcut for +null: false, nullable: false, array: true+ # (defaults to false). # * :method_name - The name of the method used to fetch the field data # (defaults to nil). # * :enabled - Mark the field as enabled # (defaults to true). # * :disabled - Works as the oposite of the enabled option # (defaults to false). # * :directives - The list of directives associated with the value # (defaults to nil). # * :desc - The description of the argument # (defaults to nil). # # It also accepts a block for further configurations class Field extend ActiveSupport::Autoload include Helpers::WithDirectives autoload :ScopedConfig autoload :ResolvedField autoload :TypedField autoload :ProxiedField autoload :InputField autoload :OutputField autoload :MutationField delegate :input_type?, :output_type?, :leaf_type?, :proxy?, :mutation?, to: :class delegate :namespaces, to: :owner attr_reader :name, :gql_name, :owner class << self # A small shared helper method that allows field information to be # proxied def proxyable_methods(*list, klass:, allow_nil: false) list = list.flatten.compact.map do |method_name| ivar = '@' + method_name.delete_suffix('?') accessor = 'field' + (allow_nil ? '&.' : '.') + method_name "def #{method_name}; defined?(#{ivar}) ? #{ivar} : #{accessor}; end" end klass.class_eval(list.join("\n"), __FILE__, __LINE__ + 1) end # Defines if the current field is valid as an input type def input_type? false end # Defines if the current field is valid as an output type def output_type? false end # Defines if the current field is considered a leaf output def leaf_type? false end # Checks if the the field is a proxy kind of field def proxy? false end # Checks if the field is associated with a mutation def mutation? false end end def initialize(name, owner:, **xargs, &block) @owner = owner normalize_name(name) @directives = GraphQL.directives_to_set(xargs[:directives], source: self) @method_name = xargs[:method_name].to_s.underscore.to_sym \ unless xargs[:method_name].nil? full = xargs.fetch(:full, false) @null = full ? false : xargs.fetch(:null, true) @array = full ? true : xargs.fetch(:array, false) @nullable = full ? false : xargs.fetch(:nullable, true) @desc = xargs[:desc]&.strip_heredoc&.chomp @enabled = xargs.fetch(:enabled, !xargs.fetch(:disabled, false)) configure(&block) if block.present? end def initialize_copy(*) # :nodoc: super @owner = nil end # Apply a controlled set of changes to the field def apply_changes(**xargs, &block) required_items! unless xargs.fetch(:nullable, true) required! unless xargs.fetch(:null, true) disable! if xargs.fetch(:disabled, false) enable! if xargs.fetch(:enabled, false) @desc = xargs[:desc].strip_heredoc.chomp if xargs.key?(:desc) configure(&block) if block.present? end # Allow extra configurations to be performed using a block def configure(&block) Field::ScopedConfig.new(self, block.binding.receiver).instance_exec(&block) end # Returns the name of the method used to retrieve the information def method_name defined?(@method_name) ? @method_name : @name end # Check if the other field is equivalent def =~(other) other.is_a?(GraphQL::Field) && other.array? == array? && (other.null? == null? || other.null? && !null?) && (other.nullable? == nullable? || other.nullable? && !nullable?) end # Checks if the argument can be null def null? !!@null end # Checks if the argument can be an array def array? !!@array end # Checks if the argument can have null elements in the array def nullable? !!@nullable end # Check if tre field is enabled def enabled? !!@enabled end # Check if tre field is disabled def disabled? !enabled? end # Mark the field as globally enabled def enable! @enabled = true end # Mark the field as globally disabled def disable! @enabled = false end # Return the description of the argument def description @desc end # Checks if a description was provided def description? defined?(@desc) && !!@desc end # Check if the field is an internal one def internal? name.start_with?('__') end # This method must be overridden by children classes def valid_input?(*) enabled? end # This method must be overridden by children classes def valid_output?(*) enabled? end # Transforms the given value to its representation in a JSON string def to_json(value) return 'null' if value.nil? return type_klass.to_json(value) unless array? value.map { |part| type_klass.to_json(part) } end # Turn the given value into a JSON string representation def as_json(value) return if value.nil? return type_klass.as_json(value) unless array? value.map { |part| type_klass.as_json(part) } end # Turn a user input of this given type into an ruby object def deserialize(value) return if value.nil? return type_klass.deserialize(value) unless array? value.map { |val| type_klass.deserialize(val) unless val.nil? } end # Check if the given value is valid using +valid_input?+ or # +valid_output?+ depending of the type of the field def valid?(value) input_type? ? valid_input?(value) : valid_output?(value) end # Checks if the definition of the field is valid. def validate!(*) super if defined? super raise NameError, <<~MSG.squish if gql_name.start_with?('__') && !internal? The name "#{gql_name}" is invalid. Only internal fields from the spec can have a name starting with "__". MSG end # Update the null value def required! @null = false end # Update the nullable value def required_items! @nullable = false end # Create a proxy of the current field def to_proxy(*args, **xargs, &block) proxy = self.class.allocate proxy.extend Field::ProxiedField proxy.send(:proxied) proxy.send(:initialize, self, *args, **xargs, &block) proxy end def inspect # :nodoc: <<~INSPECT.squish + '>' #<#{self.class.name} #{inspect_owner} #{inspect_source} #{inspect_enabled} #{gql_name}#{inspect_arguments}#{inspect_type} #{inspect_default_value} #{inspect_directives} INSPECT end protected # Allow the subclasses to define the extra inspection methods def respond_to_missing?(method_name, *) method_name.start_with?('inspect_') || super end # Allow the subclasses to define the extra inspection methods def method_missing(method_name, *) method_name.start_with?('inspect_') ? '' : super end # Ensures the consistency of the name of the field def normalize_name(value) return if value.blank? @name = value.to_s.underscore.to_sym @gql_name = @name.to_s.gsub(/^_+/, '').camelize(:lower) if internal? @gql_name.prepend('__') elsif @name.start_with?('_') @gql_name.prepend('_') end end # Helper method to inspect the directives def inspect_directives all_directives.map(&:inspect) end # Show the name of the owner of the object for inspection def inspect_owner owner.is_a?(Module) ? owner.name : owner.class.name end # Add a disable tag to the inspection if the field is disabled def inspect_enabled '[disabled]' if disabled? end end end end