All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.morimekta.providence.graphql.GQLDefinition Maven / Gradle / Ivy

There is a newer version: 2.7.0
Show newest version
package net.morimekta.providence.graphql;

import net.morimekta.providence.PEnumValue;
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.PInterfaceDescriptor;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.descriptor.PRequirement;
import net.morimekta.providence.descriptor.PService;
import net.morimekta.providence.descriptor.PServiceMethod;
import net.morimekta.providence.descriptor.PStructDescriptor;
import net.morimekta.providence.descriptor.PUnionDescriptor;
import net.morimekta.providence.graphql.gql.GQLScalar;
import net.morimekta.providence.graphql.gql.GQLUtil;
import net.morimekta.providence.graphql.introspection.EnumValue;
import net.morimekta.providence.graphql.introspection.Field;
import net.morimekta.providence.graphql.introspection.InputValue;
import net.morimekta.providence.graphql.introspection.Schema;
import net.morimekta.providence.graphql.introspection.Type;
import net.morimekta.providence.graphql.introspection.TypeKind;
import net.morimekta.util.collect.UnmodifiableList;
import net.morimekta.util.collect.UnmodifiableSet;
import net.morimekta.util.collect.UnmodifiableSortedMap;
import net.morimekta.util.io.IndentedPrintWriter;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import static java.util.Objects.requireNonNull;
import static net.morimekta.providence.graphql.gql.GQLUtil.toArgumentString;
import static net.morimekta.util.collect.UnmodifiableSet.setOf;

/**
 * A GQL service is a collection of zero or more 'queries' and zero
 * or more 'mutations'. The query and mutation distinction is meant
 * to represent reading and writing operations, but in
 * reality they distinguish parallel and serial execution,
 * in case the query contains more than one base entry.
 */
@Immutable
public class GQLDefinition {
    private final PService                 query;
    private final PService                 mutation;
    private final Map allTypes;
    private final Map inputTypes;
    private final Map outputTypes;
    private final Map        introspectionMap;
    private final Set              idFields;
    private final Set    asInterface;
    private final AtomicReference  schema;
    private final Schema                   introspectionSchema;

    public static class Builder {
        private PService                     query;
        private PService                     mutation;
        private Collection           idFields;
        private Collection asInterface;

        private Builder() {
            idFields = new ArrayList<>();
            asInterface = new ArrayList<>();
        }

        public Builder query(PService query) {
            this.query = query;
            return this;
        }

        public Builder mutation(PService mutation) {
            this.mutation = mutation;
            return this;
        }

        public Builder idField(PField... fields) {
            Collections.addAll(idFields, fields);
            return this;
        }

        public Builder asInterface(PUnionDescriptor... descriptors) {
            Collections.addAll(asInterface, descriptors);
            return this;
        }

        public GQLDefinition build() {
            if (query == null) {
                throw new IllegalStateException("No query defined.");
            }
            return new GQLDefinition(query, mutation, idFields, asInterface);
        }
    }

    public static Builder builder() {
        return new Builder();
    }

    /**
     * Create a graphql definition instance.
     *
     * @param query The query service. Mandatory.
     * @param mutation The mutation service.
     * @param idFields Collection if ID fields.
     */
    public GQLDefinition(@Nonnull PService query,
                         @Nullable PService mutation,
                         @Nonnull Collection idFields) {
        this(query, mutation, idFields, setOf());
    }

    /**
     * Create a graphql definition instance.
     *
     * @param query The query service. Mandatory.
     * @param mutation The mutation service.
     * @param idFields Collection if ID fields.
     * @param asInterface Collection of unions to be
     */
    public GQLDefinition(@Nonnull PService query,
                         @Nullable PService mutation,
                         @Nonnull Collection idFields,
                         @Nonnull Collection asInterface) {
        this.idFields = UnmodifiableSet.copyOf(idFields);
        this.asInterface = UnmodifiableSet.copyOf(asInterface);
        for (PUnionDescriptor ui : asInterface) {
            if (ui.getImplementing() == null) {
                throw new IllegalArgumentException("Union " + ui.getName() + " does not have implementing interface, " +
                                                   "so is not allowed as 'union as interface' in graphql.");
            }
        }

        // This should try to preserve order.
        LinkedHashMap typeMap      = new LinkedHashMap<>();
        LinkedHashMap inputTypeMap = new LinkedHashMap<>();
        for (PServiceMethod method : query.getMethods()) {
            registerInputTypes(method.getRequestType(), typeMap, inputTypeMap, false);
            registerTypes(method.getResponseType(), typeMap, inputTypeMap, asInterface, false);
        }
        if (mutation != null) {
            for (PServiceMethod method : mutation.getMethods()) {
                registerInputTypes(method.getRequestType(), typeMap, inputTypeMap, false);
                registerTypes(method.getResponseType(), typeMap, inputTypeMap, asInterface, false);
            }
        }
        this.inputTypes = UnmodifiableSortedMap.copyOf(inputTypeMap);
        this.outputTypes = UnmodifiableSortedMap.copyOf(typeMap);

        registerTypes(Schema.kDescriptor, typeMap, inputTypeMap, asInterface, true);

        Map allTypes      = new TreeMap<>(typeMap);
        Map        introspection = new TreeMap<>();
        ArrayList   types         = new ArrayList<>(typeMap.values());

        Collections.reverse(types);
        types.forEach(descriptor -> buildTypeDefinition(introspection, descriptor, false, false));

        types = new ArrayList<>(inputTypeMap.values());
        Collections.reverse(types);
        types.forEach(descriptor -> {
            Type tmp = buildTypeDefinition(introspection, descriptor, true, false);
            allTypes.put(tmp.getName(), descriptor);
        });
        Type queryType = buildServiceDefinition(introspection, query);
        Type mutationType = buildServiceDefinition(introspection, mutation);

        this.allTypes = UnmodifiableSortedMap.copyOf(allTypes);
        this.introspectionMap = UnmodifiableSortedMap.copyOf(introspection);
        this.introspectionSchema = Schema
                .builder()
                .setTypes(introspection
                                  .values()
                                  .stream()
                                  .filter(type -> !type.getName().startsWith("__"))
                                  .collect(Collectors.toList()))
                .setQueryType(queryType)
                .setMutationType(mutationType)
                .build();
        this.query = query;
        this.mutation = mutation;
        this.schema = new AtomicReference<>();
    }

    /**
     * Get query, e.g. for gql queries like this:
     *
     * 
     * {
     *     hero(id:1001) {
     *         name
     *     }
     * }
     * 
* * @return The query service. */ @Nonnull public PService getQuery() { return query; } /** * Get mutation by name, e.g. for gql queries like this: * *
     * mutation HeroStore {
     *     deleteHero(id:1001) {
     *         name
     *     }
     * }
     * 
* * @return The mutation service. */ @Nullable public PService getMutation() { return mutation; } /** * Get a type used in the GQL service. * * @param name The name of the type. * @return The type description, enum or message. */ public PDescriptor getType(@Nonnull String name) { return allTypes.get(name); } /** * Get the introspection type for a defined type. * @param name The type name. * @return Introspection type, or null if not defined in service. */ @Nullable public Type getIntrospectionType(@Nonnull String name) { return introspectionMap.get(name); } /** * Get introspection type for a given descriptor. * * @param descriptor The descriptor to get introspection type for. * @param isInput If the type should be an input type. * @return The introspection type. */ @Nonnull public Type getIntrospectionType(@Nonnull PDescriptor descriptor, boolean isInput) { String name = descriptor.getName(); if (descriptor instanceof PMessageDescriptor && isInput) { name = name + INPUT_TYPE; } return Optional.ofNullable(getIntrospectionType(name)) .orElseGet(() -> buildTypeDefinition( new HashMap<>(introspectionMap), descriptor, isInput, false)); } /** * Get a defined schema from the GQL service. * @return The GQL schema. */ public String getSchema() { return schema.updateAndGet(schema -> { if (schema == null) { return buildSchema(); } return schema; }); } /** * Return the introspection schema for this definition. * * @return The schema. */ @Nonnull public Schema getIntrospectionSchema() { return introspectionSchema; } private boolean isIdField(PField field) { return idFields.contains(field); } private Type buildServiceDefinition(Map introspection, PService service) { if (service == null) return null; Type._Builder builder = Type.builder(); builder.setName(service.getName()); builder.setKind(TypeKind.OBJECT); builder.setInterfaces(UnmodifiableList.listOf()); for (PServiceMethod method : service.getMethods()) { if (method.getName().startsWith("__")) { continue; } Field._Builder field = Field.builder(); field.setName(method.getName()); PUnionDescriptor response = method.getResponseType(); PStructDescriptor request = method.getRequestType(); if (response != null) { PDescriptor desc = response.fieldForId(0).getDescriptor(); field.setType(Type.builder() .setKind(TypeKind.NON_NULL) .setOfType(makeTypeReference(buildTypeDefinition(introspection, desc, false, false)))); } else { field.setType(GQLScalar.Boolean.introspection); } field.setArgs(buildInputValues(introspection, request)); builder.addToFields(field.build()); } Type type = builder.build(); introspection.put(type.getName(), type); return type; } private String defaultValueString(Object value) { if (value == null) return null; return GQLUtil.toArgumentString(value); } private List buildInputValues(Map introspection, PMessageDescriptor arguments) { List out = new ArrayList<>(); for (PField field : arguments.getFields()) { if (field.getName().startsWith("__")) { continue; } out.add(InputValue.builder() .setName(field.getName()) .setType(makeTypeReference(buildTypeDefinition( introspection, field.getDescriptor(), true, isIdField(field)))) .setDefaultValue(defaultValueString(field.getDefaultValue())) .build()); } return UnmodifiableList.copyOf(out); } private Field buildFieldSpec(Map introspection, PField field) { Field._Builder builder = Field.builder(); builder.setName(field.getName()); if (field.getArgumentsType() != null) { builder.setArgs(buildInputValues(introspection, field.getArgumentsType())); } Type type = makeTypeReference(buildTypeDefinition( introspection, field.getDescriptor(), false, isIdField(field))); if (field.getRequirement() == PRequirement.REQUIRED) { type = Type.builder() .setKind(TypeKind.NON_NULL) .setOfType(type) .build(); } return builder.setType(type) .build(); } private Type makeTypeReference(Type type) { switch (type.getKind()) { case ENUM: case UNION: case INTERFACE: case OBJECT: case INPUT_OBJECT: { return type.mutate() .clearInterfaces() .clearInputFields() .clearPossibleTypes() .clearFields() .clearEnumValues() .clearDescription() .build(); } case LIST: case NON_NULL: { return type.mutate() .setOfType(makeTypeReference(type.getOfType())) .build(); } } return type; } private boolean isUnionAsInterface(PDescriptor descriptor) { return descriptor instanceof PUnionDescriptor && asInterface.contains(descriptor) && ((PUnionDescriptor) descriptor).getImplementing() != null; } private Type buildTypeDefinition(Map introspection, PDescriptor descriptor, boolean isInput, boolean isIdField) { switch (descriptor.getType()) { case MESSAGE: { PMessageDescriptor md = (PMessageDescriptor) descriptor; String name = descriptor.getName(); if (isInput) { name += INPUT_TYPE; } else if (isUnionAsInterface(md)) { return buildTypeDefinition(introspection, requireNonNull(md.getImplementing()), false, isIdField); } if (introspection.containsKey(name)) { return introspection.get(name); } Type._Builder builder = Type.builder(); builder.setName(name); boolean isUnion = false; if (md.getVariant() == PMessageVariant.INTERFACE) { builder.setKind(TypeKind.INTERFACE); builder.setPossibleTypes(UnmodifiableList.listOf()); builder.setFields(UnmodifiableList.listOf()); } else if (isInput) { builder.setKind(TypeKind.INPUT_OBJECT); } else if (md.getVariant() == PMessageVariant.UNION && md.getImplementing() != null) { builder.setKind(TypeKind.UNION); builder.setPossibleTypes(UnmodifiableList.listOf()); isUnion = true; } else { builder.setKind(TypeKind.OBJECT); builder.setFields(UnmodifiableList.listOf()); builder.setInterfaces(UnmodifiableList.listOf()); } if (md.getImplementing() != null) { builder.addToInterfaces(buildTypeDefinition(introspection, md.getImplementing(), false, false)); if (introspection.containsKey(name)) { return introspection.get(name); } } introspection.put(name, builder.build()); if (isUnion) { for (PField field : md.getFields()) { builder.addToPossibleTypes(buildTypeDefinition(introspection, field.getDescriptor(), false, false)); } } else if (isInput) { builder.setInputFields(buildInputValues(introspection, md)); } else { for (PField field : md.getFields()) { if (field.getName().startsWith("__")) { continue; } builder.addToFields(buildFieldSpec(introspection, field)); } } introspection.put(name, builder.build()); if (md instanceof PInterfaceDescriptor) { PInterfaceDescriptor id = (PInterfaceDescriptor) descriptor; for (PMessageDescriptor pt : id.getPossibleTypes()) { if (pt.getVariant() != PMessageVariant.UNION) { builder.addToPossibleTypes(buildTypeDefinition(introspection, pt, false, false)); } } } introspection.put(name, builder.build()); return builder.build(); } case ENUM: { Type._Builder builder = Type.builder(); if (introspection.containsKey(descriptor.getName())) { return introspection.get(descriptor.getName()); } builder.setName(descriptor.getName()); builder.setKind(TypeKind.ENUM); builder.mutableEnumValues(); for (PEnumValue value : ((PEnumDescriptor) descriptor).getValues()) { builder.addToEnumValues(EnumValue.builder() .setName(value.asString()) .build()); } introspection.put(descriptor.getName(), builder.build()); return builder.build(); } case SET: case LIST: { Type._Builder builder = Type.builder(); PContainer pc = (PContainer) descriptor; builder.setKind(TypeKind.LIST); builder.setOfType(buildTypeDefinition(introspection, pc.itemDescriptor(), isInput, isIdField)); return builder.build(); } case STRING: case BINARY: if (isIdField) { return GQLScalar.ID.introspection; } return GQLScalar.String.introspection; case VOID: case BOOL: return GQLScalar.Boolean.introspection; case I64: case I32: case I16: case BYTE: return GQLScalar.Int.introspection; case DOUBLE: return GQLScalar.Float.introspection; } throw new IllegalStateException("Unsupported type: " + descriptor.getType()); } private String buildSchema() { StringWriter out = new StringWriter(); IndentedPrintWriter writer = new IndentedPrintWriter(out, " ", "\n"); writer.append("# Generated for providence graphql") .newline(); List types = new ArrayList<>(outputTypes.values()); for (PDescriptor descriptor : types) { if (descriptor.getName().startsWith("__")) { continue; } if (descriptor.getType() == PType.ENUM) { PEnumDescriptor ed = (PEnumDescriptor) descriptor; writer.formatln("enum %s {", descriptor.getName()) .begin(); for (PEnumValue val : ed.getValues()) { writer.appendln(val.asString()); } writer.end() .appendln("}") .newline(); } else if (descriptor.getType() == PType.MESSAGE) { PMessageDescriptor md = (PMessageDescriptor) descriptor; if (md.getVariant() == PMessageVariant.UNION && md.getImplementing() != null) { // union Name = Type1 | Type2 writer.formatln("union %s = ", md.getName()); boolean first = true; for (PField field : md.getFields()) { if (field.getName().startsWith("__") || field.getDescriptor().getName().startsWith("__")) { continue; } if (first) { first = false; } else { writer.append(" | "); } writer.append(field.getDescriptor().getName()); } writer.newline(); } else if (md.getVariant() == PMessageVariant.INTERFACE) { // interface Name { ... } writer.formatln("interface %s {", md.getName()) .begin(); for (PField field : md.getFields()) { if (field.getName().startsWith("__") || field.getDescriptor().getName().startsWith("__")) { continue; } writer.formatln("%s: %s", field.getName(), typeName(field)); } writer.end() .appendln("}") .newline(); } else { // type writer.formatln("type %s%s {", md.getName(), md.getImplementing() != null ? " implements " + typeName(md.getImplementing(), false) : "") .begin(); for (PField field : md.getFields()) { if (field.getName().startsWith("__")) { continue; } writer.formatln("%s", field.getName()); if (field.getArgumentsType() != null) { writer.append("("); boolean first = true; for (PField arg : field.getArgumentsType().getFields()) { if (field.getName().startsWith("__")) { continue; } if (first) { first = false; } else { writer.append(", "); } writer.format("%s: %s", arg.getName(), inputTypeName(arg.getDescriptor(), isIdField(arg))); if (arg.hasDefaultValue()) { writer.format(" = %s", toArgumentString(arg.getDefaultValue())); } } writer.append(")"); } writer.format(": %s", typeName(field)); } writer.end() .appendln("}") .newline(); } } } types = new ArrayList<>(inputTypes.values()); for (PDescriptor descriptor : types) { PMessageDescriptor md = (PMessageDescriptor) descriptor; // input writer.formatln("input %s%s%s {", md.getName(), INPUT_TYPE, md.getImplementing() != null ? " implements " + typeName(md.getImplementing(), false) : "") .begin(); for (PField field : md.getFields()) { if (field.getName().startsWith("__")) { continue; } writer.formatln("%s: %s", field.getName(), inputTypeName(field)); } writer.end() .appendln("}") .newline(); } appendService(writer, query); if (mutation != null) { appendService(writer, mutation); } writer.appendln("schema {") .begin() .formatln("query: %s", query.getName()); if (mutation != null) { writer.formatln("mutation: %s", mutation.getName()); } writer.end() .appendln("}") .newline(); writer.flush(); return out.toString(); } private void appendService(IndentedPrintWriter writer, PService service) { writer.formatln("type %s {", service.getName()) .begin(); boolean firstMethod = true; for (PServiceMethod method : service.getMethods()) { if (firstMethod) { firstMethod = false; } else { writer.newline(); } writer.appendln(method.getName()); PMessageDescriptor args = method.getRequestType(); writer.append("("); boolean first = true; for (PField arg : args.getFields()) { if (arg.getName().startsWith("__")) { continue; } if (first) { first = false; } else { writer.append(", "); } writer.format("%s: %s", arg.getName(), inputTypeName(arg.getDescriptor(), isIdField(arg))); if (arg.hasDefaultValue()) { writer.format(" = %s", toArgumentString(arg.getDefaultValue())); } } writer.append("): "); PUnionDescriptor response = method.getResponseType(); if (response != null) { PField success = response.fieldForId(0); if (success.getType() == PType.VOID) { writer.format("Boolean"); } else { writer.format("%s!", typeName(success)); } } } writer.end() .appendln("}") .newline(); } private String typeName(PField field) { return typeName(field.getDescriptor(), isIdField(field)) + (field.getRequirement() == PRequirement.REQUIRED ? "!" : ""); } private String typeName(PDescriptor descriptor, boolean idType) { switch (descriptor.getType()) { case MESSAGE: if (isUnionAsInterface(descriptor)) { return typeName(requireNonNull(((PMessageDescriptor) descriptor).getImplementing()), idType); } return descriptor.getName(); case ENUM: return descriptor.getName(); case LIST: case SET: PContainer cd = (PContainer) descriptor; return "[" + typeName(cd.itemDescriptor(), idType) + "!]"; case VOID: case BOOL: return GQLScalar.Boolean.name(); case BYTE: case I16: case I32: case I64: return GQLScalar.Int.name(); case DOUBLE: return GQLScalar.Float.name(); case STRING: case BINARY: if (idType) { return GQLScalar.ID.name(); } return GQLScalar.String.name(); } throw new UnsupportedOperationException("Not supported type " + descriptor.getQualifiedName()); } private String inputTypeName(PField field) { return inputTypeName(field.getDescriptor(), isIdField(field)) + (field.getRequirement() == PRequirement.REQUIRED ? "!" : ""); } private String inputTypeName(PDescriptor descriptor, boolean idType) { if (descriptor.getType() == PType.MESSAGE) { return descriptor.getName() + INPUT_TYPE; } else if (descriptor.getType() == PType.SET || descriptor.getType() == PType.LIST) { PContainer cd = (PContainer) descriptor; return "[" + inputTypeName(cd.itemDescriptor(), idType) + "!]"; } return typeName(descriptor, idType); } private static final String INPUT_TYPE = "InputType"; private static void registerInputTypes(PMessageDescriptor descriptor, Map types, Map inputTypes, boolean registerSelf) { if (descriptor != null) { if (descriptor.getVariant() == PMessageVariant.EXCEPTION || inputTypes.containsKey(descriptor.getName())) { return; } if (registerSelf) { inputTypes.put(descriptor.getName(), descriptor); } for (PField field : descriptor.getFields()) { if (field.getName().startsWith("__")) { continue; } if (field.getType() == PType.ENUM) { types.put(field.getDescriptor().getName(), field.getDescriptor()); } else if (field.getType() == PType.MESSAGE) { registerInputTypes((PMessageDescriptor) field.getDescriptor(), types, inputTypes, true); } else if ((field.getType() == PType.SET || field.getType() == PType.LIST || field.getType() == PType.MAP)) { PDescriptor itemType = ((PContainer) field.getDescriptor()).itemDescriptor(); if (itemType.getType() == PType.MESSAGE) { registerInputTypes((PMessageDescriptor) itemType, types, inputTypes, true); } else if (itemType.getType() == PType.ENUM) { types.put(itemType.getName(), itemType); } } } } } private static void registerTypes(PMessageDescriptor descriptor, Map types, Map inputTypes, Collection asInterface, boolean registerSelf) { if (descriptor != null) { if (descriptor.getVariant() == PMessageVariant.EXCEPTION || types.containsKey(descriptor.getName())) { return; } if (descriptor instanceof PUnionDescriptor && asInterface.contains(descriptor) && descriptor.getImplementing() != null) { registerSelf = false; } if (registerSelf) { types.put(descriptor.getName(), descriptor); } registerTypes(descriptor.getImplementing(), types, inputTypes, asInterface, true); for (PField field : descriptor.getFields()) { if (field.getName().startsWith("__")) { continue; } registerInputTypes(field.getArgumentsType(), types, inputTypes, false); if (field.getType() == PType.ENUM) { types.put(field.getDescriptor().getName(), field.getDescriptor()); } else if (field.getType() == PType.MESSAGE) { registerTypes((PMessageDescriptor) field.getDescriptor(), types, inputTypes, asInterface, true); } else if ((field.getType() == PType.SET || field.getType() == PType.LIST || field.getType() == PType.MAP)) { PDescriptor itemType = ((PContainer) field.getDescriptor()).itemDescriptor(); if (itemType.getType() == PType.MESSAGE) { registerTypes((PMessageDescriptor) itemType, types, inputTypes, asInterface, true); } else if (itemType.getType() == PType.ENUM) { types.put(itemType.getName(), itemType); } } } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy