Skip to content
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

Add @Namespace for GraphQLApi and GraphQLClientApi #2184

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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 @@ -12,8 +12,10 @@
import java.util.List;
import java.util.Map;

import org.eclipse.microprofile.graphql.Name;
import org.jboss.logging.Logger;

import io.smallrye.graphql.api.Namespace;
import io.smallrye.graphql.client.impl.ErrorMessageProvider;
import io.smallrye.graphql.client.impl.GraphQLClientConfiguration;
import io.smallrye.graphql.client.impl.GraphQLClientsConfiguration;
Expand Down Expand Up @@ -145,6 +147,14 @@ public VertxTypesafeGraphQLClientBuilder websocketInitializationTimeout(Integer

@Override
public <T> T build(Class<T> apiClass) {
Name nameAnnotation = apiClass.getAnnotation(Name.class);
Namespace namespaceAnnotation = apiClass.getAnnotation(Namespace.class);

if (nameAnnotation != null && namespaceAnnotation != null) {
throw new RuntimeException("You can only use one of the annotations - @Name or @Namespace " +
"over the GraphQLClientApi interface. Please, fix next interface: " + apiClass.getName());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"over the GraphQLClientApi interface. Please, fix next interface: " + apiClass.getName());
"over the GraphQLClientApi interface. Please, fix the interface: " + apiClass.getName());

}

if (this.options == null) {
this.options = new WebClientOptions();
}
Expand Down Expand Up @@ -185,7 +195,8 @@ public <T> T build(Class<T> apiClass) {
endpoint,
websocketUrl, executeSingleOperationsOverWebsocket, httpClient, webClient, subprotocols,
websocketInitializationTimeout,
allowUnexpectedResponseFields);
allowUnexpectedResponseFields,
namespaceAnnotation != null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would you pass a useNamespace boolean when you have already looked at the annotation and can now already just pass the information about the particular namespace as a List<String>? That would avoid having to use reflection later on.


return apiClass.cast(Proxy.newProxyInstance(getClassLoader(apiClass), new Class<?>[] { apiClass },
(proxy, method, args) -> invoke(graphQLClient, method, args)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class VertxTypesafeGraphQLClientProxy {
private final ClientModel clientModel;
private final boolean executeSingleOperationsOverWebsocket;
private final boolean allowUnexpectedResponseFields;
private final boolean useNamespace;

// Do NOT use this field directly, always retrieve by calling `webSocketHandler()`.
// When a websocket connection is required, then this is populated with a Uni
Expand All @@ -104,7 +105,8 @@ class VertxTypesafeGraphQLClientProxy {
WebClient webClient,
List<WebsocketSubprotocol> subprotocols,
Integer subscriptionInitializationTimeout,
boolean allowUnexpectedResponseFields) {
boolean allowUnexpectedResponseFields,
boolean useNamespace) {
this.api = api;
this.clientModel = clientModel;
this.additionalHeaders = additionalHeaders;
Expand Down Expand Up @@ -134,6 +136,7 @@ class VertxTypesafeGraphQLClientProxy {
this.subprotocols = subprotocols;
this.subscriptionInitializationTimeout = subscriptionInitializationTimeout;
this.allowUnexpectedResponseFields = allowUnexpectedResponseFields;
this.useNamespace = useNamespace;
}

Object invoke(MethodInvocation method) {
Expand Down Expand Up @@ -175,7 +178,7 @@ private Object executeSingleResultOperationOverHttpSync(MethodInvocation method,
}
return new ResultBuilder(method, response.bodyAsString(),
response.statusCode(), response.statusMessage(), convertHeaders(allHeaders),
allowUnexpectedResponseFields).read();
allowUnexpectedResponseFields, useNamespace).read();
}

private Uni<Object> executeSingleResultOperationOverHttpAsync(MethodInvocation method, JsonObject request,
Expand All @@ -193,7 +196,7 @@ private Uni<Object> executeSingleResultOperationOverHttpAsync(MethodInvocation m
return Uni.createFrom().completionStage(postAsync(request.toString(), allHeaders))
.map(response -> new ResultBuilder(method, response.bodyAsString(),
response.statusCode(), response.statusMessage(), convertHeaders(allHeaders),
allowUnexpectedResponseFields).read());
allowUnexpectedResponseFields, useNamespace).read());
} else {
// when all dynamic headers have been obtained, proceed with the request
return Uni.combine().all().unis(unis)
Expand All @@ -202,7 +205,7 @@ private Uni<Object> executeSingleResultOperationOverHttpAsync(MethodInvocation m
.completionStage(postAsync(request.toString(), allHeaders))
.map(response -> new ResultBuilder(method, response.bodyAsString(),
response.statusCode(), response.statusMessage(), convertHeaders(allHeaders),
allowUnexpectedResponseFields).read()));
allowUnexpectedResponseFields, useNamespace).read()));
}
}

Expand Down Expand Up @@ -232,7 +235,7 @@ private Uni<Object> executeSingleResultOperationOverWebsocket(MethodInvocation m
}
})
.onItem().transform(data -> {
Object object = new ResultBuilder(method, data, allowUnexpectedResponseFields).read();
Object object = new ResultBuilder(method, data, allowUnexpectedResponseFields, useNamespace).read();
if (object != null) {
return object;
} else {
Expand All @@ -257,7 +260,7 @@ private Multi<Object> executeSubscriptionOverWebsocket(MethodInvocation method,
handlerRef.get().cancelMulti(operationId.get());
})
.onItem().transform(data -> {
Object object = new ResultBuilder(method, data, allowUnexpectedResponseFields).read();
Object object = new ResultBuilder(method, data, allowUnexpectedResponseFields, useNamespace).read();
if (object != null) {
return object;
} else {
Expand Down Expand Up @@ -303,13 +306,13 @@ private JsonObject request(MethodInvocation method) {
JsonObjectBuilder request = jsonObjectFactory.createObjectBuilder();
String query;
if (clientModel == null) {
query = queryCache.computeIfAbsent(method.getKey(), key -> new QueryBuilder(method).build());
query = queryCache.computeIfAbsent(method.getKey(), key -> new QueryBuilder(method).build(useNamespace));
} else {
query = clientModel.getOperationMap().get(method.getMethodKey());
}
request.add("query", query);
request.add("variables", variables(method));
request.add("operationName", method.getName());
request.add("operationName", method.getOperationName(useNamespace));
JsonObject result = request.build();
log.tracef("full graphql request: %s", result.toString());
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ public QueryBuilder(MethodInvocation method) {
this.method = method;
}

public String build() {
public String build(boolean useNamespace) {
StringBuilder request = new StringBuilder(method.getOperationTypeAsString());
request.append(" ");
request.append(method.getName());
request.append(method.getOperationName(useNamespace));
if (method.hasValueParameters())
request.append(method.valueParameters().map(this::declare).collect(joining(", ", "(", ")")));

String groupName = method.getGroupName();
if (groupName != null) {
request.append(" { ");
request.append(groupName);
if (useNamespace) {
method.getNamespaces().forEach(namespace -> request.append(" { ").append(namespace));
} else if (method.getGroupName() != null) {
request.append(" { ").append(method.getGroupName());
}

if (method.isSingle()) {
Expand All @@ -45,11 +45,15 @@ public String build() {

request.append(fields(method.getReturnType()));

if (method.isSingle())
if (method.isSingle()) {
request.append(" }");
}

if (groupName != null)
request.append(" } ");
if (useNamespace) {
request.append(" }".repeat(method.getNamespaces().size()));
} else if (method.getGroupName() != null) {
request.append(" }");
}

return request.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,27 @@ public class ResultBuilder {
private JsonObject data;
private JsonObject extensions;
private Map<String, List<String>> transportMeta;
private final boolean useNamespace;

public ResultBuilder(MethodInvocation method, String responseString, boolean allowUnexpectedResponseFields) {
this(method, responseString, null, null, null, allowUnexpectedResponseFields);
public ResultBuilder(MethodInvocation method, String responseString, boolean allowUnexpectedResponseFields,
boolean useNamespace) {
this(method, responseString, null, null, null, allowUnexpectedResponseFields, useNamespace);
}

public ResultBuilder(MethodInvocation method,
String responseString,
Integer statusCode,
String statusMessage,
Map<String, List<String>> transportMeta,
boolean allowUnexpectedResponseFields) {
boolean allowUnexpectedResponseFields,
boolean useNamespace) {
this.method = method;
this.statusCode = statusCode;
this.statusMessage = statusMessage;
this.responseString = responseString;
this.transportMeta = transportMeta;
this.response = ResponseReader.parseGraphQLResponse(responseString, allowUnexpectedResponseFields);
this.useNamespace = useNamespace;
}

public Object read() {
Expand Down Expand Up @@ -92,10 +96,17 @@ public Object read() {
private JsonObject readData() {
if (!response.containsKey("data") || response.isNull("data"))
return null;
String groupName = method.getGroupName();
JsonObject data = groupName != null
? response.getJsonObject("data").getJsonObject(groupName)
: response.getJsonObject("data");

JsonObject data = response.getJsonObject("data");

if (useNamespace) {
for (String namespace : method.getNamespaces()) {
data = data.getJsonObject(namespace);
}
} else if (method.getGroupName() != null) {
data = data.getJsonObject(method.getGroupName());
}

if (method.isSingle() && !data.containsKey(method.getName()))
throw new InvalidResponseException("No data for '" + method.getName() + "'");
return data;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.smallrye.graphql.client.impl.typesafe.reflection;

import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

import java.io.Closeable;
Expand All @@ -10,10 +11,12 @@
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

Expand All @@ -23,6 +26,7 @@
import org.eclipse.microprofile.graphql.Name;
import org.eclipse.microprofile.graphql.Query;

import io.smallrye.graphql.api.Namespace;
import io.smallrye.graphql.api.Subscription;
import io.smallrye.graphql.client.core.OperationType;
import io.smallrye.graphql.client.model.MethodKey;
Expand All @@ -37,6 +41,8 @@ public static MethodInvocation of(Method method, Object... args) {
private final Method method;
private final Object[] parameterValues;
private final String groupName;
private List<String> namespaces;
private String operationName;
private List<ParameterInfo> parameters;

private MethodInvocation(TypeInfo type, Method method, Object[] parameterValues) {
Expand Down Expand Up @@ -276,4 +282,36 @@ private String readGroupName(Method method) {
}
return null;
}

public List<String> getNamespaces() {
if (namespaces == null) {
Namespace annotation = method.getDeclaringClass().getAnnotation(Namespace.class);
if (annotation != null) {
namespaces = Arrays.stream(annotation.value().split("/"))
.map(String::trim)
.collect(Collectors.toList());
} else {
namespaces = List.of();
}
}
return namespaces;
}

public String getOperationName(boolean useNamespace) {
if (operationName == null) {
if (useNamespace) {
operationName = getNamespaces().stream().map(this::makeFirstLetterUppercase).collect(joining())
+ makeFirstLetterUppercase(getName());
} else {
operationName = getGroupName() == null
? getName()
: makeFirstLetterUppercase(getGroupName()) + makeFirstLetterUppercase(getName());
}
}
return operationName;
}

private String makeFirstLetterUppercase(String value) {
return value.substring(0, 1).toUpperCase() + value.substring(1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ private static Map<DotName, AnnotationInstance> getAnnotationsWithFilter(Type ty
public static final DotName NON_BLOCKING = DotName.createSimple("io.smallrye.common.annotation.NonBlocking");

// SmallRye GraphQL Annotations (Experimental)
public static final DotName NAMESPACE = DotName.createSimple("io.smallrye.graphql.api.Namespace");
public static final DotName TO_SCALAR = DotName.createSimple("io.smallrye.graphql.api.ToScalar"); // TODO: Remove
public static final DotName ADAPT_TO_SCALAR = DotName.createSimple("io.smallrye.graphql.api.AdaptToScalar");
public static final DotName ADAPT_WITH = DotName.createSimple("io.smallrye.graphql.api.AdaptWith");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package io.smallrye.graphql.client.model;

import static io.smallrye.graphql.client.model.Annotations.GRAPHQL_CLIENT_API;
import static io.smallrye.graphql.client.model.Annotations.NAME;
import static io.smallrye.graphql.client.model.Annotations.NAMESPACE;
import static io.smallrye.graphql.client.model.ScanningContext.getIndex;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.ClassInfo;
Expand Down Expand Up @@ -56,12 +59,15 @@ private ClientModels generateClientModels() {
Collection<AnnotationInstance> graphQLApiAnnotations = getIndex()
.getAnnotations(GRAPHQL_CLIENT_API);

validateAnnotations(graphQLApiAnnotations);

graphQLApiAnnotations.forEach(graphQLApiAnnotation -> {
ClientModel operationMap = new ClientModel();
ClassInfo apiClass = graphQLApiAnnotation.target().asClass();
boolean useNamespace = apiClass.hasDeclaredAnnotation(NAMESPACE);
List<MethodInfo> methods = getAllMethodsIncludingFromSuperClasses(apiClass);
methods.forEach(method -> {
String query = new QueryBuilder(method).build();
String query = new QueryBuilder(method).build(useNamespace);
LOG.debugf("[%s] – Query created: %s", apiClass.name().toString(), query);
operationMap.getOperationMap()
.putIfAbsent(OperationModel.of(method).getMethodKey(), query);
Expand All @@ -75,6 +81,19 @@ private ClientModels generateClientModels() {
return clientModels;
}

private void validateAnnotations(Collection<AnnotationInstance> graphQLApiAnnotations) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we name this validateNamespaceAnnotations to make it more clear what it does?

List<String> errorInterfaces = graphQLApiAnnotations.stream()
.map(annotation -> annotation.target().asClass())
.filter(classInfo -> classInfo.hasDeclaredAnnotation(NAMESPACE) && classInfo.hasDeclaredAnnotation(NAME))
.map(classInfo -> classInfo.name().toString())
.collect(Collectors.toList());
if (!errorInterfaces.isEmpty()) {
throw new RuntimeException("You can only use one of the annotations - @Name or @Namespace " +
"over the GraphQLClientApi interface. Please, fix next interfaces: " +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"over the GraphQLClientApi interface. Please, fix next interfaces: " +
"over the GraphQLClientApi interface. Please, fix the following interfaces: " +

String.join(", ", errorInterfaces));
}
}

/**
* Retrieves all methods, including those from superclasses (interfaces).
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,18 @@ public QueryBuilder(MethodInfo method) {
*
* @return The constructed GraphQL query string.
*/
public String build() {
public String build(boolean useNamespace) {
StringBuilder request = new StringBuilder(method.getOperationTypeAsString());
request.append(" ");
request.append(method.getName()); // operationName
request.append(method.getOperationName(useNamespace));
if (method.hasValueParameters()) {
request.append(method.valueParameters().stream().map(method::declare).collect(joining(", ", "(", ")")));
}

String groupName = method.getGroupName();
if (groupName != null) {
request.append(" { ");
request.append(groupName);
if (useNamespace) {
method.getNamespaces().forEach(namespace -> request.append(" { ").append(namespace));
} else if (method.getGroupName() != null) {
request.append(" { ").append(method.getGroupName());
}

if (method.isSingle()) {
Expand All @@ -61,11 +61,15 @@ public String build() {

request.append(method.fields(method.getReturnType()));

if (method.isSingle())
if (method.isSingle()) {
request.append(" }");
}

if (groupName != null)
request.append(" } ");
if (useNamespace) {
request.append(" }".repeat(method.getNamespaces().size()));
} else if (method.getGroupName() != null) {
request.append(" }");
}

return request.toString();
}
Expand Down
Loading
Loading