Skip to content

Commit 0427d59

Browse files
mskacelikjmartisk
authored andcommitted
Fix nested collections parameters in Type-safe client
1 parent 36b25b3 commit 0427d59

File tree

5 files changed

+127
-75
lines changed

5 files changed

+127
-75
lines changed

client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/ParameterInfo.java

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,28 +35,25 @@ public String toString() {
3535
}
3636

3737
public String graphQlInputTypeName() {
38-
if (parameter.isAnnotationPresent(Id.class)) {
39-
if (type.isCollection()) {
40-
return "[ID" + optionalExclamationMark(type.getItemType()) + "]" + optionalExclamationMark(type);
41-
} else {
42-
return "ID" + optionalExclamationMark(type);
43-
}
44-
} else if (type.isCollection()) {
45-
return "[" + withExclamationMark(type.getItemType()) + "]" + optionalExclamationMark(type);
46-
} else if (type.isMap()) {
47-
return "[Entry_" + withExclamationMark(type.getKeyType())
48-
+ "_" + withExclamationMark(type.getValueType()) + "Input]"
49-
+ optionalExclamationMark(type);
50-
} else {
51-
return withExclamationMark(type);
52-
}
38+
return graphQlInputTypeNameRecursion(this.type);
5339
}
5440

55-
private String withExclamationMark(TypeInfo itemType) {
56-
return graphQlInputTypeName(itemType) + optionalExclamationMark(itemType);
41+
private String graphQlInputTypeNameRecursion(TypeInfo type) {
42+
if (type.isCollection()) {
43+
return "[" + graphQlInputTypeNameRecursion(type.getItemType()) + "]" + optionalExclamationMark(type);
44+
}
45+
if (type.isMap()) {
46+
String key = baseTypeName(type.getKeyType()) + optionalExclamationMark(type.getKeyType());
47+
String value = baseTypeName(type.getValueType()) + optionalExclamationMark(type.getValueType());
48+
return "[Entry_" + key + "_" + value + "Input]" + optionalExclamationMark(type);
49+
}
50+
if (parameter.isAnnotationPresent(Id.class)) {
51+
return "ID" + optionalExclamationMark(type);
52+
}
53+
return baseTypeName(type) + optionalExclamationMark(type);
5754
}
5855

59-
private String graphQlInputTypeName(TypeInfo type) {
56+
private String baseTypeName(TypeInfo type) {
6057
if (type.isAnnotated(Input.class)) {
6158
String value = type.getAnnotation(Input.class).value();
6259
if (!value.isEmpty()) {

client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/ParameterModel.java

Lines changed: 59 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import static java.util.stream.Collectors.toList;
99

1010
import java.util.List;
11-
import java.util.function.Function;
1211
import java.util.stream.Stream;
1312

1413
import org.jboss.jandex.MethodParameterInfo;
@@ -53,15 +52,6 @@ public static ParameterModel of(MethodParameterInfo parameter) {
5352
return new ParameterModel(parameter);
5453
}
5554

56-
/**
57-
* Checks if the parameter is associated with a {@link Header} annotation, indicating it is a header parameter.
58-
*
59-
* @return {@code true} if the parameter is a header parameter, otherwise {@code false}.
60-
*/
61-
private boolean isHeaderParameter() {
62-
return parameter.hasAnnotation(Header.class);
63-
}
64-
6555
/**
6656
* Checks if the parameter is a value parameter, either a root or nested parameter.
6757
*
@@ -125,41 +115,24 @@ public String getDirectiveLocation() {
125115
* @return The GraphQL input type name.
126116
*/
127117
public String graphQlInputTypeName() {
128-
if (parameter.hasAnnotation(ID)) {
129-
if (type.isCollectionOrArray()) {
130-
return "[ID" + arrayOrCollectionHelper(this::optionalExclamationMark) + "]" + optionalExclamationMark(type);
131-
}
132-
return "ID" + optionalExclamationMark(type);
133-
} else if (type.isCollectionOrArray()) {
134-
return "[" + arrayOrCollectionHelper(this::withExclamationMark) + "]" + optionalExclamationMark(type);
135-
} else if (type.isMap()) {
136-
var keyType = type.getMapKeyType();
137-
var valueType = type.getMapValueType();
138-
return "[Entry_" + withExclamationMark(keyType)
139-
+ "_" + withExclamationMark(valueType) + "Input]"
140-
+ optionalExclamationMark(type);
141-
} else {
142-
return withExclamationMark(type);
143-
}
118+
return graphQlInputTypeNameRecursion(type);
144119
}
145120

146-
/**
147-
* Adds an exclamation mark to the GraphQL input type name if the type is non-nullable.
148-
*
149-
* @param type The {@link TypeModel} representing the type of the parameter.
150-
* @return The GraphQL input type name with an optional exclamation mark.
151-
*/
152-
private String withExclamationMark(TypeModel type) {
153-
return graphQlInputTypeName(type) + optionalExclamationMark(type);
121+
@Override
122+
public boolean hasDirectives() {
123+
return !directives.isEmpty();
124+
}
125+
126+
@Override
127+
public List<DirectiveInstance> getDirectives() {
128+
return directives;
154129
}
155130

156131
/**
157-
* Gets the GraphQL input type name for the specified {@code TypeModel}.
158-
*
159-
* @param type The {@link TypeModel} for which to get the GraphQL input type name.
160-
* @return The GraphQL input type name.
132+
* Base type name without container wrappers (no [] for collections/maps) and
133+
* without trailing nullability marker.
161134
*/
162-
private String graphQlInputTypeName(TypeModel type) {
135+
private String baseTypeName(TypeModel type) {
163136
if (type.isSimpleClassType() && !type.isScalar()) {
164137
if (type.hasClassAnnotation(INPUT)) {
165138
String value = type.getClassAnnotation(INPUT).orElseThrow().valueWithDefault(getIndex()).asString();
@@ -172,7 +145,7 @@ private String graphQlInputTypeName(TypeModel type) {
172145
}
173146
}
174147
if (Scalars.isScalar(type.getName())) {
175-
return Scalars.getScalar(type.getName()); // returns simplified name
148+
return Scalars.getScalar(type.getName());
176149
}
177150
return type.getSimpleName() + (type.isEnum() ? "" : "Input");
178151
}
@@ -188,22 +161,56 @@ private String optionalExclamationMark(TypeModel type) {
188161
}
189162

190163
/**
191-
* Helper method for handling array or collection types in GraphQL input type names.
164+
* Checks if the parameter is associated with a {@link Header} annotation,
165+
* indicating it is a header parameter.
192166
*
193-
* @param function The function to apply to the array or collection element type.
194-
* @return The GraphQL input type name for array or collection types.
167+
* @return {@code true} if the parameter is a header parameter, otherwise
168+
* {@code false}.
195169
*/
196-
private String arrayOrCollectionHelper(Function<TypeModel, String> function) {
197-
return function.apply((type.isArray() ? type.getArrayElementType() : type.getCollectionElementType()));
170+
private boolean isHeaderParameter() {
171+
return parameter.hasAnnotation(Header.class);
198172
}
199173

200-
@Override
201-
public boolean hasDirectives() {
202-
return !directives.isEmpty();
203-
}
174+
/**
175+
* Gets the GraphQL input type name for the specified {@code TypeModel}.
176+
*
177+
* @param type The {@link TypeModel} for which to get the GraphQL input type
178+
* name.
179+
* @return The GraphQL input type name.
180+
*/
181+
private String graphQlInputTypeNameRecursion(TypeModel type) {
182+
if (type.isCollectionOrArray()) {
183+
TypeModel elementType = type.isArray() ? type.getArrayElementType() : type.getCollectionElementType();
184+
return "[" + graphQlInputTypeNameRecursion(elementType) + "]" + optionalExclamationMark(type);
185+
}
204186

205-
@Override
206-
public List<DirectiveInstance> getDirectives() {
207-
return directives;
187+
if (type.isMap()) {
188+
var keyType = type.getMapKeyType();
189+
var valueType = type.getMapValueType();
190+
String key = baseTypeName(keyType) + optionalExclamationMark(keyType);
191+
String value = baseTypeName(valueType) + optionalExclamationMark(valueType);
192+
return "[Entry_" + key + "_" + value + "Input]" + optionalExclamationMark(type);
193+
}
194+
195+
if (parameter.hasAnnotation(ID)) {
196+
return "ID" + optionalExclamationMark(type);
197+
}
198+
199+
if (type.isSimpleClassType() && !type.isScalar()) {
200+
if (type.hasClassAnnotation(INPUT)) {
201+
String value = type.getClassAnnotation(INPUT).orElseThrow().valueWithDefault(getIndex()).asString();
202+
if (!value.isEmpty()) {
203+
return value + optionalExclamationMark(type);
204+
}
205+
}
206+
if (type.hasClassAnnotation(NAME)) {
207+
return type.getClassAnnotation(NAME).orElseThrow().value().asString() + optionalExclamationMark(type);
208+
}
209+
}
210+
211+
if (Scalars.isScalar(type.getName())) {
212+
return Scalars.getScalar(type.getName()) + optionalExclamationMark(type);
213+
}
214+
return type.getSimpleName() + (type.isEnum() ? "" : "Input") + optionalExclamationMark(type);
208215
}
209216
}

client/model-builder/src/test/java/io/smallrye/graphql/client/model/ClientModelBuilderTest.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.List;
1111
import java.util.Set;
1212

13+
import org.eclipse.microprofile.graphql.Id;
1314
import org.eclipse.microprofile.graphql.Mutation;
1415
import org.eclipse.microprofile.graphql.Name;
1516
import org.eclipse.microprofile.graphql.Query;
@@ -272,9 +273,33 @@ void namespacedClientModelTest() throws IOException {
272273
"mutation FirstSecondUpdate($s: String) { first { second { update(s: $s) } } }");
273274
}
274275

276+
@GraphQLClientApi(configKey = "nested-collection-api")
277+
interface NestedCollectionApi {
278+
@Query
279+
String nestedParameterQuery(List<Collection<Set<String>>> param);
280+
281+
@Query
282+
String nestedParameterWithIdQuery(@Id List<Collection<Set<String>>> idCollection);
283+
}
284+
285+
@Test
286+
void testNestedCollectionParameter() throws IOException {
287+
String configKey = "nested-collection-api";
288+
ClientModels clientModels = ClientModelBuilder
289+
.build(Index.of(NestedCollectionApi.class));
290+
assertNotNull(clientModels.getClientModelByConfigKey(configKey));
291+
ClientModel clientModel = clientModels.getClientModelByConfigKey(configKey);
292+
assertEquals(2, clientModel.getOperationMap().size());
293+
assertOperation(clientModel,
294+
new MethodKey("nestedParameterQuery", new Class[] { List.class }),
295+
"query nestedParameterQuery($param: [[[String]]]) { nestedParameterQuery(param: $param) }");
296+
assertOperation(clientModel,
297+
new MethodKey("nestedParameterWithIdQuery", new Class[] { List.class }),
298+
"query nestedParameterWithIdQuery($idCollection: [[[ID]]]) { nestedParameterWithIdQuery(idCollection: $idCollection) }");
299+
}
300+
275301
private void assertOperation(ClientModel clientModel, MethodKey methodKey, String expectedQuery) {
276302
String actualQuery = clientModel.getOperationMap().get(methodKey);
277303
assertEquals(expectedQuery, actualQuery);
278304
}
279-
280305
}

client/tck/src/main/java/tck/graphql/typesafe/NestedBehavior.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,4 +783,20 @@ void shouldFailToSetMissingPrimitiveField() {
783783

784784
then(thrown).hasMessage("missing boolean value for " + MissingPrimitiveFieldApi.class.getName() + "#call.bar");
785785
}
786+
787+
@GraphQLClientApi
788+
interface MultipleNestedStringApi {
789+
String call(List<List<Set<String>>> input);
790+
}
791+
792+
@Test
793+
void shouldCallMultipleNestedStringQuery() {
794+
fixture.returnsData("'call':'a,b'");
795+
MultipleNestedStringApi api = fixture.build(MultipleNestedStringApi.class);
796+
797+
String result = api.call(List.of(List.of(Set.of("a")), List.of(Set.of("b"))));
798+
799+
then(fixture.query()).isEqualTo("query call($input: [[[String]]]) { call(input: $input) }");
800+
then(result).isEqualTo("a,b");
801+
}
786802
}

client/tck/src/main/java/tck/graphql/typesafe/ScalarBehavior.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,9 @@ String idea(
595595
@Id Long longId,
596596
@Id List<Long> longListId,
597597
@Id Integer intId,
598-
@Id UUID uuidId);
598+
@Id UUID uuidId,
599+
@Id List<List<String>> stringListListId,
600+
@Id String[] stringArrayId);
599601
}
600602

601603
@GraphQLClientApi
@@ -727,7 +729,8 @@ void shouldCallIdQuery() {
727729
IdApi api = fixture.builder().build(IdApi.class);
728730

729731
String out = api.idea("stringId", singletonList("x"), singletonList("x"), singletonList("x"), singletonList("x"),
730-
1L, 2, 3L, singletonList(5L), 4, UUID.randomUUID());
732+
1L, 2, 3L, singletonList(5L), 4, UUID.randomUUID(), singletonList(singletonList("x")),
733+
new String[] { "x" });
731734

732735
then(fixture.query()).isEqualTo("query idea(" +
733736
"$stringId: ID, " +
@@ -740,7 +743,9 @@ void shouldCallIdQuery() {
740743
"$longId: ID, " +
741744
"$longListId: [ID], " +
742745
"$intId: ID, " +
743-
"$uuidId: ID) " +
746+
"$uuidId: ID, " +
747+
"$stringListListId: [[ID]], " +
748+
"$stringArrayId: [ID]) " +
744749
"{ idea(" +
745750
"stringId: $stringId, " +
746751
"stringListId: $stringListId, " +
@@ -752,7 +757,9 @@ void shouldCallIdQuery() {
752757
"longId: $longId, " +
753758
"longListId: $longListId, " +
754759
"intId: $intId, " +
755-
"uuidId: $uuidId) }");
760+
"uuidId: $uuidId, " +
761+
"stringListListId: $stringListListId, " +
762+
"stringArrayId: $stringArrayId) }");
756763
then(out).isEqualTo("out");
757764
}
758765

0 commit comments

Comments
 (0)