Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit e7b7e68

Browse files
feat: define aliased scalars from configurations
At this stage it only works for standard and extended scalars (not custom scalar beans)
1 parent 92b3452 commit e7b7e68

File tree

7 files changed

+219
-2
lines changed

7 files changed

+219
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package graphql.kickstart.autoconfigure.scalars;
2+
3+
import static graphql.kickstart.autoconfigure.scalars.GraphQLScalarUtils.extractScalarDefinitions;
4+
5+
import graphql.Scalars;
6+
import graphql.scalars.ExtendedScalars;
7+
import graphql.schema.GraphQLScalarType;
8+
import java.util.Arrays;
9+
import java.util.Collections;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.stream.Collectors;
13+
import lombok.NonNull;
14+
import lombok.RequiredArgsConstructor;
15+
import org.springframework.context.ApplicationContextException;
16+
import org.springframework.context.ApplicationContextInitializer;
17+
import org.springframework.context.support.GenericApplicationContext;
18+
import org.springframework.core.env.ConfigurableEnvironment;
19+
import org.springframework.core.env.EnumerablePropertySource;
20+
21+
@RequiredArgsConstructor
22+
public class GraphQLAliasedScalarsInitializer
23+
implements ApplicationContextInitializer<GenericApplicationContext> {
24+
25+
private static final String GRAPHQL_ALIASED_SCALAR_PREFIX = "graphql.aliased-scalars.";
26+
private static final String JOINING_SEPARATOR = ", ";
27+
private static final String NO_BUILT_IN_SCALAR_FOUND
28+
= "Scalar(s) '%s' cannot be aliased. "
29+
+ "Only the following scalars can be aliased by configuration: %s. "
30+
+ "Note that custom scalar beans cannot be aliased this way.";
31+
32+
@Override
33+
public void initialize(@NonNull final GenericApplicationContext applicationContext) {
34+
final Map<String, GraphQLScalarType> predefinedScalars
35+
= extractScalarDefinitions(Scalars.class, ExtendedScalars.class);
36+
final ConfigurableEnvironment environment = applicationContext.getEnvironment();
37+
verifyAliasedScalarConfiguration(predefinedScalars, environment);
38+
predefinedScalars.forEach((scalarName, scalarType) ->
39+
((List<?>) environment.getProperty(GRAPHQL_ALIASED_SCALAR_PREFIX + scalarName,
40+
List.class, Collections.emptyList()))
41+
.stream()
42+
.map(String::valueOf)
43+
.map(alias -> ExtendedScalars.newAliasedScalar(alias).aliasedScalar(scalarType).build())
44+
.forEach(aliasedScalar -> applicationContext.registerBean(aliasedScalar.getName(),
45+
GraphQLScalarType.class, () -> aliasedScalar)));
46+
}
47+
48+
private void verifyAliasedScalarConfiguration(
49+
final Map<String, GraphQLScalarType> predefinedScalars,
50+
final ConfigurableEnvironment environment) {
51+
final List<String> invalidScalars = environment.getPropertySources().stream()
52+
.filter(pSource -> pSource instanceof EnumerablePropertySource)
53+
.map(pSource -> (EnumerablePropertySource<?>) pSource)
54+
.map(EnumerablePropertySource::getPropertyNames)
55+
.flatMap(Arrays::stream)
56+
.filter(pName -> pName.startsWith(GRAPHQL_ALIASED_SCALAR_PREFIX))
57+
.map(pName -> pName.replace(GRAPHQL_ALIASED_SCALAR_PREFIX, ""))
58+
.filter(scalarName -> !predefinedScalars.containsKey(scalarName))
59+
.sorted()
60+
.collect(Collectors.toList());
61+
if (!invalidScalars.isEmpty()) {
62+
final String validBuildInScalars = predefinedScalars.keySet().stream().sorted()
63+
.collect(Collectors.joining(JOINING_SEPARATOR));
64+
throw new ApplicationContextException(String.format(NO_BUILT_IN_SCALAR_FOUND,
65+
String.join(JOINING_SEPARATOR, invalidScalars), validBuildInScalars));
66+
}
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package graphql.kickstart.autoconfigure.scalars;
2+
3+
import graphql.schema.GraphQLScalarType;
4+
import java.lang.reflect.Field;
5+
import java.lang.reflect.Modifier;
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
import java.util.stream.Stream;
9+
import lombok.AccessLevel;
10+
import lombok.NoArgsConstructor;
11+
import org.springframework.util.ReflectionUtils;
12+
13+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
14+
public final class GraphQLScalarUtils {
15+
16+
/**
17+
* Extract scalar field definitions from helper classes. Public static
18+
* {@link GraphQLScalarType} fields are considered as scalar definitions.
19+
*
20+
* @param classes classes that may contain scalar definitions.
21+
* @return the map of scalar definitions (keys = scalar names, values are scalar type definitions).
22+
* May return an empty map if no definitions found. If multiple source classes define GraphQL
23+
* scalar types with the same definition, then the last one will be included in the map.
24+
*/
25+
public static Map<String, GraphQLScalarType> extractScalarDefinitions(final Class<?>... classes) {
26+
final Map<String, GraphQLScalarType> scalarTypes = new HashMap<>();
27+
Stream.of(classes).forEach(clazz -> extractScalarField(clazz, scalarTypes));
28+
return scalarTypes;
29+
}
30+
31+
private static void extractScalarField(Class<?> clazz, Map<String, GraphQLScalarType> target) {
32+
ReflectionUtils.doWithFields(clazz, scalarField -> extractedIfScalarField(target, scalarField));
33+
}
34+
35+
private static void extractedIfScalarField(Map<String, GraphQLScalarType> target, Field field)
36+
throws IllegalAccessException {
37+
if (Modifier.isPublic(field.getModifiers())
38+
&& Modifier.isStatic(field.getModifiers())
39+
&& field.getType().equals(GraphQLScalarType.class)) {
40+
final GraphQLScalarType graphQLScalarType = (GraphQLScalarType) field.get(null);
41+
target.put(graphQLScalarType.getName(), graphQLScalarType);
42+
}
43+
}
44+
}

graphql-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
"properties": [
88
{
99
"name": "graphql.extended-scalars",
10-
"type": "java.util.Set",
10+
"type": "java.util.Set<java.lang.String>",
1111
"description": "List of extended scalars to be used."
12+
}, {
13+
"name": "graphql.aliased-scalars",
14+
"type": "java.util.Map<java.lang.String, java.util.List<java.lang.String>>",
15+
"description": "Aliased scalar definitions."
1216
}
1317
]
1418
}

graphql-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
org.springframework.context.ApplicationContextInitializer=\
2-
graphql.kickstart.autoconfigure.scalars.GraphQLExtendedScalarsInitializer
2+
graphql.kickstart.autoconfigure.scalars.GraphQLExtendedScalarsInitializer,\
3+
graphql.kickstart.autoconfigure.scalars.GraphQLAliasedScalarsInitializer
34
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
45
graphql.kickstart.autoconfigure.web.servlet.GraphQLWebAutoConfiguration,\
56
graphql.kickstart.autoconfigure.web.servlet.GraphQLWebSecurityAutoConfiguration,\
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package graphql.kickstart.autoconfigure.web.servlet.test.aliasedscalars;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import graphql.schema.GraphQLScalarType;
6+
import org.junit.jupiter.api.DisplayName;
7+
import org.junit.jupiter.api.Test;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.boot.autoconfigure.SpringBootApplication;
10+
import org.springframework.boot.test.context.SpringBootTest;
11+
import org.springframework.context.ApplicationContext;
12+
import org.springframework.test.context.ActiveProfiles;
13+
14+
@SpringBootTest(
15+
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
16+
classes = AliasedScalarConfigurationTest.AliasedScalarsTestApplication.class)
17+
@ActiveProfiles("aliased-scalars")
18+
@DisplayName("Testing aliased scalars auto configuration")
19+
class AliasedScalarConfigurationTest {
20+
21+
@Autowired private ApplicationContext applicationContext;
22+
23+
@Test
24+
@DisplayName(
25+
"The aliased scalars initializer should be properly picked up by Spring auto configuration.")
26+
void testAutoConfiguration() {
27+
assertThat(applicationContext.getBeansOfType(GraphQLScalarType.class))
28+
.containsOnlyKeys("Decimal", "Number", "Text");
29+
}
30+
31+
@SpringBootApplication
32+
public static class AliasedScalarsTestApplication {}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package graphql.kickstart.autoconfigure.web.servlet.test.aliasedscalars;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
5+
6+
import graphql.kickstart.autoconfigure.scalars.GraphQLExtendedScalarsInitializer;
7+
import graphql.schema.GraphQLScalarType;
8+
import java.util.Collections;
9+
import java.util.Map;
10+
import org.junit.jupiter.api.DisplayName;
11+
import org.junit.jupiter.api.Test;
12+
import org.springframework.boot.SpringApplication;
13+
import org.springframework.boot.WebApplicationType;
14+
import org.springframework.context.ApplicationContextException;
15+
import org.springframework.context.ConfigurableApplicationContext;
16+
import org.springframework.core.env.MapPropertySource;
17+
import org.springframework.core.env.StandardEnvironment;
18+
19+
@DisplayName("Testing aliased scalars configuration")
20+
class AliasedScalarsIncorrectConfigurationTest {
21+
22+
@Test
23+
@DisplayName(
24+
"Should throw exception at context initialization when a non-built in scalar was aliased.")
25+
void shouldThrowErrorOnStartupIfScalarDoesNotExists() {
26+
// GIVEN
27+
final SpringApplication application = setupTestApplication(Collections.singletonMap(
28+
"graphql.aliased-scalars.BugDecimal",
29+
"Number"));
30+
// THEN
31+
assertThatExceptionOfType(ApplicationContextException.class)
32+
.isThrownBy(application::run)
33+
.withMessage(
34+
"Scalar(s) 'BugDecimal' cannot be aliased."
35+
+ " Only the following scalars can be aliased by configuration: BigDecimal,"
36+
+ " BigInteger, Boolean, Byte, Char, Date, DateTime, Float, ID, Int, JSON,"
37+
+ " Locale, Long, NegativeFloat, NegativeInt, NonNegativeFloat, NonNegativeInt,"
38+
+ " NonPositiveFloat, NonPositiveInt, Object, PositiveFloat, PositiveInt, Short,"
39+
+ " String, Time, Url. Note that custom scalar beans cannot be aliased this way.");
40+
}
41+
42+
@Test
43+
@DisplayName("Should not create any aliased scalars by default.")
44+
void shouldNotDeclareAnyAliasedScalarsByDefault() {
45+
// GIVEN
46+
final SpringApplication application = setupTestApplication(Collections.emptyMap());
47+
// WHEN
48+
final ConfigurableApplicationContext context = application.run();
49+
// THEN
50+
assertThat(context.getBeansOfType(GraphQLScalarType.class)).isEmpty();
51+
}
52+
53+
private SpringApplication setupTestApplication(final Map<String, Object> properties) {
54+
final StandardEnvironment standardEnvironment = new StandardEnvironment();
55+
standardEnvironment.getPropertySources().addFirst(new MapPropertySource("testProperties",
56+
properties));
57+
final SpringApplication application =
58+
new SpringApplication(GraphQLExtendedScalarsInitializer.class);
59+
application.setWebApplicationType(WebApplicationType.NONE);
60+
application.setEnvironment(standardEnvironment);
61+
return application;
62+
}
63+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
graphql:
2+
aliased-scalars:
3+
BigDecimal: Number, Decimal
4+
String: Text

0 commit comments

Comments
 (0)