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

io.smallrye.graphql.client.model.helper.OperationModel Maven / Gradle / Ivy

The newest version!
package io.smallrye.graphql.client.model.helper;

import static io.smallrye.graphql.client.model.Annotations.MULTIPLE;
import static io.smallrye.graphql.client.model.Annotations.MUTATION;
import static io.smallrye.graphql.client.model.Annotations.NAME;
import static io.smallrye.graphql.client.model.Annotations.NAMESPACE;
import static io.smallrye.graphql.client.model.Annotations.QUERY;
import static io.smallrye.graphql.client.model.Annotations.SUBCRIPTION;
import static io.smallrye.graphql.client.model.ScanningContext.getIndex;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Stack;
import java.util.stream.Collectors;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.DotName;
import org.jboss.jandex.JandexReflection;
import org.jboss.jandex.MethodInfo;

import io.smallrye.graphql.client.core.OperationType;
import io.smallrye.graphql.client.model.MethodKey;

/**
 * Represents a model for a GraphQL operation method, providing methods to generate GraphQL query fields,
 * handle parameter bindings, and extract operation-related information.
 *
 * @author mskacelik
 */
public class OperationModel implements NamedElement {
    private final MethodInfo method;
    private final List parameters;
    private final Stack typeStack = new Stack<>();
    private final Stack expressionStack = new Stack<>();
    private Stack rawParametrizedTypes = new Stack<>();
    private final List directives;
    private final String operationName;
    private final List namespaces;

    /**
     * Creates a new {@code OperationModel} instance based on the provided Jandex {@link MethodInfo}.
     *
     * @param method The Jandex {@link MethodInfo} representing the GraphQL operation method.
     */
    OperationModel(MethodInfo method) {
        this.method = method;
        this.parameters = method.parameters().stream().map(ParameterModel::of).collect(Collectors.toList());
        this.directives = DirectiveHelper.resolveDirectives(method.annotations().stream(),
                getDirectiveLocation(), AnnotationTarget.Kind.METHOD)
                .map(DirectiveInstance::of)
                .collect(toList());
        this.namespaces = readNamespaces(method);
        this.operationName = readOperationName(this.namespaces);
    }

    /**
     * Creates and returns a new {@code OperationModel} instance based on the provided Jandex {@link MethodInfo}.
     *
     * @param method The Jandex {@link MethodInfo} representing the GraphQL operation method.
     * @return A new {@code OperationModel} instance.
     */
    public static OperationModel of(MethodInfo method) {
        return new OperationModel(method);
    }

    /**
     * Generates GraphQL query fields for the specified {@code TypeModel}.
     *
     * @param type The {@link TypeModel} for which to generate GraphQL query fields.
     * @return The generated GraphQL query fields.
     * @throws IllegalStateException If a field recursion is detected.
     */
    public String fields(TypeModel type) {
        if (typeStack.contains(type.getName()))
            throw new IllegalStateException("field recursion found");
        try {
            typeStack.push(type.getName());
            return recursionCheckedFields(type);
        } finally {
            typeStack.pop();
        }
    }

    /**
     * Generates GraphQL query fields for the specified {@code TypeModel} with recursion checking.
     *
     * @param type The {@link TypeModel} for which to generate GraphQL query fields.
     * @return The generated GraphQL query fields.
     */
    public String recursionCheckedFields(TypeModel type) {
        while (type.isOptional() || type.isErrorOr() || type.isTypesafeResponse()) {
            type = type.getFirstRawType(); // unwrapping
        }
        if (type.isScalar())
            return "";
        if (type.isCollectionOrArray() || type.isAsync())
            return fields(type.getItemTypeOrElementType());
        if (type.isMap()) {
            String keyFields = fields(type.getMapKeyType());
            String valueFields = fields(type.getMapValueType());
            return " {key" + keyFields + " value" + valueFields + "}";
        }
        if (type.isUnion() || type.isInterface()) {
            return "{__typename " + type.subtypes()
                    .sorted(Comparator.comparing(TypeModel::getGraphQlTypeName)) // for deterministic order
                    .map(this::fieldsFragment)
                    .collect(joining(" ")) +
                    "}";
        }
        if (isRawParametrizedType(type)) {
            rawParametrizedTypes.push(type.getFirstRawType());
        }
        String fieldsResult = type.fields()
                .map(this::field)
                .collect(joining(" ", " {", "}"));
        if (isRawParametrizedType(type)) {
            rawParametrizedTypes.pop();
        }
        return fieldsResult;

    }

    /**
     * Generates the GraphQL representation of a field based on the provided {@code FieldModel}.
     *
     * @param field The {@link FieldModel} representing the field.
     * @return The GraphQL representation of the field.
     */
    public String field(FieldModel field) {
        TypeModel type = field.getType();
        if (type.isTypeVariable()) {
            type = rawParametrizedTypes.peek();
        }
        StringBuilder expression = new StringBuilder();
        field.getAlias().ifPresent(alias -> expression.append(alias).append(":"));
        expression.append(field.getName());

        if (field.hasDirectives()) {
            expression.append(field.getDirectives().stream().map(DirectiveInstance::buildDirective).collect(joining()));
        }

        String path = nestedExpressionPrefix() + field.getRawName();
        List nestedParameters = nestedParameters(path);
        if (!nestedParameters.isEmpty()) {
            expression.append(nestedParameters.stream()
                    .map(this::bind)
                    .collect(joining(", ", "(", ")")));
        }

        expressionStack.push(path);
        expression.append(fields(type)); // appends the empty string, if the type is scalar, etc.
        expressionStack.pop();

        return expression.toString();
    }

    /**
     * Declares a GraphQL parameter for the specified {@code ParameterModel}.
     *
     * @param parameter The {@link ParameterModel} for which to declare the GraphQL parameter.
     * @return The GraphQL declaration of the parameter.
     */
    public String declare(ParameterModel parameter) {
        return "$" + parameter.getRawName() + ": " + parameter.graphQlInputTypeName() +
                ((parameter.hasDirectives()) ? parameter.getDirectives()
                        .stream()
                        .map(DirectiveInstance::buildDirective)
                        .collect(joining())
                        : "");
    }

    /**
     * Binds a GraphQL parameter for the specified {@code ParameterModel}.
     *
     * @param parameter The {@link ParameterModel} for which to bind the GraphQL parameter.
     * @return The GraphQL binding of the parameter.
     */
    public String bind(ParameterModel parameter) {
        return parameter.getName() + ": $" + parameter.getRawName();
    }

    /**
     * Retrieves the prefix for nested expressions in GraphQL queries.
     *
     * @return The prefix for nested expressions.
     */
    public String nestedExpressionPrefix() {
        return expressionStack.isEmpty() ? "" : expressionStack.peek() + ".";
    }

    /**
     * Gets the operation type of the GraphQL operation.
     *
     * @return The {@link OperationType} of the GraphQL operation.
     */
    public OperationType getOperationType() {
        if (method.hasAnnotation(MUTATION)) {
            return OperationType.MUTATION;
        }
        if (method.hasAnnotation(SUBCRIPTION)) {
            return OperationType.SUBSCRIPTION;
        }
        return OperationType.QUERY;
    }

    /**
     * Gets the name of the GraphQL operation, considering any {@link org.eclipse.microprofile.graphql.Query}
     * or {@link org.eclipse.microprofile.graphql.Name} annotations.
     *
     * @return An optional containing the operation name if specified, otherwise empty.
     */
    public Optional queryName() {
        Optional queryAnnotation = getMethodAnnotation(QUERY);
        if (queryAnnotation.isPresent() && queryAnnotation.orElseThrow().value() != null)
            return Optional.of(queryAnnotation.orElseThrow().value().asString());
        Optional nameAnnotation = getMethodAnnotation(NAME);
        if (nameAnnotation.isPresent())
            return Optional.of(nameAnnotation.orElseThrow().value().asString());
        return Optional.empty();
    }

    /**
     * Gets the name of the GraphQL mutation, considering any {@link org.eclipse.microprofile.graphql.Mutation} annotation.
     *
     * @return An optional containing the mutation name if specified, otherwise empty.
     */
    public Optional mutationName() {
        Optional mutationAnnotation = getMethodAnnotation(MUTATION);
        if (mutationAnnotation.isPresent() && mutationAnnotation.orElseThrow().value() != null)
            return Optional.of(mutationAnnotation.orElseThrow().value().asString());
        return Optional.empty();
    }

    /**
     * Gets the name of the GraphQL subscription, considering any io.smallrye.graphql.api.Subscription annotation.
     *
     * @return An optional containing the subscription name if specified, otherwise empty.
     */
    public Optional subscriptionName() {
        Optional subscriptionAnnotation = getMethodAnnotation(SUBCRIPTION);
        if (subscriptionAnnotation.isPresent() && subscriptionAnnotation.orElseThrow().value() != null)
            return Optional.of(subscriptionAnnotation.orElseThrow().value().asString());
        return Optional.empty();
    }

    @Override
    public String getName() {
        return queryName()
                .orElseGet(() -> mutationName()
                        .orElseGet(() -> subscriptionName()
                                .orElseGet(this::getRawName)));
    }

    @Override
    public String getRawName() {
        String name = method.name();
        if (name.startsWith("get") && name.length() > 3 && Character.isUpperCase(name.charAt(3)))
            return Character.toLowerCase(name.charAt(3)) + name.substring(4);
        return name;
    }

    @Override
    public String getDirectiveLocation() {
        // currently the definition is that, if there is a method targeted directive, that directive
        // will be transformed to high-level field. So the current implementation does not work with
        // QUERY, MUTATION, SUBSCRIPTION locations...
        return "FIELD";
    }

    @Override
    public boolean hasDirectives() {
        return !directives.isEmpty();
    }

    @Override
    public List getDirectives() {
        return directives;
    }

    /**
     * Retrieves the list of parameters representing the values for the GraphQL operation.
     *
     * @return The list of value parameters.
     */
    public List valueParameters() {
        return parameters.stream().filter(ParameterModel::isValueParameter).collect(Collectors.toList());
    }

    /**
     * Retrieves the list of parameters representing the root-level parameters for the GraphQL operation.
     *
     * @return The list of root parameters.
     */
    public List rootParameters() {
        return parameters.stream().filter(ParameterModel::isRootParameter).collect(Collectors.toList());
    }

    /**
     * Retrieves the list of parameters representing nested parameters for the GraphQL operation.
     *
     * @param path The path for which nested parameters are retrieved.
     * @return The list of nested parameters.
     */
    public List nestedParameters(String path) {
        return parameters.stream()
                .filter(ParameterModel::isNestedParameter)
                .filter(parameter -> parameter
                        .getNestedParameterNames()
                        .anyMatch(path::equals))
                .collect(toList());
    }

    /**
     * Gets the return type of the GraphQL operation.
     *
     * @return The {@link TypeModel} representing the return type.
     */
    public TypeModel getReturnType() {
        return TypeModel.of(method.returnType());
    }

    /**
     * Checks if the GraphQL operation has value parameters.
     *
     * @return {@code true} if the operation has value parameters, otherwise {@code false}.
     */
    public boolean hasValueParameters() {
        return !valueParameters().isEmpty();
    }

    /**
     * Checks if the GraphQL operation has root-level parameters.
     *
     * @return {@code true} if the operation has root parameters, otherwise {@code false}.
     */
    public boolean hasRootParameters() {
        return !rootParameters().isEmpty();
    }

    /**
     * Checks if the GraphQL operation returns a single result (one GraphQL operation).
     *
     * @return {@code true} if the operation returns a single result, otherwise {@code false}.
     */
    public boolean isSingle() {
        return getReturnType().isScalar()
                || getReturnType().isParametrized()
                || getReturnType().isArray()
                || !getIndex().getClassByName(method.returnType().name()).hasAnnotation(MULTIPLE);
    }

    /**
     * Gets the key for identifying the GraphQL operation method.
     *
     * @return The {@link MethodKey} representing the key for the operation method (name, parameters types).
     */
    public MethodKey getMethodKey() {
        return new MethodKey(method.name(), method.parameters().stream()
                .map(methodParameterInfo -> JandexReflection.loadRawType(methodParameterInfo.type())).toArray(Class[]::new));
    }

    /**
     * Gets the string representation of the operation type (query, mutation, or subscription).
     *
     * @return The string representation of the operation type.
     */
    public String getOperationTypeAsString() {
        switch (getOperationType()) {
            case MUTATION:
                return "mutation";
            case SUBSCRIPTION:
                return "subscription";
            default:
                return "query";
        }
    }

    /**
     * Retrieves a non-repeatable annotation for the GraphQL operation method.
     *
     * @param annotation The {@link DotName} representing the annotation to retrieve.
     * @return An optional containing the annotation instance if present, otherwise empty.
     */
    private Optional getMethodAnnotation(DotName annotation) {
        return method.annotations().stream()
                .filter(annotationInstance -> annotationInstance.target().kind() == AnnotationTarget.Kind.METHOD)
                .filter(annotationInstance -> annotationInstance.name().equals(annotation)).findFirst();
    }

    /**
     * Checks if the given TypeModel represents a raw parametrized type, excluding type variables.
     *
     * @param type The TypeModel to check.
     * @return {@code true} if the TypeModel represents a raw parametrized type (excluding type variables), otherwise
     *         {@code false}.
     */
    private boolean isRawParametrizedType(TypeModel type) {
        return type.isCustomParametrizedType() && !type.getFirstRawType().isTypeVariable();
    }

    public List getNamespaces() {
        return namespaces;
    }

    public String getOperationName() {
        return operationName;
    }

    private List readNamespaces(MethodInfo method) {
        if (method.declaringClass().hasDeclaredAnnotation(NAMESPACE)) {
            String[] names = method.declaringClass().declaredAnnotation(NAMESPACE).value().asStringArray();
            if (names.length > 0) {
                return List.of(names);
            }
        } else if (method.declaringClass().hasDeclaredAnnotation(NAME)) {
            String value = method.declaringClass().declaredAnnotation(NAME).value().asString();
            if (!value.isEmpty()) {
                return List.of(value);
            }
        }
        return List.of();
    }

    private String readOperationName(List names) {
        if (names.isEmpty()) {
            return getName();
        } else {
            String namespace = names.stream()
                    .map(this::makeFirstLetterUppercase)
                    .collect(joining());
            return namespace + makeFirstLetterUppercase(getName());
        }
    }

    private String makeFirstLetterUppercase(String value) {
        return value.substring(0, 1).toUpperCase() + value.substring(1);
    }

    private String fieldsFragment(TypeModel type) {
        return "... on " + type.getGraphQlTypeName() + fields(type);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy