Skip to content

Commit be34269

Browse files
committed
Add deserialization tests
1 parent 2f72571 commit be34269

3 files changed

Lines changed: 180 additions & 11 deletions

File tree

graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodegenTestClassLoader.kt

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,36 +20,44 @@ package com.netflix.graphql.dgs.codegen
2020

2121
import com.google.testing.compile.Compilation
2222
import java.io.ByteArrayOutputStream
23-
import java.util.Optional
24-
import java.util.concurrent.ConcurrentHashMap
2523
import javax.tools.JavaFileObject
2624

2725
internal class CodegenTestClassLoader(
2826
private val compilation: Compilation,
2927
parent: ClassLoader?,
3028
) : ClassLoader(parent) {
31-
private val seenClasses = ConcurrentHashMap<String, Class<*>>()
29+
private val seenClasses = HashMap<String, Class<*>>()
3230

3331
@Throws(ClassNotFoundException::class)
3432
override fun loadClass(name: String): Class<*> {
3533
val packageNameAsUnixPath = name.replace(".", "/")
3634
val normalizedName = "/CLASS_OUTPUT/$packageNameAsUnixPath.class"
3735

38-
return seenClasses.computeIfAbsent(normalizedName) { _ ->
39-
Optional
40-
.ofNullable(
41-
compilation
42-
.generatedFiles()
43-
.find { it.kind == JavaFileObject.Kind.CLASS && it.name == normalizedName },
44-
).map { fileObject ->
36+
// Guard with a reentrant monitor so that defineClass() triggering a nested loadClass()
37+
// on the same thread (e.g. for a superclass) doesn't deadlock or throw.
38+
synchronized(seenClasses) {
39+
seenClasses[normalizedName]?.let { return it }
40+
41+
val fileObject =
42+
compilation
43+
.generatedFiles()
44+
.find { it.kind == JavaFileObject.Kind.CLASS && it.name == normalizedName }
45+
46+
val loaded =
47+
if (fileObject != null) {
4548
val classData =
4649
fileObject.openInputStream().use { inputStream ->
4750
val buffer = ByteArrayOutputStream()
4851
inputStream.copyTo(buffer)
4952
buffer.toByteArray()
5053
}
5154
defineClass(name, classData, 0, classData.size)
52-
}.orElse(super.loadClass(name))
55+
} else {
56+
super.loadClass(name)
57+
}
58+
59+
seenClasses[normalizedName] = loaded
60+
return loaded
5361
}
5462
}
5563
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
*
3+
* Copyright 2020 Netflix, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package com.netflix.graphql.dgs.codegen
20+
21+
import com.fasterxml.jackson.databind.ObjectMapper
22+
import org.assertj.core.api.Assertions.assertThat
23+
import org.junit.jupiter.api.Test
24+
25+
class JavaDeserializationTest {
26+
private val objectMapper = ObjectMapper()
27+
28+
@Test
29+
fun `interface deserializes to concrete subtype based on __typename`() {
30+
val schema =
31+
"""
32+
type Query {
33+
search: Show
34+
}
35+
36+
interface Show {
37+
title: String
38+
}
39+
40+
type Movie implements Show {
41+
title: String
42+
duration: Int
43+
}
44+
45+
type Series implements Show {
46+
title: String
47+
episodes: Int
48+
}
49+
""".trimIndent()
50+
51+
val codeGenResult =
52+
CodeGen(
53+
CodeGenConfig(
54+
schemas = setOf(schema),
55+
packageName = BASE_PACKAGE_NAME,
56+
),
57+
).generate()
58+
59+
val testClassLoader = assertCompilesJava(codeGenResult).toClassLoader()
60+
val showInterface = testClassLoader.loadClass("$TYPES_PACKAGE_NAME.Show")
61+
val movieClass = testClassLoader.loadClass("$TYPES_PACKAGE_NAME.Movie")
62+
63+
val json = """{"__typename":"Movie","title":"The Matrix","duration":136}"""
64+
val instance = objectMapper.readValue(json, showInterface)
65+
66+
assertThat(instance).isInstanceOf(movieClass)
67+
assertThat(movieClass.getMethod("getTitle").invoke(instance)).isEqualTo("The Matrix")
68+
assertThat(movieClass.getMethod("getDuration").invoke(instance)).isEqualTo(136)
69+
}
70+
71+
@Test
72+
fun `union deserializes to concrete subtype based on __typename`() {
73+
val schema =
74+
"""
75+
type Query {
76+
search: Video
77+
}
78+
79+
union Video = Movie | Series
80+
81+
type Movie {
82+
title: String
83+
duration: Int
84+
}
85+
86+
type Series {
87+
title: String
88+
episodes: Int
89+
}
90+
""".trimIndent()
91+
92+
val codeGenResult =
93+
CodeGen(
94+
CodeGenConfig(
95+
schemas = setOf(schema),
96+
packageName = BASE_PACKAGE_NAME,
97+
),
98+
).generate()
99+
100+
val testClassLoader = assertCompilesJava(codeGenResult).toClassLoader()
101+
val videoInterface = testClassLoader.loadClass("$TYPES_PACKAGE_NAME.Video")
102+
val seriesClass = testClassLoader.loadClass("$TYPES_PACKAGE_NAME.Series")
103+
104+
val json = """{"__typename":"Series","title":"Arrested Development","episodes":68}"""
105+
val instance = objectMapper.readValue(json, videoInterface)
106+
107+
assertThat(instance).isInstanceOf(seriesClass)
108+
assertThat(seriesClass.getMethod("getTitle").invoke(instance)).isEqualTo("Arrested Development")
109+
assertThat(seriesClass.getMethod("getEpisodes").invoke(instance)).isEqualTo(68)
110+
}
111+
}

graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenProjectionTest.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818

1919
package com.netflix.graphql.dgs.codegen.clientapi
2020

21+
import com.fasterxml.jackson.databind.ObjectMapper
22+
import com.netflix.graphql.dgs.client.codegen.BaseSubProjectionNode
2123
import com.netflix.graphql.dgs.codegen.BASE_PACKAGE_NAME
2224
import com.netflix.graphql.dgs.codegen.CodeGen
2325
import com.netflix.graphql.dgs.codegen.CodeGenConfig
2426
import com.netflix.graphql.dgs.codegen.assertCompilesJava
27+
import com.netflix.graphql.dgs.codegen.toClassLoader
2528
import com.palantir.javapoet.TypeVariableName
2629
import org.assertj.core.api.Assertions.assertThat
2730
import org.junit.jupiter.api.Test
@@ -1010,4 +1013,51 @@ class ClientApiGenProjectionTest {
10101013

10111014
assertCompilesJava(codeGenResult.clientProjections + codeGenResult.javaQueryTypes)
10121015
}
1016+
1017+
@Test
1018+
fun `concrete type deserializes JSON matching fields selected by interface subprojection`() {
1019+
val schema =
1020+
"""
1021+
type Query {
1022+
search: Details
1023+
}
1024+
1025+
type Details {
1026+
show: Show
1027+
}
1028+
1029+
interface Show {
1030+
title: String
1031+
}
1032+
1033+
type Movie implements Show {
1034+
title: String
1035+
duration: Int
1036+
}
1037+
""".trimIndent()
1038+
1039+
val codeGenResult =
1040+
CodeGen(
1041+
CodeGenConfig(
1042+
schemas = setOf(schema),
1043+
packageName = BASE_PACKAGE_NAME,
1044+
generateClientApi = true,
1045+
),
1046+
).generate()
1047+
1048+
val testClassLoader = assertCompilesJava(codeGenResult).toClassLoader()
1049+
val showProjection =
1050+
testClassLoader
1051+
.loadClass("$BASE_PACKAGE_NAME.client.ShowProjection")
1052+
.getDeclaredConstructor(BaseSubProjectionNode::class.java, BaseSubProjectionNode::class.java)
1053+
.newInstance(null, null) as BaseSubProjectionNode<*, *>
1054+
1055+
val payload = mutableMapOf<String, Any>("title" to "The Matrix", "duration" to 136)
1056+
showProjection.fields.keys.forEach { payload.putIfAbsent(it, "Movie") }
1057+
val responseJson = ObjectMapper().writeValueAsString(payload)
1058+
1059+
val movieClass = testClassLoader.loadClass("$BASE_PACKAGE_NAME.types.Movie")
1060+
// Should deserialize via ObjectMapper without error, no unrecognized fields
1061+
assertThat(ObjectMapper().readValue(responseJson, movieClass)).isNotNull
1062+
}
10131063
}

0 commit comments

Comments
 (0)