Skip to content

support nested fragments #22

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 5 commits into from
May 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -24,7 +24,7 @@ sealed class Execution {

class Fragment(
val condition: TypeCondition,
val elements : List<Execution.Node>,
val elements : List<Execution>,
val directives: Map<Directive, Arguments?>?
) : Execution()

Expand Down
36 changes: 36 additions & 0 deletions src/main/kotlin/com/apurebase/kgraphql/schema/execution/Merge.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.apurebase.kgraphql.schema.execution

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node.ObjectNode

fun MutableMap<String, JsonNode?>.merge(key: String, node: JsonNode?): MutableMap<String, JsonNode?> {
merge(key, node, this::get, this::set)
return this
}

fun ObjectNode.merge(other: ObjectNode) {
other.fields().forEach {
merge(it.key, it.value)
}
}

fun ObjectNode.merge(key: String, node: JsonNode?) {
merge(key, node, this::get, this::set)
}

fun merge(key: String, node: JsonNode?, get: (String) -> JsonNode?, set: (String, JsonNode?) -> Any?) {
val existingNode = get(key)
if (existingNode != null) {
when {
node == null -> throw IllegalStateException("trying to merge null with non-null for $key")
node is ObjectNode -> {
check(existingNode is ObjectNode) { "trying to merge object with simple node for $key" }
existingNode.merge(node)
}
existingNode is ObjectNode -> throw IllegalStateException("trying to merge simple node with object node for $key")
node != existingNode -> throw IllegalStateException("trying to merge different simple nodes for $key")
}
} else {
set(key, node)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,12 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor, Coro
if (include) {
if (expectedType.kind == TypeKind.OBJECT || expectedType.kind == TypeKind.INTERFACE) {
if (expectedType.isInstance(value)) {
return container.elements.map { handleProperty(ctx, value, it, expectedType) }.toMap()
return container.elements.flatMap { child ->
when (child) {
is Execution.Fragment -> handleFragment(ctx, value, child).toList()
else -> listOf(handleProperty(ctx, value, child, expectedType))
}
}.fold(mutableMapOf()) { map, entry -> map.merge(entry.first, entry.second) }
}
} else {
throw IllegalStateException("fragments can be specified on object types, interfaces, and unions")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,8 @@ class RequestInterpreter(val schemaModel: SchemaModel) {
return children
}

private fun handleReturnTypeChildOrFragment(node: SelectionNode, returnType: Type): Execution {
val unwrappedType = returnType.unwrapped()

return when(node){
is Fragment -> {
val conditionType = findFragmentType(node, unwrappedType)
val condition = TypeCondition(conditionType)
val elements = node.fragmentGraph.map { conditionType.handleSelection(it) }
Execution.Fragment(condition, elements, node.directives?.lookup())
}
else -> {
unwrappedType.handleSelection(node)
}
}
}
private fun handleReturnTypeChildOrFragment(node: SelectionNode, returnType: Type) =
returnType.unwrapped().handleSelectionFieldOrFragment(node)

private fun findFragmentType(fragment: Fragment, enclosingType: Type) : Type {
when(fragment){
Expand All @@ -91,6 +78,18 @@ class RequestInterpreter(val schemaModel: SchemaModel) {
}
}

private fun Type.handleSelectionFieldOrFragment(node: SelectionNode): Execution = when(node) {
is Fragment -> {
val conditionType = findFragmentType(node, this)
val condition = TypeCondition(conditionType)
val elements = node.fragmentGraph.map { conditionType.handleSelectionFieldOrFragment(it) }
Execution.Fragment(condition, elements, node.directives?.lookup())
}
else -> {
this.handleSelection(node)
}
}

private fun Type.handleSelection(selectionNode: SelectionNode, variables: List<OperationVariable>? = null): Execution.Node {
val field = this[selectionNode.key]

Expand Down
33 changes: 33 additions & 0 deletions src/test/kotlin/com/apurebase/kgraphql/integration/QueryTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,39 @@ class QueryTest : BaseSchemaTest() {
assertThat(map.extract<Int>("data/film/director/age"), equalTo(prestige.director.age))
}

@Test
fun `query with nested external fragment`() {
val map = execute("""
{
film {
title
...dir
}
}

fragment dir on Film {
director {
name
}
...dirAge
}

fragment dirIntermediate on Film {
...dirAge
}

fragment dirAge on Film {
director {
age
}
}
""".trimIndent())
assertNoErrors(map)
assertThat(map.extract<String>("data/film/title"), equalTo(prestige.title))
assertThat(map.extract<String>("data/film/director/name"), equalTo(prestige.director.name))
assertThat(map.extract<Int>("data/film/director/age"), equalTo(prestige.director.age))
}

@Test
fun `query with missing selection set`(){
expect<RequestException>("Missing selection set on property film of type Film"){
Expand Down
69 changes: 69 additions & 0 deletions src/test/kotlin/com/apurebase/kgraphql/merge/MapMergeTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.apurebase.kgraphql.merge

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node.JsonNodeFactory
import com.apurebase.kgraphql.expect
import com.apurebase.kgraphql.schema.execution.merge
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test

class MapMergeTest {
private val jsonNodeFactory = JsonNodeFactory.instance

@Test
fun `merge should add property`() {
val existing = createMap("param1" to jsonNodeFactory.textNode("value1"))
val update: JsonNode? = jsonNodeFactory.textNode("value2")

existing.merge("param2", update)

assertThat(existing.get("param2"), equalTo(update))
}

@Test
fun `merge should add nested property`() {
val existing = createMap("param1" to jsonNodeFactory.textNode("value1"))
val update: JsonNode? = jsonNodeFactory.objectNode().put("param2", "value2")

existing.merge("sub", update)

assertThat(existing.get("sub"), equalTo(update))
}

@Test
fun `merge should not change simple node`() {
val existingValue: JsonNode? = jsonNodeFactory.textNode("value1")
val existing = createMap("param" to existingValue)
val update = jsonNodeFactory.textNode("value2")

expect<IllegalStateException>("different simple nodes") { existing.merge("param", update) }

assertThat(existing.get("param"), equalTo(existingValue))
}

@Test
fun `merge should not merge simple node with object node`() {
val existingValue: JsonNode? = jsonNodeFactory.textNode("value1")
val existing = createMap("param" to existingValue)
val update = jsonNodeFactory.objectNode()

expect<IllegalStateException>("merge object with simple node") { existing.merge("param", update) }

val expected: JsonNode? = jsonNodeFactory.textNode("value1")
assertThat(existing.get("param"), equalTo(expected))
}

@Test
fun `merge should not merge object node with simple node`() {
val existingObj: JsonNode? = jsonNodeFactory.objectNode().put("other", "value1")
val existing = createMap("param" to existingObj)
val update = jsonNodeFactory.textNode("value2")

expect<IllegalStateException>("merge simple node with object node") { existing.merge("param", update) }

assertThat(existing.get("param"), equalTo(existingObj))
}

private fun createMap(vararg pairs: Pair<String, JsonNode?>) = mutableMapOf(*pairs)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.apurebase.kgraphql.merge

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node.JsonNodeFactory
import com.apurebase.kgraphql.expect
import com.apurebase.kgraphql.schema.execution.merge
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test

class ObjectNodeMergeTest {
private val jsonNodeFactory = JsonNodeFactory.instance

@Test
fun `merge should add property`() {
val existing = jsonNodeFactory.objectNode().put("param1", "value1")
val update = jsonNodeFactory.objectNode().put("param2", "value2")

existing.merge(update)

val expected: JsonNode? = jsonNodeFactory.textNode("value2")
assertThat(existing.get("param2"), equalTo(expected))
}

@Test
fun `merge should add nested property`() {
val existing = jsonNodeFactory.objectNode().put("param1", "value1")
val update = jsonNodeFactory.objectNode()
update.putObject("sub").put("param2", "value2")

existing.merge(update)

val expected: JsonNode? = jsonNodeFactory.objectNode().put("param2", "value2")
assertThat(existing.get("sub"), equalTo(expected))
}

@Test
fun `merge should not change simple node`() {
val existing = jsonNodeFactory.objectNode().put("param", "value1")
val update = jsonNodeFactory.objectNode().put("param", "value2")

expect<IllegalStateException>("different simple nodes") { existing.merge(update) }

val expected: JsonNode? = jsonNodeFactory.textNode("value1")
assertThat(existing.get("param"), equalTo(expected))
}

@Test
fun `merge should not merge simple node with object node`() {
val existing = jsonNodeFactory.objectNode().put("param", "value1")
val update = jsonNodeFactory.objectNode()
update.putObject("param")

expect<IllegalStateException>("merge object with simple node") { existing.merge(update) }

val expected: JsonNode? = jsonNodeFactory.textNode("value1")
assertThat(existing.get("param"), equalTo(expected))
}

@Test
fun `merge should not merge object node with simple node`() {
val existing = jsonNodeFactory.objectNode()
val existingObj: JsonNode? = existing.putObject("param").put("other", "value1")
val update = jsonNodeFactory.objectNode().put("param", "value2")

expect<IllegalStateException>("merge simple node with object node") { existing.merge(update) }

assertThat(existing.get("param"), equalTo(existingObj))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,39 @@ class DocumentParserTest {
)
assertThat(map.first().selectionTree, equalTo(expected))
}

@Test
fun `nested fragment parsing`() {
val map = graphParser.parseDocument("""
{
hero {
id
...heroName
}
}

fragment heroName on Hero {
name {
real
}
...heroNameDetails
}

fragment heroNameDetails on Hero {
name {
asHero
}
}
""".trimIndent())
val expected = SelectionTree(
branch("hero",
leaf("id"),
extFragment("...heroName", "Hero",
branch("name", *leafs("real")),
extFragment("...heroNameDetails", "Hero", branch("name", *leafs("asHero")))
)
)
)
assertThat(map.first().selectionTree, equalTo(expected))
}
}