GraphQL Java - Scalars
GraphQL中的Scalar
Scalar(原子类型)
在GraphQL类型系统中,类型树的叶子节点成为Scalar。一旦访问到了Scalar类型的数据,就无法在该类型基础上进一步访问其下的类型层次结构。Scalar类型意味着该类型的值无法再细分。
在GraphQL规范中,要求其所有实现都必须具有如下Scalar类型:
- String类型(GraphQLString):UTF-8编码的字符序列
- Boolean类型(GraphQLBoolean):true或false
- Int类型(GraphQLInt):32位的有符号类型
- Float类型(GraphQLFloat):单精度浮点类型
- ID类型(GraphQLID):唯一标识符类型(可以被序列化为String)。但ID类型的数据可读性较差。
GraphQL - Java补充添加了如下几种额外类型:
- Long类型(GraphQLLong):基于java.lang.Long的scalar类型
- Short类型(GraphQLShort):基于java.lang.Short的scalar类型
- Byte类型(GraphQLByte):基于java.lang.Byte的scalar类型
- BigDecimal类型(GraphQLBigDecimal):基于java.math.BigDecimal的scalar类型
- BigInteger类型(GraphQLBigInteger):基于java.math.BigInteger的scalar类型
graphql.Scalars类中包含了所有现有的scalar类型的单例实例。
编写自定义的Scalar类型
可以自定义Scalar类型。在这种实现方式下,需要在运行时自己实现数据到类型的映射机制。
假设我们有一个email的scalar类型,它将使用email地址作为输出和输出。
创建一个EMAIL的scalar类型如下:
public static final GraphQLScalarType EMAIL = new GraphQLScalarType("email", "A custom scalar that handles emails", new Coercing() {
@Override
public Object serialize(Object dataFetcherResult) {
return serializeEmail(dataFetcherResult);
}
@Override
public Object parseValue(Object input) {
return parseEmailFromVariable(input);
}
@Override
public Object parseLiteral(Object input) {
return parseEmailFromAstLiteral(input);
}
});
数据映射
自定义的scalar视线中,核心工作是数据映射的实现部分。主要需要实现如下三个方法:
- parseValue:接收一个input变量,然后在运行时转换为Java中的对象。
- parseLiteral:接收一个AST常量(graphql.language.Value类型)作为输入,在运行时转换为Java中的对象。
- serialize:接收一个Java对象,将它转换为scalar中的output表示形式。
自定义的scalar中,必须要实现两个input的转换(parseValue / parseLiteral)和一个output的转换(serialize)。
例如,对于如下的执行语句:
mutation Contact($mainContact: Email!) {
makeContact(mainContactEmail: $mainContact, backupContactEmail: "backup@company.com") {
id
mainContactEmail
}
}
自定义的Email类型scalar将会有如下调用:
- parseValue方法被调用,将$mailContact变量转换为Java中的运行时数据。
- parseLiterial方法被调用,将AST的graphql.language.StringValue(backup@company.com)转换为Java中的运行时数据。
- serialise方法被调用,将mainContactEmail的运行时表示,转换为graphQL的输出形式。
输入和输出有效性验证
scalar定义中也可以验证输入或输出的数据是否有效。例如:email是否是有效的email数据类型。
graphql.schema.Coercing中有如下规定:
- serialise只允许抛出graphql.schema.CoercingSerializeException类型的异常。它意味着value值不能被序列化为合适的数据形式。同时,也必须保证其他类型的runtime exception不会从serialise方法中抛出。另外,返回值必须是非空值。
- parseValue只允许抛出graphql.schema.CoercingSerializeException类型的异常。它意味着value值不能被解析为合适的input数据形式。同时,也必须保证其他类型的runtime exception不会从parseValue方法中抛出。另外,返回值必须是非空值。
- parseLiteral只允许抛出graphql.schema.CoercingSerializeException类型的异常。它意味着AST常量值不能被解析为合适的input数据形式。同时,也必须保证其他类型的runtime exception不会从parseLiteral方法中抛出。另外,返回值必须是非空值。
有些人视图依赖runtime exception来实现验证,然后期望它们以graphql error形式输出。但事实并不是这样。在自定义Scalar时,必须遵循Coercing方法的规范,才能使GraphQL - Java正确运行。
示例
下面是一个复杂的Email类新的Scalar实现。
public static class EmailScalar {
public static final GraphQLScalarType EMAIL = new GraphQLScalarType("email", "A custom scalar that handles emails", new Coercing() {
@Override
public Object serialize(Object dataFetcherResult) {
return serializeEmail(dataFetcherResult);
}
@Override
public Object parseValue(Object input) {
return parseEmailFromVariable(input);
}
@Override
public Object parseLiteral(Object input) {
return parseEmailFromAstLiteral(input);
}
});
private static boolean looksLikeAnEmailAddress(String possibleEmailValue) {
// ps. I am not trying to replicate RFC-3696 clearly
return Pattern.matches("[A-Za-z0-9]@[.*]", possibleEmailValue);
}
private static Object serializeEmail(Object dataFetcherResult) {
String possibleEmailValue = String.valueOf(dataFetcherResult);
if (looksLikeAnEmailAddress(possibleEmailValue)) {
return possibleEmailValue;
} else {
throw new CoercingSerializeException("Unable to serialize " + possibleEmailValue + " as an email address");
}
}
private static Object parseEmailFromVariable(Object input) {
if (input instanceof String) {
String possibleEmailValue = input.toString();
if (looksLikeAnEmailAddress(possibleEmailValue)) {
return possibleEmailValue;
}
}
throw new CoercingParseValueException("Unable to parse variable value " + input + " as an email address");
}
private static Object parseEmailFromAstLiteral(Object input) {
if (input instanceof StringValue) {
String possibleEmailValue = ((StringValue) input).getValue();
if (looksLikeAnEmailAddress(possibleEmailValue)) {
return possibleEmailValue;
}
}
throw new CoercingParseLiteralException(
"Value is not any email address : '" + String.valueOf(input) + "'"
);
}
}