net.morimekta.providence.graphql.parser.GQLParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of providence-graphql Show documentation
Show all versions of providence-graphql Show documentation
Providence Core extension for GraphQL.
package net.morimekta.providence.graphql.parser;
import net.morimekta.providence.PMessage;
import net.morimekta.providence.PMessageBuilder;
import net.morimekta.providence.PMessageVariant;
import net.morimekta.providence.PType;
import net.morimekta.providence.descriptor.PContainer;
import net.morimekta.providence.descriptor.PDescriptor;
import net.morimekta.providence.descriptor.PEnumDescriptor;
import net.morimekta.providence.descriptor.PField;
import net.morimekta.providence.descriptor.PList;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.descriptor.PPrimitive;
import net.morimekta.providence.descriptor.PService;
import net.morimekta.providence.descriptor.PServiceMethod;
import net.morimekta.providence.descriptor.PSet;
import net.morimekta.providence.descriptor.PStructDescriptor;
import net.morimekta.providence.descriptor.PUnionDescriptor;
import net.morimekta.providence.graphql.GQLDefinition;
import net.morimekta.providence.graphql.directives.IncludeArguments;
import net.morimekta.providence.graphql.directives.SkipArguments;
import net.morimekta.providence.graphql.gql.GQLDirective;
import net.morimekta.providence.graphql.gql.GQLField;
import net.morimekta.providence.graphql.gql.GQLFragmentDefinition;
import net.morimekta.providence.graphql.gql.GQLFragmentReference;
import net.morimekta.providence.graphql.gql.GQLInlineFragment;
import net.morimekta.providence.graphql.gql.GQLIntrospection;
import net.morimekta.providence.graphql.gql.GQLMethodCall;
import net.morimekta.providence.graphql.gql.GQLOperation;
import net.morimekta.providence.graphql.gql.GQLQuery;
import net.morimekta.providence.graphql.gql.GQLScalar;
import net.morimekta.providence.graphql.gql.GQLSelection;
import net.morimekta.providence.graphql.introspection.Type;
import net.morimekta.providence.graphql.introspection.TypeKind;
import net.morimekta.util.Binary;
import net.morimekta.util.Pair;
import net.morimekta.util.lexer.LexerException;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static net.morimekta.providence.graphql.gql.GQLIntrospection.findFieldByName;
import static net.morimekta.providence.graphql.parser.GQLToken.kEntrySep;
import static net.morimekta.providence.graphql.parser.GQLToken.kKeyValueSep;
import static net.morimekta.providence.graphql.parser.GQLToken.kListEnd;
import static net.morimekta.providence.graphql.parser.GQLToken.kListStart;
import static net.morimekta.providence.graphql.parser.GQLToken.kMessageEnd;
import static net.morimekta.providence.graphql.parser.GQLToken.kMessageStart;
import static net.morimekta.providence.graphql.parser.GQLToken.kParamsEnd;
import static net.morimekta.providence.graphql.parser.GQLToken.kParamsStart;
import static net.morimekta.providence.util.MessageUtil.coerce;
import static net.morimekta.util.Strings.isNullOrEmpty;
public class GQLParser {
private final GQLDefinition definition;
public GQLParser(GQLDefinition definition) {
this.definition = definition;
}
@Nonnull
public GQLQuery parseQuery(String query, Map rawVariables) throws IOException {
try {
return parseQueryInternal(query, rawVariables);
} catch (LexerException e) {
throw new GQLException(e.getMessage(), e);
}
}
public GQLQuery parseQueryInternal(String query, Map rawVariables) throws IOException {
if (isNullOrEmpty(query)) {
throw new IOException("Empty query");
}
StringReader in = new StringReader(query);
GQLLexer tokenizer = new GQLLexer(in);
String queryName = null;
boolean isMutation = false;
Map operationMap = new LinkedHashMap<>();
Map fragmentDefinitions = new LinkedHashMap<>();
Map variables = new HashMap<>();
List> fragmentReferences = new ArrayList<>();
GQLToken token = tokenizer.expect("query or mutation");
main: do {
PService service;
if (token.isIdentifier()) {
boolean vars;
switch (token.toString()) {
case "query": {
if (operationMap.containsKey("")) {
throw tokenizer.failure(token, "Default operation already defined");
}
service = definition.getQuery();
token = tokenizer.expect("query name", GQLToken::isIdentifier);
queryName = token.toString();
if (operationMap.containsKey(queryName)) {
throw tokenizer.failure(token, "Operation with name " + queryName + " already defined");
}
vars = tokenizer.expectSymbol("query start", kMessageStart, kParamsStart)
.isSymbol(kParamsStart);
break;
}
case "mutation": {
if (operationMap.containsKey("")) {
throw tokenizer.failure(token, "Default operation already defined");
}
service = definition.getMutation();
if (service == null) {
throw tokenizer.failure(token, "No mutation defined");
}
token = tokenizer.expect("mutation name", GQLToken::isIdentifier);
queryName = token.toString();
if (operationMap.containsKey(queryName)) {
throw tokenizer.failure(token, "Operation with name " + queryName + " already defined");
}
isMutation = true;
vars = tokenizer.expectSymbol("mutation start", kMessageStart, kParamsStart)
.isSymbol(kParamsStart);
break;
}
case "fragment": {
parseFragment(tokenizer, fragmentReferences, fragmentDefinitions, variables);
token = tokenizer.next();
continue main;
}
default: {
throw tokenizer.failure(token, "Expected query, mutation or fragment, got '%s'", token.toString());
}
}
if (vars) {
variables.putAll(parseVariables(tokenizer, rawVariables));
tokenizer.expectSymbol("fields start", kMessageStart);
}
} else if (token.isSymbol(kMessageStart)) {
if (operationMap.size() > 0) {
throw tokenizer.failure(token, "Operation already defined, default operation not allowed with named operations");
}
service = definition.getQuery();
} else {
throw tokenizer.failure(token, "Unexpected symbol '%s'", token.toString());
}
List selections = parseOperation(tokenizer, service, variables, fragmentReferences, fragmentDefinitions);
if (queryName == null) {
operationMap.put("", new GQLOperation(service, isMutation, queryName, selections));
} else {
operationMap.put(queryName, new GQLOperation(service, isMutation, queryName, selections));
}
token = tokenizer.next();
} while (token != null);
for (Pair reference: fragmentReferences) {
GQLFragmentDefinition definition = reference.second.getDefinition();
if (definition == null) {
throw tokenizer.failure(reference.first, "No fragment for reference %s", reference.second.getName());
}
// Validate reference spread / compatibility.
// https://facebook.github.io/graphql/June2018/#sec-Fragment-spread-is-possible
if (isFragmentTypeUnreachable(reference.second.getParentDescriptor(), definition.getTypeCondition())) {
// Type condition mismatch. The definition must be the
throw tokenizer.failure(reference.first, "Fragment %s condition not valid for %s",
reference.second.getName(),
reference.second.getParentDescriptor().getQualifiedName());
}
}
if (operationMap.isEmpty()) {
throw tokenizer.failure(tokenizer.getLastToken(), "No operation in query");
}
return new GQLQuery(operationMap, fragmentDefinitions);
}
/**
* The message (with interface) that is returned by the method or
* is of the field where the fragment is placed or used.
*
* @param containedInType The contained in type.
* @param typeCondition The type of the fragment.
* @return If the fragment can be placed on the contained type.
*/
private boolean isFragmentTypeUnreachable(@Nonnull PMessageDescriptor containedInType,
@Nonnull PMessageDescriptor typeCondition) {
PMessageDescriptor description = containedInType;
if (containedInType.getImplementing() != null) {
description = description.getImplementing();
}
if (typeCondition.equals(description) ||
typeCondition.equals(containedInType) ||
description.equals(typeCondition.getImplementing())) {
// Reachable
return false;
}
if (containedInType.getVariant() == PMessageVariant.UNION &&
description != containedInType) {
for (PField field : containedInType.getFields()) {
if (field.getDescriptor().equals(typeCondition)) {
// Reachable
return false;
}
}
}
// Unreachable
return true;
}
private void parseFragment(GQLLexer tokenizer,
List> fragmentReferences,
Map fragmentDefinitions,
Map variables) throws IOException {
GQLToken token = tokenizer.expect("fragment name", GQLToken::isIdentifier);
String fragmentName = token.toString();
token = tokenizer.expect("fragment of", GQLToken::isIdentifier);
if (!token.toString().equals("on")) {
throw tokenizer.failure(token, "expected on after fragment name");
}
token = tokenizer.expect("fragment type", GQLToken::isIdentifier);
PDescriptor type = definition.getType(token.toString());
if (type == null) {
throw tokenizer.failure(token, "unknown type: %s", token.toString());
} else if (type.getType() != PType.MESSAGE) {
throw tokenizer.failure(token, "not an object type: %s", token.toString());
}
tokenizer.expectSymbol("fragment start", '{');
List entries = parseFields(
(PMessageDescriptor) type,
tokenizer,
fragmentReferences,
fragmentDefinitions,
variables);
fragmentDefinitions.put(fragmentName, new GQLFragmentDefinition(
fragmentName, (PMessageDescriptor) type, entries));
}
private Map parseVariables(GQLLexer tokenizer, Map rawVariables)
throws IOException {
Map variables = new HashMap<>();
GQLToken token = tokenizer.expect("variable name");
do {
String name = token.toString();
if (!name.startsWith("$") || name.length() < 2) {
throw tokenizer.failure(token, "Bad variable name, must start with '$' and have length > 1");
}
name = name.substring(1);
tokenizer.expectSymbol("var value sep", kKeyValueSep);
PDescriptor type = parseType(tokenizer);
token = tokenizer.expect("after variable");
Object defaultValue = null;
if (token.isSymbol('=')) {
defaultValue = parseArgumentValue(tokenizer, type, null, variables);
token = tokenizer.expect("after default value");
}
variables.put(name, coerce(type, rawVariables.get(name))
.orElse(defaultValue));
} while (!token.isSymbol(kParamsEnd));
return variables;
}
private PDescriptor parseType(GQLLexer tokenizer) throws IOException {
GQLToken token = tokenizer.expect("type");
if (token.isSymbol(kListStart)) {
PDescriptor itemType = parseType(tokenizer);
tokenizer.expectSymbol("after list", kListEnd);
return PList.provider(() -> itemType).descriptor();
}
PDescriptor itemType;
GQLScalar scalar = GQLScalar.findByName(token.toString());
if (scalar != null) {
switch (scalar) {
case ID:
case String:
itemType = PPrimitive.STRING;
break;
case Int:
itemType = PPrimitive.I64;
break;
case Float:
itemType = PPrimitive.DOUBLE;
break;
case Boolean:
itemType = PPrimitive.BOOL;
break;
default:
itemType = null;
break;
}
} else {
itemType = definition.getType(token.toString());
}
if (itemType == null) {
throw tokenizer.failure(token, "Unknown type " + token.toString());
}
if (tokenizer.peek("after type").isSymbol('!')) {
tokenizer.next();
}
return itemType;
}
@SuppressWarnings("unchecked")
private List parseOperation(GQLLexer tokenizer,
PService service,
Map variables,
List> fragmentReferences,
Map fragmentDefinitions)
throws IOException {
List rootSelection = new ArrayList<>();
GQLToken token = tokenizer.expect("alias or method", GQLToken::isIdentifier);
do {
if (!token.isIdentifier()) {
throw tokenizer.failure(token, "Unexpected symbol '%s', expected call name or end of query", token.toString());
}
String alias = null;
if (tokenizer.peek("after name").isSymbol(kKeyValueSep)) {
alias = token.toString();
if (alias.startsWith("__")) {
throw tokenizer.failure(token, "Unknown introspection %s", token.toString());
}
tokenizer.next();
token = tokenizer.expect("method", GQLToken::isIdentifier);
}
String methodName = token.toString();
GQLIntrospection.Field intro = findFieldByName(methodName);
if (intro != null) {
PMessage> args = null;
List selection = null;
if (intro.arguments != null && tokenizer.peek("introspection arguments start").isSymbol(kParamsStart)) {
tokenizer.next();
args = parseArguments(intro.arguments, tokenizer, variables);
}
if (intro.response instanceof PMessageDescriptor) {
tokenizer.expectSymbol("introspection fields start", kMessageStart);
selection = parseFields((PMessageDescriptor) intro.response,
tokenizer,
fragmentReferences,
fragmentDefinitions,
variables);
}
rootSelection.add(new GQLIntrospection(intro, alias, args, selection));
token = tokenizer.expect("method or end");
continue;
} else if (methodName.startsWith("__")) {
throw tokenizer.failure(token, "Unknown introspection %s", token.toString());
}
PServiceMethod method = service.getMethod(methodName);
if (method == null) {
throw tokenizer.failure(token, "No method " + methodName + " in " + service.getQualifiedName());
}
PMessage> params;
token = tokenizer.expect("after method");
if (token.isSymbol(kParamsStart)) {
params = parseArguments(method.getRequestType(), tokenizer, variables);
token = tokenizer.expect("after params");
} else {
params = (PMessage) method.getRequestType().builder().build();
}
List selectionSet = null;
if (token.isSymbol(kMessageStart)) {
PUnionDescriptor mrd = method.getResponseType();
if (mrd == null) {
throw tokenizer.failure(token, "Unexpected field list");
}
PField success = mrd.findFieldById(0);
if (success == null) {
throw tokenizer.failure(token,
"Unexpected no success field for method " + methodName + " in " +
service.getQualifiedName());
}
if (success.getType() == PType.LIST ||
success.getType() == PType.SET) {
PContainer container = (PContainer) success.getDescriptor();
if (container.itemDescriptor().getType() == PType.MESSAGE) {
selectionSet = parseFields((PMessageDescriptor) container.itemDescriptor(),
tokenizer,
fragmentReferences,
fragmentDefinitions,
variables);
}
} else if (success.getType() == PType.MESSAGE) {
selectionSet = parseFields((PMessageDescriptor) success.getDescriptor(),
tokenizer,
fragmentReferences,
fragmentDefinitions,
variables);
}
token = tokenizer.expect("method or end");
}
rootSelection.add(new GQLMethodCall(method, alias, params, selectionSet));
if (token.isSymbol(kEntrySep)) {
token = tokenizer.expect("method or end");
}
} while (!token.isSymbol(kMessageEnd));
return rootSelection;
}
private PMessageDescriptor fragmentTypeDescriptor(@Nonnull GQLToken fragmentType) throws GQLException {
String fragmentTypeName = fragmentType.toString();
PDescriptor type = definition.getType(fragmentTypeName);
Type introspectionType = definition.getIntrospectionType(fragmentTypeName);
if (type == null || introspectionType == null) {
throw new GQLException("Unknown type " + fragmentTypeName, fragmentType);
}
if (type.getType() != PType.MESSAGE) {
throw new GQLException("Not an OBJECT type " + fragmentTypeName, fragmentType);
}
if (introspectionType.getKind() != TypeKind.OBJECT &&
introspectionType.getKind() != TypeKind.INTERFACE) {
throw new GQLException("Fragment type must be OBJECT or INPUT, is " +
introspectionType.getKind() + " for " + fragmentTypeName, fragmentType);
}
return (PMessageDescriptor) type;
}
@SuppressWarnings("unchecked")
private List parseFields(PMessageDescriptor descriptor,
GQLLexer tokenizer,
List> fragmentReferences,
Map fragmentDefinitions,
Map variables) throws IOException {
List fields = new ArrayList<>();
PMessageDescriptor baseDescriptor = descriptor;
if (descriptor instanceof PUnionDescriptor &&
descriptor.getImplementing() != null) {
descriptor = descriptor.getImplementing();
// Directives, used for union of lists. This will
// essentially create a list of fields for every
}
GQLToken token = tokenizer.expect("field or end");
do {
if (!token.isIdentifier()) {
if ("...".equals(token.toString())) {
// inline fragment or fragment reference
token = tokenizer.expect("inline fragment", GQLToken::isIdentifier);
if ("on".equals(token.toString())) {
token = tokenizer.expect("type name", GQLToken::isIdentifier);
PMessageDescriptor typeCondition = fragmentTypeDescriptor(token);
if (isFragmentTypeUnreachable(baseDescriptor, typeCondition)) {
throw tokenizer.failure(token, "Type %s not reachable from base of %s",
token.toString(),
baseDescriptor.getName());
}
tokenizer.expectSymbol("fragment fields start", '{');
GQLInlineFragment fragment = new GQLInlineFragment(typeCondition,
parseFields(typeCondition,
tokenizer,
fragmentReferences,
fragmentDefinitions,
variables));
fields.add(fragment);
} else {
GQLFragmentReference reference = new GQLFragmentReference(
token.toString(),
baseDescriptor,
fragmentDefinitions);
fragmentReferences.add(Pair.create(token, reference));
fields.add(reference);
}
token = tokenizer.expect("field or end");
} else {
throw tokenizer.failure(token, "Expected alias or field name, git '%s'", token.toString());
}
} else if (token.toString().startsWith("__")) {
GQLIntrospection.Field intro = findFieldByName(token.toString());
if (intro != null) {
List introFields = null;
if (intro.response instanceof PMessageDescriptor) {
tokenizer.expectSymbol("field list", kMessageStart);
introFields = parseFields((PMessageDescriptor) intro.response,
tokenizer,
fragmentReferences,
fragmentDefinitions,
variables);
}
fields.add(new GQLIntrospection(intro, null, null, introFields));
token = tokenizer.expect("after fields");
} else {
throw tokenizer.failure(token, "Unknown introspection %s", token.toString());
}
} else {
String alias = null;
if (tokenizer.peek("after field").isSymbol(kKeyValueSep)) {
alias = token.toString();
if (alias.startsWith("__")) {
throw tokenizer.failure(token, "Unknown introspection %s", token.toString());
}
tokenizer.next();
token = tokenizer.expect("field name", GQLToken::isIdentifier);
}
PField field = descriptor.findFieldByName(token.toString());
if (field == null) {
throw tokenizer.failure(token,
"Unknown field '%s' in %s",
token.toString(),
descriptor.getQualifiedName());
}
token = tokenizer.expect("after field");
PMessage> arguments = null;
if (token.isSymbol(kParamsStart)) {
if (field.getArgumentsType() == null) {
throw tokenizer.failure(token, "Unexpected arguments for non-argument field %s in %s",
field.getName(), descriptor.getQualifiedName());
} else {
arguments = parseArguments(field.getArgumentsType(), tokenizer, variables);
}
token = tokenizer.expect("after arguments");
}
boolean included = true;
while (token.isDirectve()) {
GQLDirective directive = GQLDirective.findByName(token.toString().substring(1));
if (directive == null) {
throw tokenizer.failure(token, "Unknown directive %s", token.toString());
}
switch (directive) {
case include: {
tokenizer.expectSymbol("include argument", kParamsStart);
IncludeArguments args = parseArguments(IncludeArguments.kDescriptor, tokenizer, variables);
included = args.isIf();
break;
}
case skip: {
tokenizer.expectSymbol("skip argument", kParamsStart);
SkipArguments args = parseArguments(SkipArguments.kDescriptor, tokenizer, variables);
included = !args.isIf();
break;
}
default: {
throw new IllegalStateException("Unhandled directive " + directive.name());
}
}
token = tokenizer.expect("after directive");
}
List subFields = null;
if (token.isSymbol(kMessageStart)) {
PDescriptor fieldDesc = field.getDescriptor();
if (fieldDesc.getType() == PType.LIST ||
fieldDesc.getType() == PType.SET ||
fieldDesc.getType() == PType.MAP) {
fieldDesc = ((PContainer) fieldDesc).itemDescriptor();
}
if (fieldDesc.getType() != PType.MESSAGE) {
throw tokenizer.failure(token, "Unexpected field set for non-message field %s in %s",
field.getName(), descriptor.getQualifiedName());
}
subFields = parseFields((PMessageDescriptor) fieldDesc,
tokenizer,
fragmentReferences,
fragmentDefinitions,
variables);
token = tokenizer.expect("after fields");
}
if (included) {
fields.add(new GQLField(field, alias, arguments, subFields));
}
}
if (token.isSymbol(kEntrySep)) {
token = tokenizer.expect("after sep");
}
} while (!token.isSymbol(kMessageEnd));
return fields;
}
private >
M parseArguments(PStructDescriptor descriptor,
GQLLexer tokenizer,
Map variables) throws IOException {
PMessageBuilder builder = descriptor.builder();
GQLToken token = tokenizer.expect("field id", GQLToken::isIdentifier);
do {
if (!token.isIdentifier()) {
throw tokenizer.failure(token, "expected field name or end, got '%s'",
token.toString());
}
PField field = descriptor.findFieldByName(token.toString());
if (field == null) {
throw tokenizer.failure(token, "unknown field %s in %s",
token.toString(), descriptor.getQualifiedName());
}
tokenizer.expectSymbol("Field value sep", ':');
builder.set(field.getId(), parseArgumentValue(tokenizer, field.getDescriptor(), field.getDefaultValue(), variables));
token = tokenizer.expect("field, sep, or end");
if (token.isSymbol(',')) {
token = tokenizer.expect("field or end");
}
} while (!token.isSymbol(kParamsEnd));
return builder.build();
}
private Object parseArgumentValue(GQLLexer tokenizer,
PDescriptor descriptor,
Object defaultValue,
Map variables) throws IOException {
GQLToken token = tokenizer.peek("variable");
if (token.toString().startsWith("$")) {
String name = token.toString().substring(1);
if (!variables.containsKey(name)) {
throw tokenizer.failure(token, "No such variable $%s", name);
}
tokenizer.next();
return coerce(descriptor, variables.get(name)).orElse(defaultValue);
}
switch (descriptor.getType()) {
case VOID:
case BOOL:
return Boolean.parseBoolean(tokenizer.expect("bool value", GQLToken::isIdentifier).toString());
case BYTE:
return (byte) tokenizer.expect("byte value", GQLTokenType.INTEGER).parseInteger();
case I16:
return (short) tokenizer.expect("i16 value", GQLTokenType.INTEGER).parseInteger();
case I32:
return (int) tokenizer.expect("i32 value", GQLTokenType.INTEGER).parseInteger();
case I64:
return tokenizer.expect("i64 value", GQLTokenType.INTEGER).parseInteger();
case DOUBLE:
return tokenizer.expect("double value",
t -> t.type() == GQLTokenType.INTEGER ||
t.type() == GQLTokenType.FLOAT)
.parseDouble();
case ENUM: {
PEnumDescriptor ed = (PEnumDescriptor) descriptor;
GQLToken id = tokenizer.expect("enum name", GQLToken::isIdentifier);
try {
return ed.valueForName(id.toString());
} catch (IllegalArgumentException e) {
throw tokenizer.failure(id, "No %s enum value '%s'", ed.getName(), id.toString());
}
}
case BINARY: {
GQLToken value = tokenizer.expect("binary literal", GQLTokenType.STRING);
try {
return Binary.fromBase64(value.substring(1, -1).toString());
} catch (IllegalArgumentException e) {
throw tokenizer.failure(value, "Bad base64 binary: %s", e.getMessage());
}
}
case STRING:
return tokenizer.expect("string literal", GQLTokenType.STRING).decodeString(false);
case MESSAGE: {
tokenizer.expectSymbol("message start", '{');
PMessageDescriptor md = (PMessageDescriptor) descriptor;
PMessageBuilder builder = md.builder();
token = tokenizer.expect("field name or end");
while (!token.isSymbol('}')) {
if (!token.isIdentifier()) {
throw tokenizer.failure(token, "expected field name or end, got '%s'", token.toString());
}
PField field = md.findFieldByName(token.toString());
if (field == null) {
throw tokenizer.failure(token, "unknown field %s in %s", token.toString(), md.getQualifiedName());
}
tokenizer.expectSymbol("field value sep", ':');
builder.set(field.getId(), parseArgumentValue(tokenizer, field.getDescriptor(), field.getDefaultValue(), variables));
token = tokenizer.expect("field, sep or end");
if (token.isSymbol(',')) {
token = tokenizer.expect("field name", GQLToken::isIdentifier);
}
}
return builder.build();
}
case LIST: {
tokenizer.expectSymbol("list start", '[');
@SuppressWarnings("unchecked")
PList