net.morimekta.providence.graphql.GQLDefinition 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;
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);
}
}
}
}
}
}