GQLParser.java

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<String, Object> rawVariables) throws IOException {
        try {
            return parseQueryInternal(query, rawVariables);
        } catch (LexerException e) {
            throw new GQLException(e.getMessage(), e);
        }
    }

    public GQLQuery parseQueryInternal(String query, Map<String, Object> 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<String, GQLOperation>                  operationMap        = new LinkedHashMap<>();
        Map<String, GQLFragmentDefinition>         fragmentDefinitions = new LinkedHashMap<>();
        Map<String, Object>                        variables           = new HashMap<>();
        List<Pair<GQLToken, GQLFragmentReference>> 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<GQLSelection> 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<GQLToken, GQLFragmentReference> 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<Pair<GQLToken, GQLFragmentReference>> fragmentReferences,
                               Map<String, GQLFragmentDefinition> fragmentDefinitions,
                               Map<String, Object> 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<GQLSelection> entries = parseFields(
                (PMessageDescriptor) type,
                tokenizer,
                fragmentReferences,
                fragmentDefinitions,
                variables);
        fragmentDefinitions.put(fragmentName, new GQLFragmentDefinition(
                fragmentName, (PMessageDescriptor) type, entries));
    }

    private Map<String, Object> parseVariables(GQLLexer tokenizer, Map<String, Object> rawVariables)
            throws IOException {
        Map<String, Object> 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<GQLSelection> parseOperation(GQLLexer tokenizer,
                                              PService service,
                                              Map<String, Object> variables,
                                              List<Pair<GQLToken, GQLFragmentReference>> fragmentReferences,
                                              Map<String, GQLFragmentDefinition> fragmentDefinitions)
            throws IOException {
        List<GQLSelection> 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<GQLSelection> 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<GQLSelection> 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<GQLSelection> parseFields(PMessageDescriptor descriptor,
                                           GQLLexer tokenizer,
                                           List<Pair<GQLToken, GQLFragmentReference>> fragmentReferences,
                                           Map<String, GQLFragmentDefinition> fragmentDefinitions,
                                           Map<String, Object> variables) throws IOException {
        List<GQLSelection> 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<GQLSelection> 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 || definition.isIgnoredField(field)) {
                    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<GQLSelection> 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 extends PMessage<M>>
    M parseArguments(PStructDescriptor<M> descriptor,
                     GQLLexer tokenizer,
                     Map<String, Object> variables) throws IOException {
        PMessageBuilder<M> 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 || definition.isIgnoredField(field)) {
                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<String, Object> 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 || definition.isIgnoredField(field)) {
                        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<Object>         ld      = (PList) descriptor;
                PList.Builder<Object> builder = ld.builder(4);
                GQLToken next = tokenizer.peek("list end or entry");
                while (!next.isSymbol(']')) {
                    builder.add(parseArgumentValue(tokenizer, ld.itemDescriptor(), null, variables));
                    next = tokenizer.peek("list end or entry");
                    if (next.isSymbol(',')) {
                        tokenizer.next();
                        next = tokenizer.peek("list end or entry");
                    }
                }
                tokenizer.expectSymbol("list end", ']');
                return builder.build();
            }
            case SET: {
                tokenizer.expectSymbol("set start", '[');
                @SuppressWarnings("unchecked")
                PSet<Object>         ld      = (PSet) descriptor;
                PSet.Builder<Object> builder = ld.builder(4);
                GQLToken next = tokenizer.peek("set end or entry");
                while (!next.isSymbol(']')) {
                    builder.add(parseArgumentValue(tokenizer, ld.itemDescriptor(), null, variables));
                    next = tokenizer.peek("set end or entry");
                    if (next.isSymbol(',')) {
                        tokenizer.next();
                        next = tokenizer.peek("set end or entry");
                    }
                }
                tokenizer.expectSymbol("set end", ']');
                return builder.build();
            }
            default: {
                throw new IOException("Unhandled type " + descriptor.getType().toString());
            }
        }
    }
}