Skip to content

Commit

Permalink
Return groups, separate logic between Name and Namespace, add some te…
Browse files Browse the repository at this point in the history
…sts and update documentation
  • Loading branch information
Roman Lovakov committed Sep 11, 2024
1 parent d02c67f commit 10c16d2
Show file tree
Hide file tree
Showing 35 changed files with 1,121 additions and 234 deletions.
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());
}

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);

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.getOperationName());
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,22 +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.getOperationName()); // operation name

if (method.hasValueParameters()) {
request.append(method.getOperationName(useNamespace));
if (method.hasValueParameters())
request.append(method.valueParameters().map(this::declare).collect(joining(", ", "(", ")")));
}

List<String> namespaces = method.getNamespaces();
if (!namespaces.isEmpty()) {
namespaces.forEach(value -> {
request.append(" { ");
request.append(value);
});
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 @@ -54,8 +49,10 @@ public String build() {
request.append(" }");
}

if (!namespaces.isEmpty()) {
request.append(" } ".repeat(namespaces.size()));
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 @@ -94,9 +98,15 @@ private JsonObject readData() {
return null;

JsonObject data = response.getJsonObject("data");
for (String namespace : method.getNamespaces()) {
data = data.getJsonObject(namespace);

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
Expand Up @@ -40,14 +40,16 @@ public static MethodInvocation of(Method method, Object... args) {
private final TypeInfo type;
private final Method method;
private final Object[] parameterValues;
private final List<String> namespaces;
private final String groupName;
private List<String> namespaces;
private String operationName;
private List<ParameterInfo> parameters;

private MethodInvocation(TypeInfo type, Method method, Object[] parameterValues) {
this.type = type;
this.method = method;
this.parameterValues = parameterValues;
this.namespaces = readNamespace(method);
this.groupName = readGroupName(method);
}

@Override
Expand Down Expand Up @@ -266,25 +268,50 @@ public String getOperationTypeAsString() {
}
}

public List<String> getNamespaces() {
return namespaces;
public String getGroupName() {
return groupName;
}

private List<String> readNamespace(Method method) {
Namespace annotation = method.getDeclaringClass().getAnnotation(Namespace.class);
private String readGroupName(Method method) {
Name annotation = method.getDeclaringClass().getAnnotation(Name.class);
if (annotation != null) {
return Arrays.stream(annotation.value().split("/"))
.map(String::trim)
.collect(Collectors.toList());
String groupName = annotation.value().trim();
if (!groupName.isEmpty()) {
return groupName;
}
}
return List.of();
return null;
}

public String getOperationName() {
return namespaces.stream().map(this::prettyString).collect(joining()) + prettyString(getName());
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
? makeFirstLetterUppercase(getName())
: makeFirstLetterUppercase(getGroupName()) + makeFirstLetterUppercase(getName());
}
}
return operationName;
}

private String prettyString(String value) {
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 All @@ -545,7 +546,6 @@ private static Map<DotName, AnnotationInstance> getAnnotationsWithFilter(Type ty
public static final DotName TYPE = DotName.createSimple("org.eclipse.microprofile.graphql.Type");
public static final DotName INTERFACE = DotName.createSimple("org.eclipse.microprofile.graphql.Interface");
public static final DotName UNION = DotName.createSimple("io.smallrye.graphql.api.Union");
public static final DotName NAMESPACE = DotName.createSimple("io.smallrye.graphql.api.Namespace");

public static final DotName MULTIPLE = DotName.createSimple("io.smallrye.graphql.client.typesafe.api.Multiple");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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;
Expand Down Expand Up @@ -58,20 +59,15 @@ private ClientModels generateClientModels() {
Collection<AnnotationInstance> graphQLApiAnnotations = getIndex()
.getAnnotations(GRAPHQL_CLIENT_API);

List<String> errors = graphQLApiAnnotations.stream()
.filter(annotationInstance -> annotationInstance.target().asClass().hasDeclaredAnnotation(NAME))
.map(apiClass -> "Use @Namespace instead of @Name on interface " + apiClass.target().asClass().name())
.collect(Collectors.toList());
if (!errors.isEmpty()) {
throw new RuntimeException(String.join("\n", errors));
}
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 @@ -85,6 +81,19 @@ private ClientModels generateClientModels() {
return clientModels;
}

private void validateAnnotations(Collection<AnnotationInstance> graphQLApiAnnotations) {
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: " +
String.join(", ", errorInterfaces));
}
}

/**
* Retrieves all methods, including those from superclasses (interfaces).
*
Expand Down
Loading

0 comments on commit 10c16d2

Please sign in to comment.