graphql.schema.idl.SchemaPrinter Maven / Gradle / Ivy
package graphql.schema.idl;
import graphql.Assert;
import graphql.PublicApi;
import graphql.language.AstPrinter;
import graphql.language.AstValueHelper;
import graphql.language.Comment;
import graphql.language.Description;
import graphql.language.Document;
import graphql.language.Node;
import graphql.schema.GraphQLArgument;
import graphql.schema.GraphQLDirective;
import graphql.schema.GraphQLEnumType;
import graphql.schema.GraphQLEnumValueDefinition;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLInputObjectField;
import graphql.schema.GraphQLInputObjectType;
import graphql.schema.GraphQLInputType;
import graphql.schema.GraphQLInterfaceType;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLOutputType;
import graphql.schema.GraphQLScalarType;
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLType;
import graphql.schema.GraphQLTypeUtil;
import graphql.schema.GraphQLUnionType;
import graphql.schema.visibility.GraphqlFieldVisibility;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
/**
* This can print an in memory GraphQL schema back to a logical schema definition
*/
@PublicApi
public class SchemaPrinter {
/**
* Options to use when printing a schema
*/
public static class Options {
private final boolean includeIntrospectionTypes;
private final boolean includeScalars;
private final boolean includeExtendedScalars;
private final boolean includeSchemaDefinition;
private final boolean includeDirectives;
private final SchemaPrinterComparatorRegistry comparatorRegistry;
private Options(boolean includeIntrospectionTypes,
boolean includeScalars,
boolean includeExtendedScalars,
boolean includeSchemaDefinition,
boolean includeDirectives,
SchemaPrinterComparatorRegistry comparatorRegistry) {
this.includeIntrospectionTypes = includeIntrospectionTypes;
this.includeScalars = includeScalars;
this.includeExtendedScalars = includeExtendedScalars;
this.includeSchemaDefinition = includeSchemaDefinition;
this.includeDirectives = includeDirectives;
this.comparatorRegistry = comparatorRegistry;
}
public boolean isIncludeIntrospectionTypes() {
return includeIntrospectionTypes;
}
public boolean isIncludeScalars() {
return includeScalars;
}
public boolean isIncludeExtendedScalars() {
return includeExtendedScalars;
}
public boolean isIncludeSchemaDefinition() {
return includeSchemaDefinition;
}
public boolean isIncludeDirectives() {
return includeDirectives;
}
public static Options defaultOptions() {
return new Options(false, false, false, false, true,
DefaultSchemaPrinterComparatorRegistry.defaultComparators());
}
/**
* This will allow you to include introspection types that are contained in a schema
*
* @param flag whether to include them
*
* @return options
*/
public Options includeIntrospectionTypes(boolean flag) {
return new Options(flag, this.includeScalars, this.includeExtendedScalars, this.includeSchemaDefinition, this.includeDirectives, this.comparatorRegistry);
}
/**
* This will allow you to include scalar types that are contained in a schema
*
* @param flag whether to include them
*
* @return options
*/
public Options includeScalarTypes(boolean flag) {
return new Options(this.includeIntrospectionTypes, flag, this.includeExtendedScalars, this.includeSchemaDefinition, this.includeDirectives, this.comparatorRegistry);
}
/**
* This will allow you to include the graphql 'extended' scalar types that come with graphql-java such as
* GraphQLBigDecimal or GraphQLBigInteger
*
* @param flag whether to include them
*
* @return options
*/
public Options includeExtendedScalarTypes(boolean flag) {
return new Options(this.includeIntrospectionTypes, this.includeScalars, flag, this.includeSchemaDefinition, this.includeDirectives, this.comparatorRegistry);
}
/**
* This will force the printing of the graphql schema definition even if the query, mutation, and/or subscription
* types use the default names. Some graphql parsers require this information even if the schema uses the
* default type names. The schema definition will always be printed if any of the query, mutation, or subscription
* types do not use the default names.
*
* @param flag whether to force include the schema definition
*
* @return options
*/
public Options includeSchemaDefintion(boolean flag) {
return new Options(this.includeIntrospectionTypes, this.includeScalars, this.includeExtendedScalars, flag, this.includeDirectives, this.comparatorRegistry);
}
/**
* Allow to print directives. In some situations, auto-generated schemas contain a lot of directives that
* make the printout noisy and having this flag would allow cleaner printout. On by default.
*
* @param flag whether to print directives
*
* @return new instance of options
*/
public Options includeDirectives(boolean flag) {
return new Options(this.includeIntrospectionTypes, this.includeScalars, this.includeExtendedScalars, this.includeSchemaDefinition, flag, this.comparatorRegistry);
}
/**
* The comparator registry controls the printing order for registered {@code GraphQLType}s.
*
* @param comparatorRegistry The registry containing the {@code Comparator} and environment scoping rules.
*
* @return options
*/
public Options setComparators(SchemaPrinterComparatorRegistry comparatorRegistry) {
return new Options(this.includeIntrospectionTypes, this.includeScalars, this.includeExtendedScalars, this.includeSchemaDefinition, this.includeDirectives,
comparatorRegistry);
}
public SchemaPrinterComparatorRegistry getComparatorRegistry() {
return comparatorRegistry;
}
}
private final Map> printers = new LinkedHashMap<>();
private final Options options;
public SchemaPrinter() {
this(Options.defaultOptions());
}
public SchemaPrinter(Options options) {
this.options = options;
printers.put(GraphQLSchema.class, schemaPrinter());
printers.put(GraphQLObjectType.class, objectPrinter());
printers.put(GraphQLEnumType.class, enumPrinter());
printers.put(GraphQLScalarType.class, scalarPrinter());
printers.put(GraphQLInterfaceType.class, interfacePrinter());
printers.put(GraphQLUnionType.class, unionPrinter());
printers.put(GraphQLInputObjectType.class, inputObjectPrinter());
}
/**
* This can print an in memory GraphQL IDL document back to a logical schema definition.
* If you want to turn a Introspection query result into a Document (and then into a printed
* schema) then use {@link graphql.introspection.IntrospectionResultToSchema#createSchemaDefinition(java.util.Map)}
* first to get the {@link graphql.language.Document} and then print that.
*
* @param schemaIDL the parsed schema IDL
*
* @return the logical schema definition
*/
public String print(Document schemaIDL) {
TypeDefinitionRegistry registry = new SchemaParser().buildRegistry(schemaIDL);
return print(UnExecutableSchemaGenerator.makeUnExecutableSchema(registry));
}
/**
* This can print an in memory GraphQL schema back to a logical schema definition
*
* @param schema the schema in play
*
* @return the logical schema definition
*/
public String print(GraphQLSchema schema) {
StringWriter sw = new StringWriter();
PrintWriter out = new PrintWriter(sw);
GraphqlFieldVisibility visibility = schema.getCodeRegistry().getFieldVisibility();
printer(schema.getClass()).print(out, schema, visibility);
List typesAsList = schema.getAllTypesAsList()
.stream()
.sorted(Comparator.comparing(GraphQLType::getName))
.collect(toList());
printType(out, typesAsList, GraphQLInterfaceType.class, visibility);
printType(out, typesAsList, GraphQLUnionType.class, visibility);
printType(out, typesAsList, GraphQLObjectType.class, visibility);
printType(out, typesAsList, GraphQLEnumType.class, visibility);
printType(out, typesAsList, GraphQLScalarType.class, visibility);
printType(out, typesAsList, GraphQLInputObjectType.class, visibility);
String result = sw.toString();
if (result.endsWith("\n\n")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
private interface TypePrinter {
void print(PrintWriter out, T type, GraphqlFieldVisibility visibility);
}
private boolean isIntrospectionType(GraphQLType type) {
return !options.isIncludeIntrospectionTypes() && type.getName().startsWith("__");
}
private TypePrinter scalarPrinter() {
return (out, type, visibility) -> {
if (!options.isIncludeScalars()) {
return;
}
boolean printScalar;
if (ScalarInfo.isStandardScalar(type)) {
printScalar = false;
//noinspection RedundantIfStatement
if (options.isIncludeExtendedScalars() && !ScalarInfo.isGraphqlSpecifiedScalar(type)) {
printScalar = true;
}
} else {
printScalar = true;
}
if (printScalar) {
printComments(out, type, "");
out.format("scalar %s%s\n\n", type.getName(), directivesString(GraphQLScalarType.class, type.getDirectives()));
}
};
}
private TypePrinter enumPrinter() {
return (out, type, visibility) -> {
if (isIntrospectionType(type)) {
return;
}
SchemaPrinterComparatorEnvironment environment = SchemaPrinterComparatorEnvironment.newEnvironment()
.parentType(GraphQLEnumType.class)
.elementType(GraphQLEnumValueDefinition.class)
.build();
Comparator super GraphQLType> comparator = options.comparatorRegistry.getComparator(environment);
printComments(out, type, "");
out.format("enum %s%s {\n", type.getName(), directivesString(GraphQLEnumType.class, type.getDirectives()));
List values = type.getValues()
.stream()
.sorted(comparator)
.collect(toList());
for (GraphQLEnumValueDefinition enumValueDefinition : values) {
printComments(out, enumValueDefinition, " ");
out.format(" %s%s\n", enumValueDefinition.getName(), directivesString(GraphQLEnumValueDefinition.class, enumValueDefinition.getDirectives()));
}
out.format("}\n\n");
};
}
private TypePrinter interfacePrinter() {
return (out, type, visibility) -> {
if (isIntrospectionType(type)) {
return;
}
SchemaPrinterComparatorEnvironment environment = SchemaPrinterComparatorEnvironment.newEnvironment()
.parentType(GraphQLInterfaceType.class)
.elementType(GraphQLFieldDefinition.class)
.build();
Comparator super GraphQLType> comparator = options.comparatorRegistry.getComparator(environment);
printComments(out, type, "");
out.format("interface %s%s {\n", type.getName(), directivesString(GraphQLInterfaceType.class, type.getDirectives()));
visibility.getFieldDefinitions(type)
.stream()
.sorted(comparator)
.forEach(fd -> {
printComments(out, fd, " ");
out.format(" %s%s: %s%s\n",
fd.getName(), argsString(GraphQLFieldDefinition.class, fd.getArguments()), typeString(fd.getType()),
directivesString(GraphQLFieldDefinition.class, fd.getDirectives()));
});
out.format("}\n\n");
};
}
private TypePrinter unionPrinter() {
return (out, type, visibility) -> {
if (isIntrospectionType(type)) {
return;
}
SchemaPrinterComparatorEnvironment environment = SchemaPrinterComparatorEnvironment.newEnvironment()
.parentType(GraphQLUnionType.class)
.elementType(GraphQLOutputType.class)
.build();
Comparator super GraphQLType> comparator = options.comparatorRegistry.getComparator(environment);
printComments(out, type, "");
out.format("union %s%s = ", type.getName(), directivesString(GraphQLUnionType.class, type.getDirectives()));
List types = type.getTypes()
.stream()
.sorted(comparator)
.collect(toList());
for (int i = 0; i < types.size(); i++) {
GraphQLOutputType objectType = types.get(i);
if (i > 0) {
out.format(" | ");
}
out.format("%s", objectType.getName());
}
out.format("\n\n");
};
}
private TypePrinter objectPrinter() {
return (out, type, visibility) -> {
if (isIntrospectionType(type)) {
return;
}
printComments(out, type, "");
if (type.getInterfaces().isEmpty()) {
out.format("type %s%s {\n", type.getName(), directivesString(GraphQLObjectType.class, type.getDirectives()));
} else {
SchemaPrinterComparatorEnvironment environment = SchemaPrinterComparatorEnvironment.newEnvironment()
.parentType(GraphQLObjectType.class)
.elementType(GraphQLOutputType.class)
.build();
Comparator super GraphQLType> implementsComparator = options.comparatorRegistry.getComparator(environment);
Stream interfaceNames = type.getInterfaces()
.stream()
.sorted(implementsComparator)
.map(GraphQLType::getName);
out.format("type %s implements %s%s {\n",
type.getName(),
interfaceNames.collect(joining(" & ")),
directivesString(GraphQLObjectType.class, type.getDirectives()));
}
SchemaPrinterComparatorEnvironment environment = SchemaPrinterComparatorEnvironment.newEnvironment()
.parentType(GraphQLObjectType.class)
.elementType(GraphQLFieldDefinition.class)
.build();
Comparator super GraphQLType> comparator = options.comparatorRegistry.getComparator(environment);
visibility.getFieldDefinitions(type)
.stream()
.sorted(comparator)
.forEach(fd -> {
printComments(out, fd, " ");
out.format(" %s%s: %s%s\n",
fd.getName(), argsString(GraphQLFieldDefinition.class, fd.getArguments()), typeString(fd.getType()),
directivesString(GraphQLFieldDefinition.class, fd.getDirectives()));
});
out.format("}\n\n");
};
}
private TypePrinter inputObjectPrinter() {
return (out, type, visibility) -> {
if (isIntrospectionType(type)) {
return;
}
printComments(out, type, "");
SchemaPrinterComparatorEnvironment environment = SchemaPrinterComparatorEnvironment.newEnvironment()
.parentType(GraphQLInputObjectType.class)
.elementType(GraphQLInputObjectField.class)
.build();
Comparator super GraphQLType> comparator = options.comparatorRegistry.getComparator(environment);
out.format("input %s%s {\n", type.getName(), directivesString(GraphQLInputObjectType.class, type.getDirectives()));
visibility.getFieldDefinitions(type)
.stream()
.sorted(comparator)
.forEach(fd -> {
printComments(out, fd, " ");
out.format(" %s: %s",
fd.getName(), typeString(fd.getType()));
Object defaultValue = fd.getDefaultValue();
if (defaultValue != null) {
String astValue = printAst(defaultValue, fd.getType());
out.format(" = %s", astValue);
}
out.format(directivesString(GraphQLInputObjectField.class, fd.getDirectives()));
out.format("\n");
});
out.format("}\n\n");
};
}
private static String printAst(Object value, GraphQLInputType type) {
return AstPrinter.printAst(AstValueHelper.astFromValue(value, type));
}
private TypePrinter schemaPrinter() {
return (out, schema, visibility) -> {
GraphQLObjectType queryType = schema.getQueryType();
GraphQLObjectType mutationType = schema.getMutationType();
GraphQLObjectType subscriptionType = schema.getSubscriptionType();
// when serializing a GraphQL schema using the type system language, a
// schema definition should be omitted if only uses the default root type names.
boolean needsSchemaPrinted = options.includeSchemaDefinition;
if (!needsSchemaPrinted) {
if (queryType != null && !queryType.getName().equals("Query")) {
needsSchemaPrinted = true;
}
if (mutationType != null && !mutationType.getName().equals("Mutation")) {
needsSchemaPrinted = true;
}
if (subscriptionType != null && !subscriptionType.getName().equals("Subscription")) {
needsSchemaPrinted = true;
}
}
if (needsSchemaPrinted) {
out.format("schema {\n");
if (queryType != null) {
out.format(" query: %s\n", queryType.getName());
}
if (mutationType != null) {
out.format(" mutation: %s\n", mutationType.getName());
}
if (subscriptionType != null) {
out.format(" subscription: %s\n", subscriptionType.getName());
}
out.format("}\n\n");
}
if (options.includeDirectives) {
out.format("%s", directiveDefinitions(getDirectives(schema)));
}
};
}
private List getDirectives(GraphQLSchema schema) {
// we don't print the standard directives that always ship with graphql-java
List standardDirectives = Arrays.asList(
"skip",
"include",
"defer",
"deprecated");
Predicate standard = directive -> standardDirectives.contains(directive.getName());
return schema.getDirectives().stream().filter(standard.negate()).collect(Collectors.toList());
}
String typeString(GraphQLType rawType) {
return GraphQLTypeUtil.simplePrint(rawType);
}
String argsString(List arguments) {
return argsString(null, arguments);
}
String argsString(Class extends GraphQLType> parent, List arguments) {
boolean hasDescriptions = arguments.stream().anyMatch(arg -> !isNullOrEmpty(arg.getDescription()));
String halfPrefix = hasDescriptions ? " " : "";
String prefix = hasDescriptions ? " " : "";
int count = 0;
StringBuilder sb = new StringBuilder();
SchemaPrinterComparatorEnvironment environment = SchemaPrinterComparatorEnvironment.newEnvironment()
.parentType(parent)
.elementType(GraphQLArgument.class)
.build();
Comparator super GraphQLType> comparator = options.comparatorRegistry.getComparator(environment);
arguments = arguments
.stream()
.sorted(comparator)
.collect(toList());
for (GraphQLArgument argument : arguments) {
if (count == 0) {
sb.append("(");
} else {
sb.append(", ");
}
if (hasDescriptions) {
sb.append("\n");
}
String description = argument.getDescription();
if (!isNullOrEmpty(description)) {
String[] descriptionSplitByNewlines = description.split("\n");
Stream stream = Arrays.stream(descriptionSplitByNewlines);
if (descriptionSplitByNewlines.length > 1) {
String multiLineComment = "\"\"\"";
stream = Stream.concat(Stream.of(multiLineComment), stream);
stream = Stream.concat(stream, Stream.of(multiLineComment));
stream.map(s -> prefix + s + "\n").forEach(sb::append);
} else {
stream.map(s -> prefix + "#" + s + "\n").forEach(sb::append);
}
}
sb.append(prefix).append(argument.getName()).append(": ").append(typeString(argument.getType()));
Object defaultValue = argument.getDefaultValue();
if (defaultValue != null) {
sb.append(" = ");
sb.append(printAst(defaultValue, argument.getType()));
}
argument.getDirectives().stream()
.map(this::directiveString)
.filter(it -> !it.isEmpty())
.forEach(directiveString -> sb.append(" ").append(directiveString));
count++;
}
if (count > 0) {
if (hasDescriptions) {
sb.append("\n");
}
sb.append(halfPrefix).append(")");
}
return sb.toString();
}
String directivesString(Class extends GraphQLType> parent, List directives) {
if (!options.includeDirectives) {
return "";
}
StringBuilder sb = new StringBuilder();
if (!directives.isEmpty()) {
sb.append(" ");
}
SchemaPrinterComparatorEnvironment environment = SchemaPrinterComparatorEnvironment.newEnvironment()
.parentType(parent)
.elementType(GraphQLDirective.class)
.build();
Comparator super GraphQLType> comparator = options.comparatorRegistry.getComparator(environment);
directives = directives
.stream()
.sorted(comparator)
.collect(toList());
for (int i = 0; i < directives.size(); i++) {
GraphQLDirective directive = directives.get(i);
sb.append(directiveString(directive));
if (i < directives.size() - 1) {
sb.append(" ");
}
}
return sb.toString();
}
private String directiveString(GraphQLDirective directive) {
if (!options.includeDirectives) {
return "";
}
StringBuilder sb = new StringBuilder();
sb.append("@").append(directive.getName());
SchemaPrinterComparatorEnvironment environment = SchemaPrinterComparatorEnvironment.newEnvironment()
.parentType(GraphQLDirective.class)
.elementType(GraphQLArgument.class)
.build();
Comparator super GraphQLType> comparator = options.comparatorRegistry.getComparator(environment);
List args = directive.getArguments();
args = args
.stream()
.sorted(comparator)
.collect(toList());
if (!args.isEmpty()) {
sb.append("(");
for (int i = 0; i < args.size(); i++) {
GraphQLArgument arg = args.get(i);
sb.append(arg.getName());
if (arg.getValue() != null) {
sb.append(" : ");
sb.append(printAst(arg.getValue(), arg.getType()));
} else if (arg.getDefaultValue() != null) {
sb.append(" : ");
sb.append(printAst(arg.getDefaultValue(), arg.getType()));
}
if (i < args.size() - 1) {
sb.append(", ");
}
}
sb.append(")");
}
return sb.toString();
}
private String directiveDefinitions(List directives) {
StringBuilder sb = new StringBuilder();
for (GraphQLDirective directive : directives) {
sb.append(directiveDefinition(directive));
sb.append("\n\n");
}
return sb.toString();
}
private String directiveDefinition(GraphQLDirective directive) {
StringBuilder sb = new StringBuilder();
StringWriter sw = new StringWriter();
printComments(new PrintWriter(sw), directive, "");
sb.append(sw.toString());
sb.append("directive @").append(directive.getName());
SchemaPrinterComparatorEnvironment environment = SchemaPrinterComparatorEnvironment.newEnvironment()
.parentType(GraphQLDirective.class)
.elementType(GraphQLArgument.class)
.build();
Comparator super GraphQLType> comparator = options.comparatorRegistry.getComparator(environment);
List args = directive.getArguments();
args = args
.stream()
.sorted(comparator)
.collect(toList());
sb.append(argsString(GraphQLDirective.class, args));
sb.append(" on ");
String locations = directive.validLocations().stream().map(Enum::name).collect(Collectors.joining(" | "));
sb.append(locations);
return sb.toString();
}
@SuppressWarnings("unchecked")
private TypePrinter printer(Class> clazz) {
TypePrinter typePrinter = printers.computeIfAbsent(clazz, k -> {
Class> superClazz = clazz.getSuperclass();
TypePrinter result;
if (superClazz != Object.class) {
result = printer(superClazz);
} else {
result = (out, type, visibility) -> out.println("Type not implemented : " + type);
}
return result;
});
return (TypePrinter) typePrinter;
}
public String print(GraphQLType type) {
StringWriter sw = new StringWriter();
PrintWriter out = new PrintWriter(sw);
printType(out, type, DEFAULT_FIELD_VISIBILITY);
return sw.toString();
}
@SuppressWarnings("unchecked")
private void printType(PrintWriter out, List typesAsList, Class typeClazz, GraphqlFieldVisibility visibility) {
typesAsList.stream()
.filter(type -> typeClazz.isAssignableFrom(type.getClass()))
.forEach(type -> printType(out, type, visibility));
}
private void printType(PrintWriter out, GraphQLType type, GraphqlFieldVisibility visibility) {
TypePrinter
© 2015 - 2025 Weber Informatics LLC | Privacy Policy