-
Notifications
You must be signed in to change notification settings - Fork 4
Support for GraphQL Abstract Data Types
In the previous article, we got acquainted with the basic concepts of a generated DSL - projections, entities and data transfer objects. Now let's see how Kobby works with abstract GraphQL data types - interfaces and unions.
Let define a GraphQL schema:
type Query {
shapes: [Shape!]!
}
interface Shape {
background: String!
}
type Circle implements Shape {
background: String!
radius: Int!
}
type Rectangle implements Shape {
background: String!
width: Int!
height: Int!
}
This scheme allows us to build GraphQL queries of the form:
query {
shapes {
background
... on Circle {
radius
}
... on Rectangle {
width
height
}
}
}
To support the construction of such queries, Kobby generates the following projection graph:
@ExampleDSL
interface QueryProjection {
fun shapes(__projection: ShapeQualifiedProjection.() -> Unit = {}): Unit
}
@ExampleDSL
interface ShapeProjection {
fun background(): Unit
}
@ExampleDSL
interface ShapeQualification {
fun __onCircle(__projection: CircleProjection.() -> Unit): Unit
fun __onRectangle(__projection: RectangleProjection.() -> Unit): Unit
}
@ExampleDSL
interface ShapeQualifiedProjection : ShapeProjection, ShapeQualification
@ExampleDSL
interface CircleProjection : ShapeProjection {
override fun background(): Unit
fun radius(): Unit
}
@ExampleDSL
interface RectangleProjection : ShapeProjection {
override fun background(): Unit
fun width(): Unit
fun height(): Unit
}
As you can see for the Shape
interface, besides the projection, Kobby generates two more
interfaces: ShapeQualification
and ShapeQualifiedProjection
.
- the
ShapeProjection
interface is responsible for defining the fields of theShape
interface in a query, and is also the basic interface for projections of inherited types. - the
ShapeQualification
interface is responsible for defining fields of inherited types in a query. - the
ShapeQualifiedProjection
is just "projection" + "qualification" interface.
With the help of such additional interfaces for projection, we can build queries for abstract data types:
GraphQL query:
query {
shapes {
background
... on Circle {
radius
}
... on Rectangle {
width
height
}
}
}
Kotlin query:
fun main() = runBlocking {
val context: ExampleContext = exampleContextOf(createMyAdapter())
val response: Query = context.query {
shapes {
background()
__onCircle {
radius()
}
__onRectangle {
width()
height()
}
}
}
}
In the entity graph, the entity Shape
is the base interface for entities of inherited types:
interface Query {
fun __context(): ExampleContext
val shapes: List<Shape>
}
interface Shape {
fun __context(): ExampleContext
val background: String
}
interface Circle : Shape {
override fun __context(): ExampleContext
override val background: String
val radius: Int
}
interface Rectangle : Shape {
override fun __context(): ExampleContext
override val background: String
val width: Int
val height: Int
}
This entity hierarchy allows us to intuitively handle the results of queries to abstract data types:
val response: Query = context.query {
shapes {
background()
__onCircle {
radius()
}
__onRectangle {
width()
height()
}
}
}
response.shapes.forEach { shape: Shape ->
when (shape) {
is Circle ->
println("${shape.background} circle with radius ${shape.radius}")
is Rectangle ->
println(
"${shape.background} rectangle " +
"with width ${shape.width} and height ${shape.height}"
)
}
}
The client-side DSL generated by Kobby automatically adds the __typename
pseudo-field to queries generated for
abstract data types. And in the generated DTO graph, Kobby uses the __typename
property to declare the type hierarchy
in Jackson's annotations. What helps the adapter to deserialize abstract data types.
@JsonTypeName(value = "Query")
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "__typename",
defaultImpl = QueryDto::class
)
@JsonInclude(value = JsonInclude.Include.NON_ABSENT)
data class QueryDto @JsonCreator constructor(
val shapes: List<ShapeDto>? = null
)
// -----------------------------------------------------------------
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "__typename"
)
@JsonSubTypes(
JsonSubTypes.Type(value = CircleDto::class, name = "Circle"),
JsonSubTypes.Type(value = RectangleDto::class, name = "Rectangle")
)
interface ShapeDto {
val background: String?
}
// -----------------------------------------------------------------
@JsonTypeName(value = "Circle")
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "__typename",
defaultImpl = CircleDto::class
)
@JsonInclude(value = JsonInclude.Include.NON_ABSENT)
data class CircleDto(
override val background: String? = null,
val radius: Int? = null
) : ShapeDto
// -----------------------------------------------------------------
@JsonTypeName(value = "Rectangle")
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "__typename",
defaultImpl = RectangleDto::class
)
@JsonInclude(value = JsonInclude.Include.NON_ABSENT)
data class RectangleDto(
override val background: String? = null,
val width: Int? = null,
val height: Int? = null
) : ShapeDto
Let's make a GraphQL union out of the Shape
interface in our example schema, and see how Kobby works with GraphQL
unions:
type Query {
shapes: [Shape!]!
}
union Shape = Circle | Rectangle
type Circle {
radius: Int!
}
type Rectangle {
width: Int!
height: Int!
}
The generated DSL for the new example is identical to the DSL for the old example, but without the background
field in
the generated Shape
interfaces and classes. So the GraphQL union for Kobby is just a marker interface.
@ExampleDSL
interface QueryProjection {
fun shapes(__projection: ShapeQualifiedProjection.() -> Unit = {}): Unit
}
@ExampleDSL
interface ShapeProjection
@ExampleDSL
interface ShapeQualification {
fun __onCircle(__projection: CircleProjection.() -> Unit): Unit
fun __onRectangle(__projection: RectangleProjection.() -> Unit): Unit
}
@ExampleDSL
interface ShapeQualifiedProjection : ShapeProjection, ShapeQualification
@ExampleDSL
interface CircleProjection : ShapeProjection {
fun radius(): Unit
}
@ExampleDSL
interface RectangleProjection : ShapeProjection {
fun width(): Unit
fun height(): Unit
}
GraphQL query:
query {
shapes {
... on Circle {
radius
}
... on Rectangle {
width
height
}
}
}
Kotlin query:
fun main() = runBlocking {
val context: ExampleContext = exampleContextOf(createMyAdapter())
val response: Query = context.query {
shapes {
__onCircle {
radius()
}
__onRectangle {
width()
height()
}
}
}
}
interface Query {
fun __context(): ExampleContext
val shapes: List<Shape>
}
interface Shape {
fun __context(): ExampleContext
}
interface Circle : Shape {
override fun __context(): ExampleContext
val radius: Int
}
interface Rectangle : Shape {
override fun __context(): ExampleContext
val width: Int
val height: Int
}
Handle the results of queries to union:
val response: Query = context.query {
shapes {
__onCircle {
radius()
}
__onRectangle {
width()
height()
}
}
}
response.shapes.forEach { shape: Shape ->
when (shape) {
is Circle ->
println("Circle with radius ${shape.radius}")
is Rectangle ->
println(
"Rectangle with width ${shape.width} and height ${shape.height}"
)
}
}
@JsonTypeName(value = "Query")
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "__typename",
defaultImpl = QueryDto::class
)
@JsonInclude(value = JsonInclude.Include.NON_ABSENT)
data class QueryDto @JsonCreator constructor(
val shapes: List<ShapeDto>? = null
)
// -----------------------------------------------------------------
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "__typename"
)
@JsonSubTypes(
JsonSubTypes.Type(value = CircleDto::class, name = "Circle"),
JsonSubTypes.Type(value = RectangleDto::class, name = "Rectangle")
)
interface ShapeDto
// -----------------------------------------------------------------
@JsonTypeName(value = "Circle")
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "__typename",
defaultImpl = CircleDto::class
)
@JsonInclude(value = JsonInclude.Include.NON_ABSENT)
data class CircleDto @JsonCreator constructor(
val radius: Int? = null
) : ShapeDto
// -----------------------------------------------------------------
@JsonTypeName(value = "Rectangle")
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "__typename",
defaultImpl = RectangleDto::class
)
@JsonInclude(value = JsonInclude.Include.NON_ABSENT)
data class RectangleDto(
val width: Int? = null,
val height: Int? = null
) : ShapeDto