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 the code into an abstract syntax tree
-
Analyze/Modify the code with a visitor
-
Print the code back to a string
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:
-
.merge(new_attrs)returns a copy of the node withnew_attrsapplied. This new copy can replace the original node. -
.add_{child}(new_child_attrs)makes a new node withnew_child_attrs, adds it to the array specified by{child}, and returns a copy whose{children}array contains the newly created node.
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:
-
GraphQL::Language::Nodes::Fields are routed to#on_field -
GraphQL::Language::Nodes::Arguments are routed to#on_argument
See the GraphQL::Language::Visitor API docs for a full list of methods.
Each method is called with (node, parent), where:
-
nodeis the AST node currently visited -
parentis the AST node above this node in the tree
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:
-
Make a modified copy of
node -
Pass the modified copy to
super(new_node, parent)
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.)
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.