-
Notifications
You must be signed in to change notification settings - Fork 5
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
ShapeProjectioninterface is responsible for defining the fields of theShapeinterface in a query, and is also the basic interface for projections of inherited types. - the
ShapeQualificationinterface is responsible for defining fields of inherited types in a query. - the
ShapeQualifiedProjectionis 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
) : ShapeDtoLet'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