Apollo AST
To generate client code, Apollo Kotlin parses both your GraphQL schema and each operation you write against it into an Abstract Syntax Tree (AST). An AST represents a GraphQL document in a type-safe, machine-readable format.
Apollo Kotlin's parser has its own module (apollo-ast
), which you can use independently of apollo-runtime
or apollo-api
.
Features of apollo-ast
include:
Parsing schema and operation documents into abstract syntax trees (ASTs)
Providing input validation to raise warnings and errors (See the GraphQL spec)
Support for outputting ASTs as valid, indented GraphQL documents
Support for manipulation of ASTs via the
transform
API
Installation
Add the apollo-ast
dependency to your project:
1dependencies {
2 // ...
3
4 implementation("com.apollographql.apollo3:apollo-ast:3.8.5")
5}
Parsing a document
Use the parseAsGQLDocument
method to parse a document from a File
, a String
, or an Okio BufferedSource
.
1val graphQLText = """
2 query HeroForEpisode(${"$"}ep: Episode) {
3 hero(episode: ${"$"}ep) {
4 name
5 friends {
6 height
7 }
8 foobar
9 }
10 }
11""".trimIndent()
12
13val parseResult = Buffer().writeUtf8(graphQLText).parseAsGQLDocument()
This method returns a GQLResult<GQLDocument>
, which contains the document and/or parsing issues, each of which can have a severity of either WARNING
or ERROR
. Because there can be warnings, it is possible to have both a valid document and issues at the same time.
To get the document and throw on errors, use valueAssertNoErrors()
:
1val queryGqlDocument = parseResult.valueAssertNoErrors()
GQLDocument
is the root of the AST. It contains a list of GQLDefinition
s that together represent the GraphQL document.
All nodes in an AST are subclasses of GQLNode
(all named with the GQL
prefix). Each subclass exposes specific properties and methods relevant to the corresponding node type.
Example AST structure
In the HeroForEpisode
example above, here's the structure of the AST returned by the parser:
1GQLDocument
2 └─GQLOperationDefinition query "HeroForEpisode"
3 ├─GQLVariableDefinition "ep": "Episode"
4 └─GQLSelectionSet
5 └─GQLField "hero"
6 ├─GQLSelectionSet
7 │ ├─GQLField "name"
8 │ ├─GQLField "friends"
9 │ │ └─GQLSelectionSet
10 │ │ └─GQLField "height"
11 │ └─GQLField "foobar"
12 └─GQLArguments
13 └─GQLArgument "episode"
14 └─GQLVariableValue "ep"
Note that this structure and its node names closely follow the GraphQL specification.
Validating input
In addition to parsing, the apollo-ast
library provides methods to perform higher-level validation of GraphQL documents.
To validate a parsed GQLDocument
:
If the document represents a schema, call the
validateAsSchema
method.If the document represents one or more operations, call the
validateAsExecutable
method.
validateAsSchema
validateAsSchema
returns a GQLResult<Schema>
. The following snippet parses and validates a short invalid schema that uses an undefined directive (@private
):
1val schemaText = """
2 type Query {
3 hero(episode: Episode): Character
4 }
5
6 enum Episode {
7 NEWHOPE
8 EMPIRE
9 }
10
11 type Character @private {
12 name: String
13 height: Int @deprecated
14 friends: [Character]
15 }
16""".trimIndent()
17
18val schemaGQLDocument = Buffer().writeUtf8(schemaText).parseAsGQLDocument().valueAssertNoErrors()
19val schemaResult = schemaGQLDocument.validateAsSchema()
20println(schemaResult.issues.map { it.severity.name + ": " + it.message })
When executed, this snippet prints [WARNING: Unknown directive 'private']
.
Because this is a warning and not an error, you can still use the returned schemaResult.valueAssertNoErrors()
validateAsExecutable
The validateAsExecutable
method checks whether a document's defined operations are valid against a particular provided Schema
. You can obtain this Schema
parameter by calling the above validateAsSchema
on the GQLDocument
that represents the schema:
1val schema = schemaGQLDocument.validateAsSchema().valueAssertNoErrors()
2val executableIssues = queryGqlDocument.validateAsExecutable(schema)
3println(executableIssues.map { it.severity.name + ": " + it.message })
If the queryGqlDocument
queries a deprecated field and misspells another, this snippet might print the following:
1[WARNING: Use of deprecated field 'height', ERROR: Can't query 'frends' on type 'Character']
Outputting SDL
You can output a GQLDocument
to standard GraphQL syntax with the toUtf8
extensions:
1// Returns a string
2println(queryGqlDocument.toUtf8())
3
4// Output to a File
5queryGqlDocument.toUtf8(file)
6
7// Output to an Okio BufferedSink
8queryGqlDocument.toUtf8(sink)
Transforming an AST
You can use the transform
method of GQLDocument
to modify an existing AST.
You pass transform
a lambda that accepts a GQLNode
and also returns instructions to manipulate the AST:
Continue
: keep the node as-is and continue visiting the childrenDelete
: delete the nodeReplace(GQLNode)
: replace the node with the given one
The transform
method traverses the AST and executes the lambda for each node, then acts on the AST according to the lambda's return value.
Note that with Delete
and Replace
, the node's children are not visited automatically, so you should call transform
recursively if that is needed.
For example, this snippet removes all fields named restrictedField
from operations defined in queryGqlDocument
and prints the result:
1val transformedQuery = queryGqlDocument.transform{ node ->
2 if (node is GQLField && node.name == "restrictedField") {
3 TransformResult.Delete
4 } else {
5 TransformResult.Continue
6 }
7}
8println(transformedQuery!!.toUtf8())