Skip to content

Commit

Permalink
Adds a registry based mechanism for registering Custom GraphQL scalars (
Browse files Browse the repository at this point in the history
#1131)

* Added support for OffsetDateTimeScalar scalar and refactored GraphQLConversionUtils implementation to support runtime detection and registration of GraphQl Scalars

* Added ElideCoercing Interface which has method to allow implementations to return Serde for a given Scalar
Added usesSerdeOfType field in ElideScalarType annotation to give user flexibility to specify Type for which Serde is written

* Adding test cases for OffsetDateTime scalar and GraphQLConversionUtils

* Applied style check suggestion

* Applied Codacy suggestion

* Update OffsetDateTimeSerde.java

Fixing checkstyles.

* Elide Annotaion now supports subtypes
Elide Annotaion now registers Serde instead of ElideCoercing
Auto-scan happens on Elide initialization

* Applied PR Review Suggestions

1. Fixed typos
2. Removed unused method from ClassScanner
3. Added suggested refactored Serde scanning code
4. CoerceUtil now returns Immutable Serde map

* Fixing checkstyle (extra space)

Co-authored-by: Aaron Klish <[email protected]>
  • Loading branch information
murtuza-ranapur and aklish committed Jan 16, 2020
1 parent 5c43ee8 commit cd2a159
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 3 deletions.
65 changes: 65 additions & 0 deletions elide-core/src/main/java/com/yahoo/elide/Elide.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.yahoo.elide.core.exceptions.InvalidURLException;
import com.yahoo.elide.core.exceptions.JsonPatchExtensionException;
import com.yahoo.elide.core.exceptions.TransactionException;
import com.yahoo.elide.core.exceptions.UnableToAddSerdeException;
import com.yahoo.elide.extensions.JsonApiPatch;
import com.yahoo.elide.extensions.PatchRequestScope;
import com.yahoo.elide.jsonapi.JsonApiMapper;
Expand All @@ -31,10 +32,18 @@
import com.yahoo.elide.parsers.PatchVisitor;
import com.yahoo.elide.parsers.PostVisitor;
import com.yahoo.elide.security.User;
import com.yahoo.elide.utils.ClassScanner;
import com.yahoo.elide.utils.coerce.CoerceUtil;
import com.yahoo.elide.utils.coerce.converters.ElideTypeConverter;
import com.yahoo.elide.utils.coerce.converters.Serde;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;

import org.antlr.v4.runtime.misc.ParseCancellationException;
import org.apache.commons.lang3.StringUtils;
Expand All @@ -45,6 +54,8 @@
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Set;
import java.util.function.Supplier;

import javax.validation.ConstraintViolationException;
Expand Down Expand Up @@ -77,6 +88,60 @@ public Elide(ElideSettings elideSettings) {
elideSettings.getSerdes().forEach((targetType, serde) -> {
CoerceUtil.register(targetType, serde);
});

registerCustomSerde();
}

private void registerCustomSerde() {
Set<Class<?>> classes = ClassScanner.getAnnotatedClasses(ElideTypeConverter.class);

for (Class<?> clazz : classes) {
if (!Serde.class.isAssignableFrom(clazz)) {
log.warn("Skipping Serde registration (not a Serde!): {}", clazz);
continue;
}
Serde serde;
try {
serde = (Serde) clazz
.getDeclaredConstructor()
.newInstance();
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException | InvocationTargetException e) {
String errorMsg = String.format("Error while registering custom Serde: %s", e.getLocalizedMessage());
log.error(errorMsg);
throw new UnableToAddSerdeException(errorMsg);
}
ElideTypeConverter converter = clazz.getAnnotation(ElideTypeConverter.class);
Class baseType = converter.type();
registerCustomSerde(baseType, serde, converter.name());

for (Class type : converter.subTypes()) {
if (!baseType.isAssignableFrom(type)) {
throw new IllegalArgumentException("Mentioned type " + type
+ " not subtype of " + baseType);
}
registerCustomSerde(type, serde, converter.name());
}
}
}

private void registerCustomSerde(Class<?> type, Serde serde, String name) {
log.info("Registering serde for type : {}", type);
CoerceUtil.register(type, serde);
registerCustomSerdeInObjectMapper(type, serde, name);
}

private void registerCustomSerdeInObjectMapper(Class<?> type, Serde serde, String name) {
ObjectMapper objectMapper = mapper.getObjectMapper();
objectMapper.registerModule(new SimpleModule(name)
.addSerializer(type, new JsonSerializer<Object>() {
@Override
public void serialize(Object obj, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider)
throws IOException, JsonProcessingException {
jsonGenerator.writeObject(serde.serialize(obj));
}
}));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright 2020, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.core.exceptions;

public class UnableToAddSerdeException extends RuntimeException {
public UnableToAddSerdeException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ public static <S, T> Serde<S, T> lookup(Class<T> targetType) {
return (Serde<S, T>) SERDES.getOrDefault(targetType, null);
}

public static Map<Class<?>, Serde<?, ?>> getSerdes() {
return Collections.unmodifiableMap(SERDES);
}

/**
* Perform CoerceUtil setup.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2020, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.utils.coerce.converters;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ElideTypeConverter {
Class<?> type();
String name();
String description() default "Custom Elide type";
Class<?> [] subTypes() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2020, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.utils.coerce.converters;

import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;

@ElideTypeConverter(type = OffsetDateTime.class, name = "OffsetDateTime")
public class OffsetDateTimeSerde implements Serde<String, OffsetDateTime> {

@Override
public OffsetDateTime deserialize(String val) {
return OffsetDateTime.parse(val, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}

@Override
public String serialize(OffsetDateTime val) {
return val.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2018, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide;

import static org.junit.jupiter.api.Assertions.assertNotNull;

import com.yahoo.elide.core.datastore.inmemory.HashMapDataStore;
import com.yahoo.elide.core.datastore.inmemory.InMemoryDataStore;
import com.yahoo.elide.utils.coerce.CoerceUtil;
import com.yahoo.elide.utils.coerce.converters.ElideTypeConverter;
import com.yahoo.elide.utils.coerce.converters.Serde;

import org.junit.jupiter.api.Test;

class Dummy {
}

class DummyTwo extends Dummy {
}

class DummyThree extends Dummy {
}

@ElideTypeConverter(type = Dummy.class, name = "Dummy", subTypes = {DummyThree.class, DummyTwo.class})
class DummySerde implements Serde<String, Dummy> {

@Override
public Dummy deserialize(String val) {
return null;
}

@Override
public String serialize(Dummy val) {
return null;
}
}

public class ElideCustomSerdeRegistrationTest {
@Test
public void testRegisterCustomSerde() {
HashMapDataStore wrapped = new HashMapDataStore(Dummy.class.getPackage());
InMemoryDataStore store = new InMemoryDataStore(wrapped);
ElideSettings elideSettings = new ElideSettingsBuilder(store).build();
new Elide(elideSettings);
assertNotNull(CoerceUtil.lookup(Dummy.class));
assertNotNull(CoerceUtil.lookup(DummyTwo.class));
assertNotNull(CoerceUtil.lookup(DummyThree.class));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2018, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.utils.coerce.converters;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;

public class OffsetDateTimeTest {

@Test
public void testGraphQLOffsetDateTimeSerialize() {
OffsetDateTime offsetDateTime =
OffsetDateTime.of(1995, 11, 2,
16, 45, 4, 56,
ZoneOffset.ofHoursMinutes(5, 30));
String expected = "1995-11-02T16:45:04.000000056+05:30";
OffsetDateTimeSerde offsetDateTimeScalar = new OffsetDateTimeSerde();
Object actualDate = offsetDateTimeScalar.serialize(offsetDateTime);
assertEquals(expected, actualDate);
}

@Test
public void testGraphQLOffsetDateTimeDeserialize() {
OffsetDateTime actualDate =
OffsetDateTime.of(1995, 11, 2,
16, 45, 4, 56,
ZoneOffset.ofHoursMinutes(5, 30));
String actual = "1995-11-02T16:45:04.000000056+05:30";
OffsetDateTimeSerde offsetDateTimeScalar = new OffsetDateTimeSerde();
Object expected = offsetDateTimeScalar.deserialize(actual);
assertEquals(expected, actualDate);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
import static graphql.schema.GraphQLObjectType.newObject;

import com.yahoo.elide.core.EntityDictionary;
import com.yahoo.elide.utils.coerce.CoerceUtil;
import com.yahoo.elide.utils.coerce.converters.ElideTypeConverter;
import com.yahoo.elide.utils.coerce.converters.Serde;

import graphql.Scalars;
import graphql.schema.DataFetcher;
Expand Down Expand Up @@ -41,6 +44,9 @@ public class GraphQLConversionUtils {
protected static final String MAP = "Map";
protected static final String KEY = "key";
protected static final String VALUE = "value";
protected static final String ERROR_MESSAGE = "Value should either be integer, String or float";

private final Map<Class<?>, GraphQLScalarType> scalarMap = new HashMap<>();

protected NonEntityDictionary nonEntityDictionary = new NonEntityDictionary();
protected EntityDictionary entityDictionary;
Expand All @@ -52,6 +58,20 @@ public class GraphQLConversionUtils {

public GraphQLConversionUtils(EntityDictionary dictionary) {
this.entityDictionary = dictionary;
registerCustomScalars();
}

private void registerCustomScalars() {
for (Class serdeType : CoerceUtil.getSerdes().keySet()) {
Serde serde = CoerceUtil.lookup(serdeType);
ElideTypeConverter elideTypeConverter = serde.getClass()
.getAnnotation(ElideTypeConverter.class);
if (elideTypeConverter != null) {
SerdeCoercing serdeCoercing = new SerdeCoercing(ERROR_MESSAGE, serde);
scalarMap.put(elideTypeConverter.type(), new GraphQLScalarType(elideTypeConverter.name(),
elideTypeConverter.description(), serdeCoercing));
}
}
}

/**
Expand All @@ -74,12 +94,13 @@ public GraphQLScalarType classToScalarType(Class<?> clazz) {
return Scalars.GraphQLShort;
} else if (clazz.equals(String.class)) {
return Scalars.GraphQLString;
} else if (Date.class.isAssignableFrom(clazz)) {
return GraphQLScalars.GRAPHQL_DATE_TYPE;
} else if (clazz.equals(BigDecimal.class)) {
return Scalars.GraphQLBigDecimal;
} else if (Date.class.isAssignableFrom(clazz)) {
return GraphQLScalars.GRAPHQL_DATE_TYPE;
} else if (scalarMap.containsKey(clazz)) {
return scalarMap.get(clazz);
}

return null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2020, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.graphql;

import com.yahoo.elide.utils.coerce.converters.Serde;

import graphql.language.FloatValue;
import graphql.language.IntValue;
import graphql.language.StringValue;
import graphql.schema.Coercing;
import graphql.schema.CoercingParseValueException;
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class SerdeCoercing<I, O> implements Coercing<I, O> {
private String errorMessage;
private Serde<O, I> serde;

@Override
public O serialize(Object dataFetcherResult) {
return serde.serialize((I) dataFetcherResult);
}

@Override
public I parseValue(Object input) {
return serde.deserialize((O) input);
}

public I parseLiteral(Object o) {
Object input;
if (o instanceof IntValue) {
input = ((IntValue) o).getValue().longValue();
} else if (o instanceof StringValue) {
input = ((StringValue) o).getValue();
} else if (o instanceof FloatValue) {
input = ((FloatValue) o).getValue().floatValue();
} else {
throw new CoercingParseValueException(errorMessage);
}
return parseValue(input);
}
}
Loading

0 comments on commit cd2a159

Please sign in to comment.