# frozen_string_literal: true
module GraphQL
module Language
module Nodes
# {AbstractNode} is the base class for all nodes in a GraphQL AST.
#
# It provides some APIs for working with ASTs:
# - `children` returns all AST nodes attached to this one. Used for tree traversal.
# - `scalars` returns all scalar (Ruby) values attached to this one. Used for comparing nodes.
# - `to_query_string` turns an AST node into a GraphQL string
class AbstractNode
module DefinitionNode
# This AST node's {#line} returns the first line, which may be the description.
# @return [Integer] The first line of the definition (not the description)
attr_reader :definition_line
def initialize(options = {})
@definition_line = options.delete(:definition_line)
super(options)
end
end
attr_reader :line, :col, :filename
# Initialize a node by extracting its position,
# then calling the class's `initialize_node` method.
# @param options [Hash] Initial attributes for this node
def initialize(options = {})
if options.key?(:position_source)
position_source = options.delete(:position_source)
@line = position_source.line
@col = position_source.col
end
@filename = options.delete(:filename)
initialize_node(**options)
end
# Value equality
# @return [Boolean] True if `self` is equivalent to `other`
def ==(other)
return true if equal?(other)
other.kind_of?(self.class) &&
other.scalars == self.scalars &&
other.children == self.children
end
NO_CHILDREN = [].freeze
# @return [Array<GraphQL::Language::Nodes::AbstractNode>] all nodes in the tree below this one
def children
NO_CHILDREN
end
# @return [Array<Integer, Float, String, Boolean, Array>] Scalar values attached to this node
def scalars
NO_CHILDREN
end
# This might be unnecessary, but its easiest to add it here.
def initialize_copy(other)
@children = nil
@scalars = nil
@query_string = nil
end
def children_method_name
self.class.children_method_name
end
def position
[line, col]
end
def to_query_string(printer: GraphQL::Language::Printer.new)
if printer.is_a?(GraphQL::Language::Printer)
@query_string ||= printer.print(self)
else
printer.print(self)
end
end
# This creates a copy of `self`, with `new_options` applied.
# @param new_options [Hash]
# @return [AbstractNode] a shallow copy of `self`
def merge(new_options)
dup.merge!(new_options)
end
# Copy `self`, but modify the copy so that `previous_child` is replaced by `new_child`
def replace_child(previous_child, new_child)
# Figure out which list `previous_child` may be found in
method_name = previous_child.children_method_name
# Get the value from this (original) node
prev_children = public_send(method_name)
if prev_children.is_a?(Array)
# Copy that list, and replace `previous_child` with `new_child`
# in the list.
new_children = prev_children.dup
prev_idx = new_children.index(previous_child)
new_children[prev_idx] = new_child
else
# Use the new value for the given attribute
new_children = new_child
end
# Copy this node, but with the new child value
copy_of_self = merge(method_name => new_children)
# Return the copy:
copy_of_self
end
# TODO DRY with `replace_child`
def delete_child(previous_child)
# Figure out which list `previous_child` may be found in
method_name = previous_child.children_method_name
# Copy that list, and delete previous_child
new_children = public_send(method_name).dup
new_children.delete(previous_child)
# Copy this node, but with the new list of children:
copy_of_self = merge(method_name => new_children)
# Return the copy:
copy_of_self
end
protected
def merge!(new_options)
new_options.each do |key, value|
instance_variable_set(:"@#{key}", value)
end
self
end
class << self
# Add a default `#visit_method` and `#children_method_name` using the class name
def inherited(child_class)
super
name_underscored = child_class.name
.split("::").last
.gsub(/([a-z])([A-Z])/,'\1_\2') # insert underscores
.downcase # remove caps
child_class.module_eval <<-RUBY
def visit_method
:on_#{name_underscored}
end
class << self
attr_accessor :children_method_name
end
self.children_method_name = :#{name_underscored}s
RUBY
end
private
# Name accessors which return lists of nodes,
# along with the kind of node they return, if possible.
# - Add a reader for these children
# - Add a persistent update method to add a child
# - Generate a `#children` method
def children_methods(children_of_type)
if defined?(@children_methods)
raise "Can't re-call .children_methods for #{self} (already have: #{@children_methods})"
else
@children_methods = children_of_type
end
if children_of_type == false
@children_methods = {}
# skip
else
children_of_type.each do |method_name, node_type|
module_eval <<-RUBY, __FILE__, __LINE__
# A reader for these children
attr_reader :#{method_name}
RUBY
if node_type
# Only generate a method if we know what kind of node to make
module_eval <<-RUBY, __FILE__, __LINE__
# Singular method: create a node with these options
# and return a new `self` which includes that node in this list.
def merge_#{method_name.to_s.sub(/s$/, "")}(node_opts)
merge(#{method_name}: #{method_name} + [#{node_type.name}.new(node_opts)])
end
RUBY
end
end
if children_of_type.size == 1
module_eval <<-RUBY, __FILE__, __LINE__
alias :children #{children_of_type.keys.first}
RUBY
else
module_eval <<-RUBY, __FILE__, __LINE__
def children
@children ||= begin
if #{children_of_type.keys.map { |k| "@#{k}.any?" }.join(" || ")}
new_children = []
#{children_of_type.keys.map { |k| "new_children.concat(@#{k})" }.join("; ")}
new_children.freeze
new_children
else
NO_CHILDREN
end
end
end
RUBY
end
end
if defined?(@scalar_methods)
if !method_defined?(:initialize_node)
generate_initialize_node
else
# This method was defined manually
end
else
raise "Can't generate_initialize_node because scalar_methods wasn't called; call it before children_methods"
end
end
# These methods return a plain Ruby value, not another node
# - Add reader methods
# - Add a `#scalars` method
def scalar_methods(*method_names)
if defined?(@scalar_methods)
raise "Can't re-call .scalar_methods for #{self} (already have: #{@scalar_methods})"
else
@scalar_methods = method_names
end
if method_names == [false]
@scalar_methods = []
# skip it
else
module_eval <<-RUBY, __FILE__, __LINE__
# add readers for each scalar
attr_reader #{method_names.map { |m| ":#{m}"}.join(", ")}
def scalars
@scalars ||= [#{method_names.map { |k| "@#{k}" }.join(", ")}].freeze
end
RUBY
end
end
def generate_initialize_node
scalar_method_names = @scalar_methods
# TODO: These probably should be scalar methods, but `types` returns an array
[:types, :description].each do |extra_method|
if method_defined?(extra_method)
scalar_method_names += [extra_method]
end
end
all_method_names = scalar_method_names + @children_methods.keys
if all_method_names.include?(:alias)
# Rather than complicating this special case,
# let it be overridden (in field)
return
else
arguments = scalar_method_names.map { |m| "#{m}: nil"} +
@children_methods.keys.map { |m| "#{m}: NO_CHILDREN" }
assignments = scalar_method_names.map { |m| "@#{m} = #{m}"} +
@children_methods.keys.map { |m| "@#{m} = #{m}.freeze" }
module_eval <<-RUBY, __FILE__, __LINE__
def initialize_node #{arguments.join(", ")}
#{assignments.join("\n")}
end
RUBY
end
end
end
end
# Base class for non-null type names and list type names
class WrapperType < AbstractNode
scalar_methods :of_type
children_methods(false)
end
# Base class for nodes whose only value is a name (no child nodes or other scalars)
class NameOnlyNode < AbstractNode
scalar_methods :name
children_methods(false)
end
# A key-value pair for a field's inputs
class Argument < AbstractNode
scalar_methods :name, :value
children_methods(false)
# @!attribute name
# @return [String] the key for this argument
# @!attribute value
# @return [String, Float, Integer, Boolean, Array, InputObject] The value passed for this key
def children
@children ||= Array(value).flatten.select { |v| v.is_a?(AbstractNode) }
end
end
class Directive < AbstractNode
scalar_methods :name
children_methods(arguments: GraphQL::Language::Nodes::Argument)
end
class DirectiveLocation < NameOnlyNode
end
class DirectiveDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name, :repeatable
children_methods(
locations: Nodes::DirectiveLocation,
arguments: Nodes::Argument,
)
end
# This is the AST root for normal queries
#
# @example Deriving a document by parsing a string
# document = GraphQL.parse(query_string)
#
# @example Creating a string from a document
# document.to_query_string
# # { ... }
#
# @example Creating a custom string from a document
# class VariableScrubber < GraphQL::Language::Printer
# def print_argument(arg)
# "#{arg.name}: <HIDDEN>"
# end
# end
#
# document.to_query_string(printer: VariableScrubber.new)
#
class Document < AbstractNode
scalar_methods false
children_methods(definitions: nil)
# @!attribute definitions
# @return [Array<OperationDefinition, FragmentDefinition>] top-level GraphQL units: operations or fragments
def slice_definition(name)
GraphQL::Language::DefinitionSlice.slice(self, name)
end
end
# An enum value. The string is available as {#name}.
class Enum < NameOnlyNode
end
# A null value literal.
class NullValue < NameOnlyNode
end
# A single selection in a GraphQL query.
class Field < AbstractNode
NONE = [].freeze
scalar_methods :name, :alias
children_methods({
arguments: GraphQL::Language::Nodes::Argument,
selections: GraphQL::Language::Nodes::Field,
directives: GraphQL::Language::Nodes::Directive,
})
# @!attribute selections
# @return [Array<Nodes::Field>] Selections on this object (or empty array if this is a scalar field)
def initialize_node(attributes)
@name = attributes[:name]
@arguments = attributes[:arguments] || NONE
@directives = attributes[:directives] || NONE
@selections = attributes[:selections] || NONE
# oops, alias is a keyword:
@alias = attributes[:alias]
end
# Override this because default is `:fields`
self.children_method_name = :selections
end
# A reusable fragment, defined at document-level.
class FragmentDefinition < AbstractNode
# @!attribute name
# @return [String] the identifier for this fragment, which may be applied with `...#{name}`
# @!attribute type
# @return [String] the type condition for this fragment (name of type which it may apply to)
def initialize_node(name: nil, type: nil, directives: [], selections: [])
@name = name
@type = type
@directives = directives
@selections = selections
end
scalar_methods :name, :type
children_methods({
selections: GraphQL::Language::Nodes::Field,
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
# Application of a named fragment in a selection
class FragmentSpread < AbstractNode
scalar_methods :name
children_methods(directives: GraphQL::Language::Nodes::Directive)
self.children_method_name = :selections
# @!attribute name
# @return [String] The identifier of the fragment to apply, corresponds with {FragmentDefinition#name}
end
# An unnamed fragment, defined directly in the query with `... { }`
class InlineFragment < AbstractNode
scalar_methods :type
children_methods({
selections: GraphQL::Language::Nodes::Field,
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :selections
# @!attribute type
# @return [String, nil] Name of the type this fragment applies to, or `nil` if this fragment applies to any type
end
# A collection of key-value inputs which may be a field argument
class InputObject < AbstractNode
scalar_methods(false)
children_methods(arguments: GraphQL::Language::Nodes::Argument)
# @!attribute arguments
# @return [Array<Nodes::Argument>] A list of key-value pairs inside this input object
# @return [Hash<String, Any>] Recursively turn this input object into a Ruby Hash
def to_h(options={})
arguments.inject({}) do |memo, pair|
v = pair.value
memo[pair.name] = serialize_value_for_hash v
memo
end
end
self.children_method_name = :value
private
def serialize_value_for_hash(value)
case value
when InputObject
value.to_h
when Array
value.map do |v|
serialize_value_for_hash v
end
when Enum
value.name
when NullValue
nil
else
value
end
end
end
# A list type definition, denoted with `[...]` (used for variable type definitions)
class ListType < WrapperType
end
# A non-null type definition, denoted with `...!` (used for variable type definitions)
class NonNullType < WrapperType
end
# An operation-level query variable
class VariableDefinition < AbstractNode
scalar_methods :name, :type, :default_value
children_methods false
# @!attribute default_value
# @return [String, Integer, Float, Boolean, Array, NullValue] A Ruby value to use if no other value is provided
# @!attribute type
# @return [TypeName, NonNullType, ListType] The expected type of this value
# @!attribute name
# @return [String] The identifier for this variable, _without_ `$`
self.children_method_name = :variables
end
# A query, mutation or subscription.
# May be anonymous or named.
# May be explicitly typed (eg `mutation { ... }`) or implicitly a query (eg `{ ... }`).
class OperationDefinition < AbstractNode
scalar_methods :operation_type, :name
children_methods({
variables: GraphQL::Language::Nodes::VariableDefinition,
selections: GraphQL::Language::Nodes::Field,
directives: GraphQL::Language::Nodes::Directive,
})
# @!attribute variables
# @return [Array<VariableDefinition>] Variable $definitions for this operation
# @!attribute selections
# @return [Array<Field>] Root-level fields on this operation
# @!attribute operation_type
# @return [String, nil] The root type for this operation, or `nil` for implicit `"query"`
# @!attribute name
# @return [String, nil] The name for this operation, or `nil` if unnamed
self.children_method_name = :definitions
end
# A type name, used for variable definitions
class TypeName < NameOnlyNode
end
# Usage of a variable in a query. Name does _not_ include `$`.
class VariableIdentifier < NameOnlyNode
self.children_method_name = :value
end
class SchemaDefinition < AbstractNode
include DefinitionNode
scalar_methods :query, :mutation, :subscription
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
class SchemaExtension < AbstractNode
scalar_methods :query, :mutation, :subscription
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
class ScalarTypeDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
class ScalarTypeExtension < AbstractNode
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
class InputValueDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name, :type, :default_value
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :fields
end
class FieldDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name, :type
children_methods({
directives: GraphQL::Language::Nodes::Directive,
arguments: GraphQL::Language::Nodes::InputValueDefinition,
})
self.children_method_name = :fields
# this is so that `children_method_name` of `InputValueDefinition` works properly
# with `#replace_child`
alias :fields :arguments
def merge(new_options)
if (f = new_options.delete(:fields))
new_options[:arguments] = f
end
super
end
end
class ObjectTypeDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name, :interfaces
children_methods({
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::FieldDefinition,
})
self.children_method_name = :definitions
end
class ObjectTypeExtension < AbstractNode
scalar_methods :name, :interfaces
children_methods({
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::FieldDefinition,
})
self.children_method_name = :definitions
end
class InterfaceTypeDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name
children_methods({
interfaces: GraphQL::Language::Nodes::TypeName,
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::FieldDefinition,
})
self.children_method_name = :definitions
end
class InterfaceTypeExtension < AbstractNode
scalar_methods :name
children_methods({
interfaces: GraphQL::Language::Nodes::TypeName,
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::FieldDefinition,
})
self.children_method_name = :definitions
end
class UnionTypeDefinition < AbstractNode
include DefinitionNode
attr_reader :description, :types
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
class UnionTypeExtension < AbstractNode
attr_reader :types
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :definitions
end
class EnumValueDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
})
self.children_method_name = :values
end
class EnumTypeDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
values: GraphQL::Language::Nodes::EnumValueDefinition,
})
self.children_method_name = :definitions
end
class EnumTypeExtension < AbstractNode
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
values: GraphQL::Language::Nodes::EnumValueDefinition,
})
self.children_method_name = :definitions
end
class InputObjectTypeDefinition < AbstractNode
include DefinitionNode
attr_reader :description
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::InputValueDefinition,
})
self.children_method_name = :definitions
end
class InputObjectTypeExtension < AbstractNode
scalar_methods :name
children_methods({
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::InputValueDefinition,
})
self.children_method_name = :definitions
end
end
end
end