AST Visitor

GraphQL code is usually contained in a string, for example:

query_string = "query { user(id: \"1\") { userName } }"

You can perform programmatic analysis and modifications to GraphQL code using a three-step process:

Parse

GraphQL.parse turns a string into a GraphQL document:

parsed_doc = GraphQL.parse("{ user(id: \"1\") { userName } }")
# => #<GraphQL::Language::Nodes::Document ...>

Also, GraphQL.parse_file parses the contents of the named file and includes a filename in the parsed document.

AST Nodes

The parsed document is a tree of nodes, called an abstract syntax tree (AST). This tree is immutable: once a document has been parsed, those Ruby objects can’t be changed. Modifications are performed by copying existing nodes, applying changes to the copy, then making a new tree to hold the copied node. Where possible, unmodified nodes are retained in the new tree (it’s persistent).

The copy-and-modify workflow is supported by a few methods on the AST nodes:

For example, to rename a field and add an argument to it, you could:

modified_node = field_node
  # Apply a new name
  .merge(name: "newName")
  # Add an argument to this field's arguments
  .add_argument(name: "newArgument", value: "newValue")

Above, field_node is unmodified, but modified_node reflects the new name and new argument.

Analyze/Modify

To inspect or modify a parsed document, extend GraphQL::Language::Visitor and implement its various hooks. It’s an implementation of the visitor pattern. In short, each node of the tree will be “visited” by calling a method, and those methods can gather information and perform modifications.

In the visitor, each node class has a hook, for example:

See the GraphQL::Language::Visitor API docs for a full list of methods.

Each method is called with (node, parent), where:

The method has a few options for analyzing or modifying the AST:

Continue/Halt

To continue visiting, the hook should call super. This allows the visit to continue to node’s children in the tree, for example:

def on_field(_node, _parent)
  # Do nothing, this is the default behavior:
  super
end

To halt the visit, a method may skip the call to super. For example, if the visitor encountered an error, it might want to return early instead of continuing to visit.

Modify a Node

Visitor hooks are expected to return the (node, parent) they are called with. If they return a different node, then that node will replace the original node. When you call super(node, parent), the node is returned. So, to modify a node and continue visiting:

For example, to rename an argument:

def on_argument(node, parent)
  # make a copy of `node` with a new name
  modified_node = node.merge(name: "renamed")
  # continue visiting with the modified node and parent
  super(modified_node, parent)
end

Delete a Node

To delete the currently-visited node, don’t pass node to super(...). Instead, pass a magic constant, DELETE_NODE, in place of node.

For example, to delete a directive:

def on_directive(node, parent)
  # Don't pass `node` to `super`,
  # instead, pass `DELETE_NODE`
  super(DELETE_NODE, parent)
end

Insert a Node

Inserting nodes is similar to modifying nodes. To insert a new child into node, call one of its .add_ helpers. This returns a copied node with a new child added. For example, to add a selection to a field’s selection set:

def on_field(node, parent)
  node_with_selection = node.add_selection(name: "emailAddress")
  super(node_with_selection, parent)
end

This will add emailAddress the fields selection on node.

(These .add_* helpers are wrappers around GraphQL::Language::Nodes::AbstractNode#merge.)

Print

The easiest way to turn an AST back into a string of GraphQL is GraphQL::Language::Nodes::AbstractNode#to_query_string, for example:

parsed_doc.to_query_string
# => '{ user(id: "1") { userName } }'

You can also create a subclass of GraphQL::Language::Printer to customize how nodes are printed.