class GraphQL::Language::Nodes::AbstractNode

  • ‘to_query_string` turns an AST node into a GraphQL string
    - `scalars` returns all scalar (Ruby) values attached to this one. Used for comparing nodes.
    - `children` returns all AST nodes attached to this one. Used for tree traversal.
    It provides some APIs for working with ASTs:
    {AbstractNode} is the base class for all nodes in a GraphQL AST.

def ==(other)

Returns:
  • (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

def children

Returns:
  • (Array) - all nodes in the tree below this one
def children
  NO_CHILDREN
end

def children_method_name

def children_method_name
  self.class.children_method_name
end

def children_methods(children_of_type)

- Generate a `#children` method
- Add a persistent update method to add a child
- Add a reader for these children
along with the kind of node they return, if possible.
Name accessors which return lists of nodes,
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 ||= (#{children_of_type.keys.map { |k| "@#{k}" }.join(" + ")}).freeze
        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

def delete_child(previous_child)

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

def generate_initialize_node

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}: []" }
    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

def inherited(child_class)

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

def initialize(options = {})

Parameters:
  • 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

def initialize_copy(other)

This might be unnecessary, but its easiest to add it here.
def initialize_copy(other)
  @children = nil
  @scalars = nil
  @query_string = nil
end

def merge(new_options)

Returns:
  • (AbstractNode) - a shallow copy of `self`

Parameters:
  • new_options (Hash) --
def merge(new_options)
  dup.merge!(new_options)
end

def merge!(new_options)

def merge!(new_options)
  new_options.each do |key, value|
    instance_variable_set(:"@#{key}", value)
  end
  self
end

def position

def position
  [line, col]
end

def replace_child(previous_child, new_child)

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

def scalar_methods(*method_names)

- Add a `#scalars` method
- Add reader methods
These methods return a plain Ruby value, not another node
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 scalars

Returns:
  • (Array) - Scalar values attached to this node
def scalars
  NO_CHILDREN
end

def to_query_string(printer: GraphQL::Language::Printer.new)

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