Skip to content

[spec] Implement schema coordinates #6560

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ fun String.parseAsGQLValue(options: ParserOptions = ParserOptions.Default): GQLR
return parseInternal(null, options) { parseValue() }
}

@ApolloExperimental
fun String.parseAsGQLSchemaCoordinate(options: ParserOptions = ParserOptions.Default): GQLResult<GQLSchemaCoordinate> {
return parseInternal(null, options) { parseSchemaCoordinate() }
}

fun String.toGQLValue(options: ParserOptions = ParserOptions.Default): GQLValue {
return parseAsGQLValue(options).getOrThrow()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2026,6 +2026,155 @@ enum class GQLDirectiveLocation {
INPUT_FIELD_DEFINITION,
}

sealed interface GQLSchemaCoordinate

class GQLTypeCoordinate(
override val sourceLocation: SourceLocation?,
val name: String,
): GQLNode, GQLSchemaCoordinate {
override val children: List<GQLNode> = emptyList()

override fun writeInternal(writer: SDLWriter) {
writer.write(name)
}

fun copy(
sourceLocation: SourceLocation? = this.sourceLocation,
name: String = this.name
): GQLTypeCoordinate {
return GQLTypeCoordinate(
sourceLocation,
name
)
}
override fun copyWithNewChildrenInternal(container: NodeContainer): GQLNode {
return copy()
}
}

class GQLDirectiveCoordinate(
override val sourceLocation: SourceLocation?,
val name: String,
): GQLNode, GQLSchemaCoordinate {
override val children: List<GQLNode> = emptyList()

override fun writeInternal(writer: SDLWriter) {
writer.write("@")
writer.write(name)
}

fun copy(
sourceLocation: SourceLocation? = this.sourceLocation,
name: String = this.name
): GQLDirectiveCoordinate {
return GQLDirectiveCoordinate(
sourceLocation,
name
)
}
override fun copyWithNewChildrenInternal(container: NodeContainer): GQLNode {
return copy()
}
}

class GQLMemberCoordinate(
override val sourceLocation: SourceLocation?,
val type: String,
val member: String
): GQLNode, GQLSchemaCoordinate {
override val children: List<GQLNode> = emptyList()

override fun writeInternal(writer: SDLWriter) {
writer.write(type)
writer.write(".")
writer.write(member)
}

fun copy(
sourceLocation: SourceLocation? = this.sourceLocation,
type: String = this.type,
name: String = this.member
): GQLMemberCoordinate {
return GQLMemberCoordinate(
sourceLocation,
type,
name
)
}

override fun copyWithNewChildrenInternal(container: NodeContainer): GQLNode {
return copy()
}
}

class GQLArgumentCoordinate(
override val sourceLocation: SourceLocation?,
val type: String,
val field: String,
val argument: String,
): GQLNode, GQLSchemaCoordinate {
override val children: List<GQLNode> = emptyList()

override fun writeInternal(writer: SDLWriter) {
writer.write(type)
writer.write(".")
writer.write(field)
writer.write("(")
writer.write(argument)
writer.write(":)")
}

fun copy(
sourceLocation: SourceLocation? = this.sourceLocation,
type: String = this.type,
name: String = this.field,
argument: String = this.argument
): GQLArgumentCoordinate {
return GQLArgumentCoordinate(
sourceLocation,
type,
name,
argument
)
}

override fun copyWithNewChildrenInternal(container: NodeContainer): GQLNode {
return copy()
}
}

class GQLDirectiveArgumentCoordinate(
override val sourceLocation: SourceLocation?,
val name: String,
val argument: String,
): GQLNode, GQLSchemaCoordinate {
override val children: List<GQLNode> = emptyList()

override fun writeInternal(writer: SDLWriter) {
writer.write("@")
writer.write(name)
writer.write("(")
writer.write(argument)
writer.write(":)")
}

fun copy(
sourceLocation: SourceLocation? = this.sourceLocation,
name: String = this.name,
argument: String = this.argument
): GQLDirectiveArgumentCoordinate {
return GQLDirectiveArgumentCoordinate(
sourceLocation,
name,
argument
)
}

override fun copyWithNewChildrenInternal(container: NodeContainer): GQLNode {
return copy()
}
}


@Suppress("UNCHECKED_CAST")
class NodeContainer(nodes: List<GQLNode>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.apollographql.apollo.ast

import com.apollographql.apollo.annotations.ApolloExperimental

fun resolveSchemaCoordinate(schema: Schema, coordinate: String): ResolvedSchemaElement? {
return resolveSchemaCoordinate(schema, coordinate.parseAsGQLSchemaCoordinate().getOrThrow())
}

sealed interface ResolvedSchemaElement

class ResolvedType(val typeDefinition: GQLTypeDefinition) : ResolvedSchemaElement
class ResolvedField(val fieldDefinition: GQLFieldDefinition) : ResolvedSchemaElement
class ResolvedInputField(val inputField: GQLInputValueDefinition) : ResolvedSchemaElement
class ResolvedEnumValue(val enumValue: GQLEnumValueDefinition) : ResolvedSchemaElement
class ResolvedFieldArgument(val argument: GQLInputValueDefinition) : ResolvedSchemaElement
class ResolvedDirective(val directiveDefinition: GQLDirectiveDefinition) : ResolvedSchemaElement
class ResolvedDirectiveArgument(val argument: GQLInputValueDefinition) : ResolvedSchemaElement

/**
* Resolves the given schema coordinate according to [schema].
*
* @return the [ResolvedSchemaElement] or `null` if not found.
*
* @throws IllegalArgumentException if any of the containing elements is not found or is of an unexpected type.
*/
@ApolloExperimental
fun resolveSchemaCoordinate(schema: Schema, coordinate: GQLSchemaCoordinate): ResolvedSchemaElement? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect candidate for rich errors

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed!

return when (coordinate) {
is GQLArgumentCoordinate -> {
val typeDefinition =
schema.typeDefinitions.get(coordinate.type) ?: throw IllegalArgumentException("Unknow type '${coordinate.type}'")

val fieldDefinition = when (typeDefinition) {
is GQLObjectTypeDefinition, is GQLInterfaceTypeDefinition -> {
typeDefinition.fieldDefinitions(schema).firstOrNull { it.name == coordinate.field }
?: throw IllegalArgumentException("Unknow field '${coordinate.field}' in type '${typeDefinition.name}'")
}

else -> throw IllegalArgumentException("Expected '${typeDefinition.name}' to be an object or interface type")
}
fieldDefinition.arguments.firstOrNull { it.name == coordinate.argument }?.let { ResolvedFieldArgument(it) }
}

is GQLMemberCoordinate -> {
val typeDefinition =
schema.typeDefinitions.get(coordinate.type) ?: throw IllegalArgumentException("Unknow type '${coordinate.type}'")

when (typeDefinition) {
is GQLObjectTypeDefinition, is GQLInterfaceTypeDefinition -> {
typeDefinition.fieldDefinitions(schema).firstOrNull { it.name == coordinate.member }?.let { ResolvedField(it) }
}

is GQLInputObjectTypeDefinition -> {
typeDefinition.inputFields.firstOrNull { it.name == coordinate.member }?.let { ResolvedInputField(it) }
}

is GQLEnumTypeDefinition -> {
typeDefinition.enumValues.firstOrNull { it.name == coordinate.member }?.let { ResolvedEnumValue(it) }
}

else -> throw IllegalArgumentException("Expected '${typeDefinition.name}' to be an object, input object, interface or enum type")
}
}

is GQLTypeCoordinate -> {
schema.typeDefinitions.get(coordinate.name)?.let { ResolvedType(it) }
}

is GQLDirectiveArgumentCoordinate -> {
val directive =
schema.directiveDefinitions.get(coordinate.name) ?: throw IllegalArgumentException("Unknow directive '@${coordinate.name}'")

directive.arguments.firstOrNull { it.name == coordinate.argument }?.let { ResolvedDirectiveArgument(it) }
}

is GQLDirectiveCoordinate -> {
schema.directiveDefinitions.get(coordinate.name)?.let { ResolvedDirective(it) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.apollographql.apollo.ast

import com.apollographql.apollo.annotations.ApolloInternal
import com.apollographql.apollo.ast.Schema.Companion.NONNULL
import kotlin.reflect.KClass

// 5.5.2.3 Fragment spread is possible
internal fun GQLTypeDefinition.sharesPossibleTypesWith(other: GQLTypeDefinition, schema: Schema): Boolean {
Expand Down Expand Up @@ -69,3 +70,16 @@ fun GQLTypeDefinition.canHaveKeyFields(): Boolean {
else -> false
}
}


internal fun KClass<out GQLTypeDefinition>.prettyName(): String {
return when (this) {
GQLObjectTypeDefinition::class -> "object"
GQLInterfaceTypeDefinition::class -> "interface"
GQLUnionTypeDefinition::class -> "union"
GQLEnumTypeDefinition::class -> "enum"
GQLScalarTypeDefinition::class -> "scalar"
GQLInputValueDefinition::class -> "input object"
else -> error("")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ internal class Lexer(val src: String) {
'(' -> return Token.LeftParenthesis(start, line, column(start))
')' -> return Token.RightParenthesis(start, line, column(start))
'.' -> {
if (pos + 1 < len && src[pos] == '.' && src[pos + 1] == '.') {
if (pos == len || src[pos] != '.') {
return Token.Dot(start, line, column(start))
} else if (pos + 1 < len && src[pos] == '.' && src[pos + 1] == '.') {
pos += 2
return Token.Spread(start, line, column(start))
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.apollographql.apollo.ast.internal

import com.apollographql.apollo.ast.GQLArgument
import com.apollographql.apollo.ast.GQLArgumentCoordinate
import com.apollographql.apollo.ast.GQLBooleanValue
import com.apollographql.apollo.ast.GQLDefinition
import com.apollographql.apollo.ast.GQLDirective
import com.apollographql.apollo.ast.GQLDirectiveArgumentCoordinate
import com.apollographql.apollo.ast.GQLDirectiveCoordinate
import com.apollographql.apollo.ast.GQLDirectiveDefinition
import com.apollographql.apollo.ast.GQLDirectiveLocation
import com.apollographql.apollo.ast.GQLDocument
Expand All @@ -25,6 +28,7 @@ import com.apollographql.apollo.ast.GQLInterfaceTypeDefinition
import com.apollographql.apollo.ast.GQLInterfaceTypeExtension
import com.apollographql.apollo.ast.GQLListType
import com.apollographql.apollo.ast.GQLListValue
import com.apollographql.apollo.ast.GQLMemberCoordinate
import com.apollographql.apollo.ast.GQLNamedType
import com.apollographql.apollo.ast.GQLNonNullType
import com.apollographql.apollo.ast.GQLNullValue
Expand All @@ -36,11 +40,13 @@ import com.apollographql.apollo.ast.GQLOperationDefinition
import com.apollographql.apollo.ast.GQLOperationTypeDefinition
import com.apollographql.apollo.ast.GQLScalarTypeDefinition
import com.apollographql.apollo.ast.GQLScalarTypeExtension
import com.apollographql.apollo.ast.GQLSchemaCoordinate
import com.apollographql.apollo.ast.GQLSchemaDefinition
import com.apollographql.apollo.ast.GQLSchemaExtension
import com.apollographql.apollo.ast.GQLSelection
import com.apollographql.apollo.ast.GQLStringValue
import com.apollographql.apollo.ast.GQLType
import com.apollographql.apollo.ast.GQLTypeCoordinate
import com.apollographql.apollo.ast.GQLTypeDefinition
import com.apollographql.apollo.ast.GQLUnionTypeDefinition
import com.apollographql.apollo.ast.GQLUnionTypeExtension
Expand All @@ -49,6 +55,7 @@ import com.apollographql.apollo.ast.GQLVariableDefinition
import com.apollographql.apollo.ast.GQLVariableValue
import com.apollographql.apollo.ast.ParserOptions
import com.apollographql.apollo.ast.SourceLocation
import kotlin.math.exp

internal class Parser(
src: String,
Expand Down Expand Up @@ -89,6 +96,10 @@ internal class Parser(
return parseTopLevel(::parseTypeInternal)
}

fun parseSchemaCoordinate(): GQLSchemaCoordinate {
return parseTopLevel(::parseSchemaCoordinateInternal)
}

private fun advance() {
lastToken = token
if (lookaheadToken != null) {
Expand Down Expand Up @@ -980,6 +991,39 @@ internal class Parser(
)
}

private fun parseSchemaCoordinateInternal(): GQLSchemaCoordinate {
val sourceLocation = sourceLocation()
return if (token is Token.At) {
advance()
val name = expectToken<Token.Name>().value
if (token is Token.LeftParenthesis) {
advance()
val argument = expectToken<Token.Name>().value
expectToken<Token.Colon>()
expectToken<Token.RightParenthesis>()
GQLDirectiveArgumentCoordinate(sourceLocation, name, argument)
} else {
GQLDirectiveCoordinate(sourceLocation, name)
}
} else {
val name = expectToken<Token.Name>().value
if (token is Token.Dot) {
advance()
val member = expectToken<Token.Name>().value
if (token is Token.LeftParenthesis) {
advance()
val argument = expectToken<Token.Name>().value
expectToken<Token.Colon>()
expectToken<Token.RightParenthesis>()
GQLArgumentCoordinate(sourceLocation, name, member, argument)
} else {
GQLMemberCoordinate(sourceLocation, name, member)
}
} else {
GQLTypeCoordinate(sourceLocation, name)
}
}
}
private fun parseTypeInternal(): GQLType {
val start = token

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,8 @@ internal sealed class Token(val start: kotlin.Int, val end: kotlin.Int, val line
class String(start: kotlin.Int, end: kotlin.Int, line: kotlin.Int, column: kotlin.Int, val value: kotlin.String) : Token(start, end, line, column) {
override fun toString() = "string: \"$value\""
}

class Dot(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) {
override fun toString() = "."
}
}
Loading
Loading