|
| 1 | +--- |
| 2 | +layout: guide |
| 3 | +doc_stub: false |
| 4 | +search: true |
| 5 | +section: Language Tools |
| 6 | +title: AST Visitor |
| 7 | +desc: Analyze and modify parsed GraphQL code |
| 8 | +index: 0 |
| 9 | +--- |
| 10 | + |
| 11 | +GraphQL code is usually contained in a string, for example: |
| 12 | + |
| 13 | +```ruby |
| 14 | +query_string = "query { user(id: \"1\") { userName } }" |
| 15 | +``` |
| 16 | + |
| 17 | +You can perform programmatic analysis and modifications to GraphQL code using a three-step process: |
| 18 | + |
| 19 | +- __Parse__ the code into an abstract syntax tree |
| 20 | +- __Analyze/Modify__ the code with a visitor |
| 21 | +- __Print__ the code back to a string |
| 22 | + |
| 23 | +## Parse |
| 24 | + |
| 25 | +{{ "GraphQL.parse" | api_doc }} turns a string into a GraphQL document: |
| 26 | + |
| 27 | +```ruby |
| 28 | +parsed_doc = GraphQL.parse("{ user(id: \"1\") { userName } }") |
| 29 | +# => #<GraphQL::Language::Nodes::Document ...> |
| 30 | +``` |
| 31 | + |
| 32 | +Also, {{ "GraphQL.parse_file" | api_doc }} parses the contents of the named file and includes a `filename` in the parsed document. |
| 33 | + |
| 34 | +#### AST Nodes |
| 35 | + |
| 36 | +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_). |
| 37 | + |
| 38 | +The copy-and-modify workflow is supported by a few methods on the AST nodes: |
| 39 | + |
| 40 | +- `.merge(new_attrs)` returns a copy of the node with `new_attrs` applied. This new copy can replace the original node. |
| 41 | +- `.add_{child}(new_child_attrs)` makes a new node with `new_child_attrs`, adds it to the array specified by `{child}`, and returns a copy whose `{children}` array contains the newly created node. |
| 42 | + |
| 43 | +For example, to rename a field and add an argument to it, you could: |
| 44 | + |
| 45 | +```ruby |
| 46 | +modified_node = field_node |
| 47 | + # Apply a new name |
| 48 | + .merge(name: "newName") |
| 49 | + # Add an argument to this field's arguments |
| 50 | + .add_argument(name: "newArgument", value: "newValue") |
| 51 | +``` |
| 52 | + |
| 53 | +Above, `field_node` is unmodified, but `modified_node` reflects the new name and new argument. |
| 54 | + |
| 55 | +## Analyze/Modify |
| 56 | + |
| 57 | +To inspect or modify a parsed document, extend {{ "GraphQL::Language::Visitor" | api_doc }} and implement its various hooks. It's an implementation of the [visitor pattern](https://en.wikipedia.org/wiki/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. |
| 58 | + |
| 59 | +In the visitor, each node class has a hook, for example: |
| 60 | + |
| 61 | +- {{ "GraphQL::Language::Nodes::Field" | api_doc }}s are routed to `#on_field` |
| 62 | +- {{ "GraphQL::Language::Nodes::Argument" | api_doc }}s are routed to `#on_argument` |
| 63 | + |
| 64 | +See the {{ "GraphQL::Language::Visitor" | api_doc }} API docs for a full list of methods. |
| 65 | + |
| 66 | +Each method is called with `(node, parent)`, where: |
| 67 | + |
| 68 | +- `node` is the AST node currently visited |
| 69 | +- `parent` is the AST node above this node in the tree |
| 70 | + |
| 71 | +The method has a few options for analyzing or modifying the AST: |
| 72 | + |
| 73 | +#### Continue/Halt |
| 74 | + |
| 75 | +To continue visiting, the hook should call `super`. This allows the visit to continue to `node`'s children in the tree, for example: |
| 76 | + |
| 77 | +```ruby |
| 78 | +def on_field(_node, _parent) |
| 79 | + # Do nothing, this is the default behavior: |
| 80 | + super |
| 81 | +end |
| 82 | +``` |
| 83 | + |
| 84 | +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. |
| 85 | + |
| 86 | +#### Modify a Node |
| 87 | + |
| 88 | +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: |
| 89 | + |
| 90 | +- Make a modified copy of `node` |
| 91 | +- Pass the modified copy to `super(new_node, parent)` |
| 92 | + |
| 93 | +For example, to rename an argument: |
| 94 | + |
| 95 | +```ruby |
| 96 | +def on_argument(node, parent) |
| 97 | + # make a copy of `node` with a new name |
| 98 | + modified_node = node.merge(name: "renamed") |
| 99 | + # continue visiting with the modified node and parent |
| 100 | + super(modified_node, parent) |
| 101 | +end |
| 102 | +``` |
| 103 | + |
| 104 | +#### Delete a Node |
| 105 | + |
| 106 | +To delete the currently-visited `node`, don't pass `node` to `super(...)`. Instead, pass a magic constant, `DELETE_NODE`, in place of `node`. |
| 107 | + |
| 108 | +For example, to delete a directive: |
| 109 | + |
| 110 | +```ruby |
| 111 | +def on_directive(node, parent) |
| 112 | + # Don't pass `node` to `super`, |
| 113 | + # instead, pass `DELETE_NODE` |
| 114 | + super(DELETE_NODE, parent) |
| 115 | +end |
| 116 | +``` |
| 117 | + |
| 118 | +#### Insert a Node |
| 119 | + |
| 120 | +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: |
| 121 | + |
| 122 | +```ruby |
| 123 | +def on_field(node, parent) |
| 124 | + node_with_selection = node.add_selection(name: "emailAddress") |
| 125 | + super(node_with_selection, parent) |
| 126 | +end |
| 127 | +``` |
| 128 | + |
| 129 | +This will add `emailAddress` the fields selection on `node`. |
| 130 | + |
| 131 | + |
| 132 | +(These `.add_*` helpers are wrappers around {{ "GraphQL::Language::Nodes::AbstractNode#merge" | api_doc }}.) |
| 133 | + |
| 134 | +## Print |
| 135 | + |
| 136 | +The easiest way to turn an AST back into a string of GraphQL is {{ "GraphQL::Language::Nodes::AbstractNode#to_query_string" | api_doc }}, for example: |
| 137 | + |
| 138 | +```ruby |
| 139 | +parsed_doc.to_query_string |
| 140 | +# => '{ user(id: "1") { userName } }' |
| 141 | +``` |
| 142 | + |
| 143 | +You can also create a subclass of {{ "GraphQL::Language::Printer" | api_doc }} to customize how nodes are printed. |
0 commit comments