Skip to content

Commit

Permalink
GraphQL client configuration rework
Browse files Browse the repository at this point in the history
  • Loading branch information
jmartisk committed Jul 1, 2021
1 parent 0bbf085 commit 5b7f51f
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 42 deletions.
6 changes: 3 additions & 3 deletions docs/src/main/asciidoc/smallrye-graphql-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ specify that within the `@GraphQLClientApi` annotation (by setting the `endpoint
or move this over to the configuration file, `application.properties`:

----
star-wars-typesafe/mp-graphql/url=https://swapi-graphql.netlify.app/.netlify/functions/index
quarkus.smallrye-graphql-client.star-wars-typesafe.url=https://swapi-graphql.netlify.app/.netlify/functions/index
----

`star-wars-typesafe` is the name of the configured client instance, and corresponds to the `configKey`
Expand Down Expand Up @@ -274,7 +274,7 @@ representations of the GraphQL types and documents. The client API interface is

We still need to configure the URL for the client, so let's put this into `application.properties`:
----
star-wars-dynamic/mp-graphql/url=https://swapi-graphql.netlify.app/.netlify/functions/index
quarkus.smallrye-graphql-client.star-wars-dynamic.url=https://swapi-graphql.netlify.app/.netlify/functions/index
----

We decided to name the client `star-wars-dynamic`. We will use this name when injecting a dynamic client
Expand All @@ -283,7 +283,7 @@ to properly qualify the injection point.
If you need to add an authorization header, or any other custom HTTP header (in our case
it's not required), this can be done by:
----
star-wars-dynamic/mp-graphql/header/HEADER-KEY=HEADER-VALUE"
quarkus.smallrye-graphql-client.star-wars-dynamic.header.HEADER-KEY=HEADER-VALUE"
----

Add this to the `StarWarsResource` created earlier:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;
import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.inject.Singleton;
Expand All @@ -17,6 +19,7 @@
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.AutoInjectAnnotationBuildItem;
import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
import io.quarkus.arc.deployment.BeanContainerBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.annotations.BuildProducer;
Expand All @@ -27,6 +30,9 @@
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.smallrye.graphql.client.runtime.GraphQLClientConfigurationMergerBean;
import io.quarkus.smallrye.graphql.client.runtime.GraphQLClientSupport;
import io.quarkus.smallrye.graphql.client.runtime.GraphQLClientsConfig;
import io.quarkus.smallrye.graphql.client.runtime.SmallRyeGraphQLClientRecorder;

Expand Down Expand Up @@ -109,21 +115,67 @@ void initializeTypesafeClient(BeanArchiveIndexBuildItem index,
reflectiveClass.produce(ReflectiveClassBuildItem.builder("java.util.Collection").methods(true).build());
}

/**
* io.smallrye.graphql.client.GraphQLClientsConfiguration bean requires knowledge of all interfaces annotated
* with `@GraphQLClientApi`
*/
@BuildStep
@Record(STATIC_INIT)
void setTypesafeApiClasses(BeanArchiveIndexBuildItem index,
BeanContainerBuildItem beanContainerBuildItem,
SmallRyeGraphQLClientRecorder recorder) {
List<String> apiClassNames = new ArrayList<>();
for (AnnotationInstance annotation : index.getIndex().getAnnotations(GRAPHQL_CLIENT_API)) {
ClassInfo apiClassInfo = annotation.target().asClass();
apiClassNames.add(apiClassInfo.name().toString());
}
recorder.setTypesafeApiClasses(apiClassNames);
}

/**
* Allows the optional usage of short class names in GraphQL client configuration rather than
* fully qualified names. This method computes a mapping between short names and qualified names,
* and the configuration merger bean will take it into account when merging Quarkus configuration
* with SmallRye-side configuration.
*/
@BuildStep
@Record(RUNTIME_INIT)
void translateClientConfiguration(BeanArchiveIndexBuildItem index,
void shortNamesToQualifiedNames(BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
SmallRyeGraphQLClientRecorder recorder,
GraphQLClientsConfig config) {
// Map with all classes annotated with @GraphQLApi, the key is its short name,
// value is the fully qualified name. The reason is being able to match short name
// used in the configuration to a class
GraphQLClientsConfig quarkusConfig,
BeanArchiveIndexBuildItem index) {
Map<String, String> shortNamesToQualifiedNames = new HashMap<>();
for (AnnotationInstance annotation : index.getIndex().getAnnotations(GRAPHQL_CLIENT_API)) {
ClassInfo clazz = annotation.target().asClass();
shortNamesToQualifiedNames.put(clazz.name().withoutPackagePrefix(), clazz.name().toString());
}

recorder.translateClientConfiguration(config, shortNamesToQualifiedNames);
RuntimeValue<GraphQLClientSupport> support = recorder.clientSupport(shortNamesToQualifiedNames);

DotName supportClassName = DotName.createSimple(GraphQLClientSupport.class.getName());
SyntheticBeanBuildItem bean = SyntheticBeanBuildItem
.configure(supportClassName)
.addType(supportClassName)
.scope(Singleton.class)
.runtimeValue(support)
.setRuntimeInit()
.unremovable()
.done();
syntheticBeans.produce(bean);
}

@BuildStep
AdditionalBeanBuildItem configurationMergerBean() {
return AdditionalBeanBuildItem.unremovableOf(GraphQLClientConfigurationMergerBean.class);
}

// FIXME: this seems unnecessary, but is needed to make sure that the GraphQLClientConfigurationMergerBean
// gets initialized, can this be done differently?
@BuildStep
@Record(RUNTIME_INIT)
void initializeConfigMergerBean(BeanContainerBuildItem containerBuildItem,
SmallRyeGraphQLClientRecorder recorder) {
recorder.initializeConfigurationMergerBean();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@ public class TypesafeGraphQLClientInjectionTest {
static QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(TestingGraphQLApi.class, TestingGraphQLClientApi.class, Person.class)
.addAsResource(new StringAsset("typesafeclient/mp-graphql/url=" + url),
// TODO: adding headers via config is not supported by typesafe client yet
// + "\n" +
// "typesafeclient/mp-graphql/header/My-Header=My-Value"),
.addAsResource(new StringAsset("typesafeclient/mp-graphql/url=" + url + "\n" +
"typesafeclient/mp-graphql/header/My-Header=My-Value"),
"application.properties")
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"));

Expand All @@ -44,10 +42,14 @@ public void performQuery() {
assertEquals("Arthur", people.get(1).getFirstName());
}

// TODO: adding headers via config is not supported by typesafe client yet
// @Test
// public void checkHeaders() throws ExecutionException, InterruptedException {
// assertEquals("My-Value", client.returnHeader("My-Header"));
// }
/**
* Verify that configured HTTP headers are applied by the client.
* We do this by asking the server side to read the header received from the client and send
* its value back to the client.
*/
@Test
public void checkHeaders() {
assertEquals("My-Value", client.returnHeader("My-Header"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@ public class TypesafeGraphQLClientInjectionWithQuarkusConfigTest {
static QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(TestingGraphQLApi.class, TestingGraphQLClientApi.class, Person.class)
.addAsResource(new StringAsset("quarkus.smallrye-graphql-client.typesafeclient.url=" + url),
// TODO: adding headers via config is not supported by typesafe client yet
// + "\n" +
// "quarkus.smallrye-graphql-client.typesafeclient.header.My-Header=My-Value"),
.addAsResource(new StringAsset("quarkus.smallrye-graphql-client.typesafeclient.url=" + url + "\n" +
"quarkus.smallrye-graphql-client.typesafeclient.header.My-Header=My-Value"),
"application.properties")
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"));

Expand All @@ -44,10 +42,14 @@ public void performQuery() {
assertEquals("Arthur", people.get(1).getFirstName());
}

// TODO: adding headers via config is not supported by typesafe client yet
// @Test
// public void checkHeaders() throws ExecutionException, InterruptedException {
// assertEquals("My-Value", client.returnHeader("My-Header"));
// }
/**
* Verify that configured HTTP headers are applied by the client.
* We do this by asking the server side to read the header received from the client and send
* its value back to the client.
*/
@Test
public void checkHeaders() {
assertEquals("My-Value", client.returnHeader("My-Header"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.quarkus.smallrye.graphql.client.runtime;

import java.util.Map;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Singleton;

import io.smallrye.graphql.client.GraphQLClientConfiguration;
import io.smallrye.graphql.client.GraphQLClientsConfiguration;

/**
* On startup, this beans takes Quarkus-specific configuration of GraphQL clients (quarkus.* properties)
* and merges this configuration with the configuration parsed by SmallRye GraphQL itself (CLIENT/mp-graphql/* properties)
*
* The resulting merged configuration resides in the application-scoped `io.smallrye.graphql.client.GraphQLClientConfiguration`
*
* Quarkus configuration overrides SmallRye configuration where applicable.
*/
@Singleton
public class GraphQLClientConfigurationMergerBean {

@Inject
GraphQLClientsConfiguration upstreamConfiguration;

@Inject
GraphQLClientsConfig quarkusConfiguration;

@Inject
GraphQLClientSupport support;

@PostConstruct
void enhanceGraphQLConfiguration() {
for (Map.Entry<String, GraphQLClientConfig> client : quarkusConfiguration.clients.entrySet()) {
// the raw config key provided in the config, this might be a short class name,
// so translate that into the fully qualified name if applicable
String rawConfigKey = client.getKey();
Map<String, String> shortNamesToQualifiedNamesMapping = support.getShortNamesToQualifiedNamesMapping();
String configKey = shortNamesToQualifiedNamesMapping != null &&
shortNamesToQualifiedNamesMapping.containsKey(rawConfigKey)
? shortNamesToQualifiedNamesMapping.get(rawConfigKey)
: rawConfigKey;

GraphQLClientConfig quarkusConfig = client.getValue();
// if SmallRye configuration does not contain this client, simply use it
if (!upstreamConfiguration.getClients().containsKey(configKey)) {
GraphQLClientConfiguration transformed = new GraphQLClientConfiguration();
transformed.setHeaders(quarkusConfig.headers);
transformed.setUrl(quarkusConfig.url);
upstreamConfiguration.getClients().put(configKey, transformed);
} else {
// if SmallRye configuration already contains this client, override it with the Quarkus configuration
GraphQLClientConfiguration upstreamConfig = upstreamConfiguration.getClients().get(configKey);
if (quarkusConfig.url != null) {
upstreamConfig.setUrl(quarkusConfig.url);
}
// merge the headers
if (quarkusConfig.headers != null) {
upstreamConfig.getHeaders().putAll(quarkusConfig.headers);
}
}
}

}

public void nothing() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkus.smallrye.graphql.client.runtime;

import java.util.Map;

/**
* Items from build time needed to make available at runtime.
*/
public class GraphQLClientSupport {

/**
* Allows the optional usage of short class names in GraphQL client configuration rather than
* fully qualified names. The configuration merger bean will take it into account
* when merging Quarkus configuration with SmallRye-side configuration.
*/
private Map<String, String> shortNamesToQualifiedNamesMapping;

public Map<String, String> getShortNamesToQualifiedNamesMapping() {
return shortNamesToQualifiedNamesMapping;
}

public void setShortNamesToQualifiedNamesMapping(Map<String, String> shortNamesToQualifiedNamesMapping) {
this.shortNamesToQualifiedNamesMapping = shortNamesToQualifiedNamesMapping;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package io.quarkus.smallrye.graphql.client.runtime;

import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import io.quarkus.arc.Arc;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;
import io.smallrye.graphql.client.GraphQLClientsConfiguration;
import io.smallrye.graphql.client.typesafe.api.TypesafeGraphQLClientBuilder;

@Recorder
Expand All @@ -16,21 +21,28 @@ public <T> Supplier<T> typesafeClientSupplier(Class<T> targetClassName) {
};
}

/**
* Translates quarkus.* configuration properties to system properties understood by SmallRye GraphQL.
*/
public void translateClientConfiguration(GraphQLClientsConfig clientsConfig,
Map<String, String> shortNamesToQualifiedNames) {
for (Map.Entry<String, GraphQLClientConfig> client : clientsConfig.clients.entrySet()) {
String configKey = client.getKey();
// the config key can be a short class name, in which case we try to translate it to a fully qualified name
// and use the FQ name in the set property, because that's what SmallRye GraphQL understands
String clientName = shortNamesToQualifiedNames.getOrDefault(configKey, configKey);
GraphQLClientConfig config = client.getValue();
System.setProperty(clientName + "/mp-graphql/url", config.url);
for (Map.Entry<String, String> header : config.headers.entrySet()) {
System.setProperty(clientName + "/mp-graphql/header/" + header.getKey(), header.getValue());
public void setTypesafeApiClasses(List<String> apiClassNames) {
GraphQLClientsConfiguration configBean = Arc.container().instance(GraphQLClientsConfiguration.class).get();
List<Class<?>> classes = apiClassNames.stream().map(className -> {
try {
return Class.forName(className, true, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}).collect(Collectors.toList());
configBean.apiClasses(classes);
}

public RuntimeValue<GraphQLClientSupport> clientSupport(Map<String, String> shortNamesToQualifiedNames) {
GraphQLClientSupport support = new GraphQLClientSupport();
support.setShortNamesToQualifiedNamesMapping(shortNamesToQualifiedNames);
return new RuntimeValue<>(support);
}

public void initializeConfigurationMergerBean() {
GraphQLClientConfigurationMergerBean merger = Arc.container()
.instance(GraphQLClientConfigurationMergerBean.class).get();
merger.nothing();
}

}

0 comments on commit 5b7f51f

Please sign in to comment.