Skip to content

Commit

Permalink
Merge pull request #1849 from rmosolgo/1.9-dev-2
Browse files Browse the repository at this point in the history
Try to fix 1.9-dev branch
  • Loading branch information
Robert Mosolgo authored Sep 13, 2018
2 parents 18d5461 + 9a5b2cb commit 52ddbbf
Show file tree
Hide file tree
Showing 66 changed files with 2,500 additions and 1,375 deletions.
1 change: 1 addition & 0 deletions guides/guides.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- name: GraphQL Pro
- name: GraphQL Pro - OperationStore
- name: JavaScript Client
- name: Language Tools
- name: Other
---

Expand Down
143 changes: 143 additions & 0 deletions guides/language_tools/visitor.md
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 }}.)

## 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.
104 changes: 104 additions & 0 deletions guides/type_definitions/field_extensions.md
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
```
8 changes: 2 additions & 6 deletions lib/graphql/compatibility/schema_parser_specification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -595,31 +595,27 @@ def test_it_parses_whole_definition_with_descriptions

assert_equal 6, document.definitions.size

schema_definition = document.definitions.shift
schema_definition, directive_definition, enum_type_definition, object_type_definition, input_object_type_definition, interface_type_definition = document.definitions

assert_equal GraphQL::Language::Nodes::SchemaDefinition, schema_definition.class

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

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

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

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

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

interface_type_definition = document.definitions.shift
assert_equal GraphQL::Language::Nodes::InterfaceTypeDefinition, interface_type_definition.class
assert_equal 'Comment for interface definitions', interface_type_definition.description
assert_equal 'Amount of wheels', interface_type_definition.fields[0].description
Expand Down
Loading

0 comments on commit 52ddbbf

Please sign in to comment.