Skip to content

Commit ac1c2d2

Browse files
committed
Merge branch '1.4.x'
2 parents 73c2bec + 478fb65 commit ac1c2d2

File tree

2 files changed

+120
-1
lines changed

2 files changed

+120
-1
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@
2525
import java.util.Optional;
2626

2727
import graphql.schema.DataFetchingEnvironment;
28+
import kotlin.jvm.JvmClassMappingKt;
29+
import kotlin.reflect.KClass;
30+
import kotlin.reflect.KFunction;
31+
import kotlin.reflect.KParameter;
32+
import kotlin.reflect.KType;
33+
import kotlin.reflect.full.KClasses;
34+
import kotlin.reflect.jvm.KCallablesJvm;
35+
import kotlin.reflect.jvm.ReflectJvmMapping;
2836
import org.jspecify.annotations.Nullable;
2937

3038
import org.springframework.beans.BeanInstantiationException;
@@ -37,6 +45,7 @@
3745
import org.springframework.beans.TypeMismatchException;
3846
import org.springframework.core.CollectionFactory;
3947
import org.springframework.core.Conventions;
48+
import org.springframework.core.KotlinDetector;
4049
import org.springframework.core.MethodParameter;
4150
import org.springframework.core.ResolvableType;
4251
import org.springframework.core.convert.ConversionService;
@@ -301,7 +310,7 @@ private Map<?, Object> bindMapToMap(
301310
Map<String, Object> dataToBind = new HashMap<>(rawMap);
302311
@Nullable String[] paramNames = BeanUtils.getParameterNames(constructor);
303312
Class<?>[] paramTypes = constructor.getParameterTypes();
304-
@Nullable Object[] constructorArguments = new Object[paramTypes.length];
313+
@Nullable Object[] constructorArguments = new Object[paramNames.length];
305314

306315
for (int i = 0; i < paramNames.length; i++) {
307316
String name = paramNames[i];
@@ -328,6 +337,9 @@ private Map<?, Object> bindMapToMap(
328337
constructorArguments[i] = bindRawValue(
329338
name, rawValue, isNotPresent, targetType, paramTypes[i], bindingResult);
330339
}
340+
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(constructor.getDeclaringClass())) {
341+
KotlinDelegate.rebindKotlinArguments(constructorArguments, constructor);
342+
}
331343

332344
Object target;
333345
try {
@@ -553,4 +565,37 @@ void rejectArgumentValue(
553565
}
554566
}
555567

568+
// remove in favor of https://github.com/spring-projects/spring-framework/issues/33630
569+
private static final class KotlinDelegate {
570+
571+
public static void rebindKotlinArguments(@Nullable Object[] arguments, Constructor<?> constructor) {
572+
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(constructor);
573+
if (function == null) {
574+
return;
575+
}
576+
int index = 0;
577+
for (KParameter parameter : function.getParameters()) {
578+
switch (parameter.getKind()) {
579+
case VALUE, EXTENSION_RECEIVER -> {
580+
Object rawValue = arguments[index];
581+
if (!(parameter.isOptional() && rawValue == null)) {
582+
KType type = parameter.getType();
583+
if (!(type.isMarkedNullable() && rawValue == null) && type.getClassifier() instanceof KClass<?> kClass
584+
&& KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
585+
KFunction<?> argConstructor = KClasses.getPrimaryConstructor(kClass);
586+
if (argConstructor != null) {
587+
if (!KCallablesJvm.isAccessible(argConstructor)) {
588+
KCallablesJvm.setAccessible(argConstructor, true);
589+
}
590+
arguments[index] = argConstructor.call(rawValue);
591+
}
592+
}
593+
}
594+
}
595+
}
596+
index++;
597+
}
598+
}
599+
}
600+
556601
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.data
18+
19+
import com.fasterxml.jackson.core.type.TypeReference
20+
import com.fasterxml.jackson.databind.json.JsonMapper
21+
import graphql.schema.DataFetchingEnvironment
22+
import graphql.schema.DataFetchingEnvironmentImpl
23+
import org.assertj.core.api.Assertions.assertThat
24+
import org.junit.jupiter.api.Test
25+
import org.springframework.core.ResolvableType
26+
import org.springframework.format.support.DefaultFormattingConversionService
27+
import org.springframework.graphql.data.GraphQlArgumentBinder.Options
28+
29+
/**
30+
* Tests for [GraphQlArgumentBinder]
31+
*/
32+
class GraphQlArgumentBinderKotlinTests {
33+
34+
private val mapper = JsonMapper.builder().build()
35+
36+
private val binder = GraphQlArgumentBinder(Options.create().conversionService(DefaultFormattingConversionService()))
37+
38+
@Test
39+
fun bindValueClass() {
40+
val targetType = ResolvableType.forClass(MyDataClassWithValueClass::class.java)
41+
val result = bind(binder, "{\"first\": \"firstValue\", \"second\": \"secondValue\"}", targetType) as MyDataClassWithValueClass
42+
assertThat(result.first).isEqualTo("firstValue")
43+
assertThat(result.second).isEqualTo(MyValueClass("secondValue"))
44+
}
45+
46+
@Test
47+
fun bindValueClassWithDefaultValue() {
48+
val targetType = ResolvableType.forClass(MyDataClassWithDefaultValueClass::class.java)
49+
val result = bind(binder, "{\"first\": \"firstValue\"}", targetType) as MyDataClassWithDefaultValueClass
50+
assertThat(result.first).isEqualTo("firstValue")
51+
52+
}
53+
54+
@Throws(Exception::class)
55+
private fun bind(binder: GraphQlArgumentBinder, json: String, targetType: ResolvableType): Any? {
56+
val typeRef: TypeReference<Map<String, Any>> = object : TypeReference<Map<String, Any>>() {}
57+
val map = this.mapper.readValue("{\"key\":$json}", typeRef)
58+
val environment: DataFetchingEnvironment =
59+
DataFetchingEnvironmentImpl.newDataFetchingEnvironment()
60+
.arguments(map)
61+
.build()
62+
return binder.bind(environment, "key", targetType)
63+
}
64+
65+
data class MyDataClassWithValueClass(val first: String, val second: MyValueClass)
66+
67+
data class MyDataClassWithDefaultValueClass(val first: String, val second: MyValueClass = MyValueClass("secondValue"))
68+
69+
@JvmInline
70+
value class MyValueClass(val value: String) {
71+
72+
}
73+
74+
}

0 commit comments

Comments
 (0)