File: visitor.md

package info (click to toggle)
ruby-graphql 2.2.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,584 kB
  • sloc: ruby: 67,505; ansic: 1,753; yacc: 831; javascript: 331; makefile: 6
file content (143 lines) | stat: -rw-r--r-- 5,252 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
---
layout: guide
doc_stub: false
search: true
section: Language Tools
title: AST Visitor
desc: Analyze and modify parsed GraphQL code
index: 0
---

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

```ruby
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" | api_doc }} turns a string into a GraphQL document:

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

Also, {{ "GraphQL.parse_file" | api_doc }} 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 with `new_attrs` applied. This new copy can replace the original node.
- `.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.

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

```ruby
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" | 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.

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

- {{ "GraphQL::Language::Nodes::Field" | api_doc }}s are routed to `#on_field`
- {{ "GraphQL::Language::Nodes::Argument" | api_doc }}s are routed to `#on_argument`

See the {{ "GraphQL::Language::Visitor" | api_doc }} API docs for a full list of methods.

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

- `node` is the AST node currently visited
- `parent` is 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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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" | api_doc }}.)

## Print

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:

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

You can also create a subclass of {{ "GraphQL::Language::Printer" | api_doc }} to customize how nodes are printed.