Skip to content

Commit 52ddbbf

Browse files
author
Robert Mosolgo
authored
Merge pull request #1849 from rmosolgo/1.9-dev-2
Try to fix 1.9-dev branch
2 parents 18d5461 + 9a5b2cb commit 52ddbbf

File tree

66 files changed

+2500
-1375
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+2500
-1375
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.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
layout: guide
3+
doc_stub: false
4+
search: true
5+
section: Type Definitions
6+
title: Field Extensions
7+
desc: Programmatically modify field configuration and resolution
8+
index: 10
9+
class_based_api: true
10+
---
11+
12+
{{ "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 }}).
13+
14+
### Making a new extension
15+
16+
Field extensions are subclasses of {{ "GraphQL::Schema::FieldExtension" | api_doc }}:
17+
18+
```ruby
19+
class MyExtension < GraphQL::Schema::FieldExtension
20+
end
21+
```
22+
23+
### Using an extension
24+
25+
Defined extensions can be added to fields using the `extensions: [...]` option or the `extension(...)` method:
26+
27+
```ruby
28+
field :name, String, null: false, extensions: [UpcaseExtension]
29+
# or:
30+
field :description, String, null: false do
31+
extension(UpcaseExtension)
32+
end
33+
```
34+
35+
See below for how extensions may modify fields.
36+
37+
### Modifying field configuration
38+
39+
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:
40+
41+
```ruby
42+
class SearchableExtension < GraphQL::Schema::FieldExtension
43+
def apply
44+
# add an argument to this field:
45+
field.argument(:query, String, required: false, description: "A search query")
46+
end
47+
end
48+
```
49+
50+
This way, an extension can encapsulate a behavior requiring several configuration options.
51+
52+
### Modifying field execution
53+
54+
Extensions have two hooks that wrap field resolution. Since GraphQL-Ruby supports deferred execution, these hooks _might not_ be called back-to-back.
55+
56+
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.
57+
58+
After resolution, {{ "GraphQL::Schema::FieldExtension#after_resolve" | api_doc }} is called. Whatever that method returns will be used as the field's return value.
59+
60+
See the linked API docs for the parameters of those methods.
61+
62+
#### Execution "memo"
63+
64+
One parameter to `after_resolve` deserves special attention: `memo:`. `before_resolve` _may_ yield a third value. For example:
65+
66+
```ruby
67+
def before_resolve(object:, arguments:, **rest)
68+
# yield the current time as `memo`
69+
yield(object, arguments, Time.now.to_i)
70+
end
71+
```
72+
73+
If a third value is yielded, it will be passed to `after_resolve` as `memo:`, for example:
74+
75+
```ruby
76+
def after_resolve(value:, memo:, **rest)
77+
puts "Elapsed: #{Time.now.to_i - memo}"
78+
# Return the original value
79+
value
80+
end
81+
```
82+
83+
This allows the `before_resolve` hook to pass data to `after_resolve`.
84+
85+
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.)
86+
87+
### Extension options
88+
89+
The `extension(...)` method takes an optional second argument, for example:
90+
91+
```ruby
92+
extension(LimitExtension, limit: 20)
93+
```
94+
95+
In this case, `{limit: 20}` will be passed as `options:` to `#initialize` and `options[:limit]` will be `20`.
96+
97+
For example, options can be used for modifying execution:
98+
99+
```ruby
100+
def after_resolve(value:, **rest)
101+
# Apply the limit from the options
102+
value.limit(options[:limit])
103+
end
104+
```

lib/graphql/compatibility/schema_parser_specification.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -595,31 +595,27 @@ def test_it_parses_whole_definition_with_descriptions
595595

596596
assert_equal 6, document.definitions.size
597597

598-
schema_definition = document.definitions.shift
598+
schema_definition, directive_definition, enum_type_definition, object_type_definition, input_object_type_definition, interface_type_definition = document.definitions
599+
599600
assert_equal GraphQL::Language::Nodes::SchemaDefinition, schema_definition.class
600601

601-
directive_definition = document.definitions.shift
602602
assert_equal GraphQL::Language::Nodes::DirectiveDefinition, directive_definition.class
603603
assert_equal 'This is a directive', directive_definition.description
604604

605-
enum_type_definition = document.definitions.shift
606605
assert_equal GraphQL::Language::Nodes::EnumTypeDefinition, enum_type_definition.class
607606
assert_equal "Multiline comment\n\nWith an enum", enum_type_definition.description
608607

609608
assert_nil enum_type_definition.values[0].description
610609
assert_equal 'Not a creative color', enum_type_definition.values[1].description
611610

612-
object_type_definition = document.definitions.shift
613611
assert_equal GraphQL::Language::Nodes::ObjectTypeDefinition, object_type_definition.class
614612
assert_equal 'Comment without preceding space', object_type_definition.description
615613
assert_equal 'And a field to boot', object_type_definition.fields[0].description
616614

617-
input_object_type_definition = document.definitions.shift
618615
assert_equal GraphQL::Language::Nodes::InputObjectTypeDefinition, input_object_type_definition.class
619616
assert_equal 'Comment for input object types', input_object_type_definition.description
620617
assert_equal 'Color of the car', input_object_type_definition.fields[0].description
621618

622-
interface_type_definition = document.definitions.shift
623619
assert_equal GraphQL::Language::Nodes::InterfaceTypeDefinition, interface_type_definition.class
624620
assert_equal 'Comment for interface definitions', interface_type_definition.description
625621
assert_equal 'Amount of wheels', interface_type_definition.fields[0].description

0 commit comments

Comments
 (0)