-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1849 from rmosolgo/1.9-dev-2
Try to fix 1.9-dev branch
- Loading branch information
Showing
66 changed files
with
2,500 additions
and
1,375 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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 }}.) | ||
|
||
|
||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
--- | ||
layout: guide | ||
doc_stub: false | ||
search: true | ||
section: Type Definitions | ||
title: Field Extensions | ||
desc: Programmatically modify field configuration and resolution | ||
index: 10 | ||
class_based_api: true | ||
--- | ||
|
||
{{ "GraphQL::Schema::FieldExtension" | api_doc }} provides a way to modify user-defined fields in a programmatic way. For example, Relay connections are implemented as a field extension ({{ "GraphQL::Schema::Field::ConnectionExtension" | api_doc }}). | ||
|
||
### Making a new extension | ||
|
||
Field extensions are subclasses of {{ "GraphQL::Schema::FieldExtension" | api_doc }}: | ||
|
||
```ruby | ||
class MyExtension < GraphQL::Schema::FieldExtension | ||
end | ||
``` | ||
|
||
### Using an extension | ||
|
||
Defined extensions can be added to fields using the `extensions: [...]` option or the `extension(...)` method: | ||
|
||
```ruby | ||
field :name, String, null: false, extensions: [UpcaseExtension] | ||
# or: | ||
field :description, String, null: false do | ||
extension(UpcaseExtension) | ||
end | ||
``` | ||
|
||
See below for how extensions may modify fields. | ||
|
||
### Modifying field configuration | ||
|
||
When extensions are attached, they are initialized with a `field:` and `options:`. Then, `#apply` is called, when they may extend the field they're attached to. For example: | ||
|
||
```ruby | ||
class SearchableExtension < GraphQL::Schema::FieldExtension | ||
def apply | ||
# add an argument to this field: | ||
field.argument(:query, String, required: false, description: "A search query") | ||
end | ||
end | ||
``` | ||
|
||
This way, an extension can encapsulate a behavior requiring several configuration options. | ||
|
||
### Modifying field execution | ||
|
||
Extensions have two hooks that wrap field resolution. Since GraphQL-Ruby supports deferred execution, these hooks _might not_ be called back-to-back. | ||
|
||
First, {{ "GraphQL::Schema::FieldExtension#before_resolve" | api_doc }} is called. `before_resolve` should `yield(object, arguments)` to continue execution. If it doesn't `yield`, then the field won't resolve, and the methods return value will be returned to GraphQL instead. | ||
|
||
After resolution, {{ "GraphQL::Schema::FieldExtension#after_resolve" | api_doc }} is called. Whatever that method returns will be used as the field's return value. | ||
|
||
See the linked API docs for the parameters of those methods. | ||
|
||
#### Execution "memo" | ||
|
||
One parameter to `after_resolve` deserves special attention: `memo:`. `before_resolve` _may_ yield a third value. For example: | ||
|
||
```ruby | ||
def before_resolve(object:, arguments:, **rest) | ||
# yield the current time as `memo` | ||
yield(object, arguments, Time.now.to_i) | ||
end | ||
``` | ||
|
||
If a third value is yielded, it will be passed to `after_resolve` as `memo:`, for example: | ||
|
||
```ruby | ||
def after_resolve(value:, memo:, **rest) | ||
puts "Elapsed: #{Time.now.to_i - memo}" | ||
# Return the original value | ||
value | ||
end | ||
``` | ||
|
||
This allows the `before_resolve` hook to pass data to `after_resolve`. | ||
|
||
Instance variables may not be used because, in a given GraphQL query, the same field may be resolved several times concurrently, and that would result in overriding the instance variable in an unpredictable way. (In fact, extensions are frozen to prevent instance variable writes.) | ||
|
||
### Extension options | ||
|
||
The `extension(...)` method takes an optional second argument, for example: | ||
|
||
```ruby | ||
extension(LimitExtension, limit: 20) | ||
``` | ||
|
||
In this case, `{limit: 20}` will be passed as `options:` to `#initialize` and `options[:limit]` will be `20`. | ||
|
||
For example, options can be used for modifying execution: | ||
|
||
```ruby | ||
def after_resolve(value:, **rest) | ||
# Apply the limit from the options | ||
value.limit(options[:limit]) | ||
end | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.