Skip to content

Commit ae2690b

Browse files
committed
Add visitor guide
1 parent 62cdab5 commit ae2690b

File tree

2 files changed

+144
-0
lines changed

2 files changed

+144
-0
lines changed

guides/guides.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- name: GraphQL Pro
1515
- name: GraphQL Pro - OperationStore
1616
- name: JavaScript Client
17+
- name: Language Tools
1718
- name: Other
1819
---
1920

guides/language_tools/visitor.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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

Comments
 (0)