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

io.smallrye.graphql.client.generator.Generator Maven / Gradle / Ivy

package io.smallrye.graphql.client.generator;

import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collector;
import java.util.stream.Stream;

import graphql.language.Argument;
import graphql.language.Document;
import graphql.language.Field;
import graphql.language.FieldDefinition;
import graphql.language.ListType;
import graphql.language.NonNullType;
import graphql.language.ObjectTypeDefinition;
import graphql.language.OperationDefinition;
import graphql.language.SelectionSet;
import graphql.language.Type;
import graphql.language.TypeName;
import graphql.language.Value;
import graphql.language.VariableDefinition;
import graphql.language.VariableReference;
import graphql.parser.Parser;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;

public class Generator {
    private final String pkg;
    private final String apiTypeName;
    private final String schemaString;
    private final List queryStrings;

    private TypeDefinitionRegistry schema;
    private List queries;

    public Generator(String pkg, String apiTypeName, String schema, List queries) {
        this.pkg = pkg;
        this.apiTypeName = apiTypeName;
        this.schemaString = schema;
        this.queryStrings = queries;
    }

    public Map generateSourceFiles() {
        this.schema = parseSchema();
        this.queries = parseQueries();
        Map sourceFiles = new LinkedHashMap<>();

        new Api().addTo(sourceFiles);

        return sourceFiles;
    }

    private TypeDefinitionRegistry parseSchema() {
        try {
            return new SchemaParser().parse(schemaString);
        } catch (Exception e) {
            throw new GraphQLGeneratorException("can't parse schema: " + schemaString, e);
        }
    }

    private List parseQueries() {
        return queryStrings.stream()
                .map(this::query)
                .collect(toList());
    }

    private Document query(String query) {
        try {
            return Parser.parse(query);
        } catch (Exception e) {
            throw new GraphQLGeneratorException("can't parse query: " + query, e);
        }
    }

    private abstract class SourceFileGenerator {
        protected final String typeType;

        protected final Set imports = new TreeSet<>();

        protected abstract String generateBody();

        final List other = new ArrayList<>();

        public SourceFileGenerator(String typeType) {
            this.typeType = typeType;
        }

        public abstract String getTypeName();

        protected class JavaType {
            private final Type type;
            private final List fieldNames;

            public JavaType(Type type, List fieldNames) {
                this.type = type;
                this.fieldNames = fieldNames;
            }

            @Override
            public String toString() {
                if (type instanceof ListType)
                    return toListJava((ListType) type);
                if (type instanceof NonNullType)
                    return toNonNullJava((NonNullType) type);
                if (type instanceof TypeName)
                    return toJava((TypeName) type);
                throw new UnsupportedOperationException("unexpected type of type"); // unreachable
            }

            private String toListJava(ListType type) {
                imports.add("java.util.List");
                return "List<" + new JavaType(type.getType(), fieldNames) + ">";
            }

            private String toNonNullJava(NonNullType type) {
                imports.add("org.eclipse.microprofile.graphql.NonNull");
                return "@NonNull " + new JavaType(type.getType(), fieldNames);
            }

            private String toJava(TypeName typeName) {
                String string = typeName.getName();
                switch (string) {
                    case "Int":
                        return "Integer";
                    case "Boolean":
                    case "Float":
                    case "String":
                        return string;
                    case "ID":
                        return "String";
                    default:
                        other.add(new TypeGenerator(string, fieldNames));
                        return string;
                }
            }
        }

        public void addTo(Map sourceFiles) {
            String body = generateBody();
            String source = "package " + pkg + ";\n" +
                    "\n" +
                    imports() +
                    "public " + typeType + " " + getTypeName() + " {\n" +
                    body +
                    "}\n";
            String previousSource = sourceFiles.put(pkg + "." + getTypeName(), source);
            if (previousSource != null && !previousSource.equals(source))
                throw new GraphQLGeneratorException("already generated " + getTypeName());
            other.forEach(it -> it.addTo(sourceFiles));
        }

        private String imports() {
            return imports.isEmpty() ? "" : imports.stream().collect(joining(";\nimport ", "import ", ";\n\n"));
        }
    }

    private class Api extends SourceFileGenerator {
        private final StringBuilder body = new StringBuilder();

        public Api() {
            super("interface");
        }

        @Override
        public String getTypeName() {
            return apiTypeName;
        }

        @Override
        protected String generateBody() {
            queries.forEach(this::generateQueryMethod);

            return body.toString();
        }

        private void generateQueryMethod(Document query) {
            List definitions = query.getDefinitionsOfType(OperationDefinition.class);
            if (definitions.size() != 1)
                throw new GraphQLGeneratorException("expected exactly one definition but found "
                        + definitions.stream().map(this::operationInfo).collect(listString()));
            OperationDefinition operation = definitions.get(0);
            List fields = operation.getSelectionSet().getSelectionsOfType(Field.class);
            if (fields.size() != 1)
                throw new GraphQLGeneratorException("expected exactly one field but got "
                        + fields.stream().map(Field::getName).collect(listString()));
            Field field = fields.get(0);
            body.append(new MethodGenerator(operation, field));
        }

        private String operationInfo(OperationDefinition definition) {
            return definition.getOperation().toString().toLowerCase() + " " + definition.getName();
        }

        private class MethodGenerator {
            private final OperationDefinition operation;
            private final Field method;

            private final StringBuilder annotations = new StringBuilder();

            public MethodGenerator(OperationDefinition operation, Field method) {
                this.operation = operation;
                this.method = method;
            }

            @Override
            public String toString() {
                JavaType returnType = returnType();
                String methodName = methodName();
                String argumentList = argumentList();
                return "    " + this.annotations + returnType + " " + methodName + argumentList + ";\n";
            }

            private JavaType returnType() {
                ObjectTypeDefinition query = (ObjectTypeDefinition) schema.getType("Query")
                        .orElseThrow(() -> new GraphQLGeneratorException("'Query' type not found in schema"));
                FieldDefinition fieldDefinition = query.getFieldDefinitions().stream()
                        .filter(f -> f.getName().equals(method.getName()))
                        .findAny()
                        .orElseThrow(() -> new GraphQLGeneratorException("field (method) '" + method.getName()
                                + "' not found in "
                                + query.getFieldDefinitions().stream().map(FieldDefinition::getName).collect(listString())));
                List selections = operation.getSelectionSet().getSelectionsOfType(Field.class).stream()
                        .flatMap(this::selectedFields)
                        .map(Field::getName)
                        .collect(toList());
                return new JavaType(fieldDefinition.getType(), selections);
            }

            private Stream selectedFields(Field field) {
                SelectionSet selectionSet = field.getSelectionSet();
                return (selectionSet == null) ? Stream.of() : selectionSet.getSelectionsOfType(Field.class).stream();
            }

            public String methodName() {
                if (method.getAlias() != null) {
                    nameQuery();
                    return method.getAlias();
                }
                if (operation.getName() == null)
                    return method.getName();
                if (!operation.getName().equals(method.getName()))
                    nameQuery();
                return operation.getName();
            }

            private void nameQuery() {
                imports.add("org.eclipse.microprofile.graphql.Query");
                annotations.append("@Query(\"").append(method.getName()).append("\") ");
            }

            public String argumentList() {
                return new ArgumentList(method, operation.getVariableDefinitions()).toString();
            }

            private class ArgumentList {
                private final Field field;
                private final List variableDefinitions;

                public ArgumentList(Field field, List variableDefinitions) {
                    this.field = field;
                    this.variableDefinitions = variableDefinitions;
                }

                @Override
                public String toString() {
                    return field.getArguments().stream()
                            .map(argument -> new JavaType(type(argument), emptyList()) + " " + argument.getName())
                            .collect(joining(", ", "(", ")"));
                }

                private Type type(Argument argument) {
                    Value value = argument.getValue();
                    if (value instanceof VariableReference)
                        return resolve(((VariableReference) value).getName());
                    throw new GraphQLGeneratorException(
                            "unsupported type " + value + " for argument '" + argument.getName() + "'");
                }

                private Type resolve(String name) {
                    return variableDefinitions.stream()
                            .filter(var -> var.getName().equals(name))
                            .findAny()
                            .map(VariableDefinition::getType)
                            .orElseThrow(() -> new GraphQLGeneratorException("no definition found for parameter '" + name
                                    + "' in "
                                    + variableDefinitions.stream().map(VariableDefinition::getName).collect(listString())));
                }
            }
        }
    }

    private class TypeGenerator extends SourceFileGenerator {
        private final String typeName;
        private final List fieldNames;

        public TypeGenerator(String typeName, List fieldNames) {
            super("class");
            this.typeName = typeName;
            this.fieldNames = fieldNames;
        }

        @Override
        public String getTypeName() {
            return typeName;
        }

        @Override
        protected String generateBody() {
            ObjectTypeDefinition typeDefinition = (ObjectTypeDefinition) schema.getType(typeName)
                    .orElseThrow(() -> new GraphQLGeneratorException("type '" + typeName + "' not found in schema"));
            return typeDefinition.getFieldDefinitions().stream()
                    .filter(field -> fieldNames.contains(field.getName()))
                    .map(this::toJava)
                    .collect(joining(";\n    ", "    ", ";\n"));
        }

        private String toJava(FieldDefinition field) {
            return new JavaType(field.getType(), fieldNames) + " " + field.getName();
        }
    }

    private static Collector listString() {
        return joining(", ", "[", "]");
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy