diff --git a/client/model-builder/pom.xml b/client/model-builder/pom.xml
new file mode 100644
index 000000000..c717935f7
--- /dev/null
+++ b/client/model-builder/pom.xml
@@ -0,0 +1,192 @@
+
+
+ 4.0.0
+
+ io.smallrye
+ smallrye-graphql-client-parent
+ 2.6.1-SNAPSHOT
+
+
+ smallrye-graphql-client-model-builder
+ SmallRye: GraphQL Client :: model builder
+
+
+ ${project.groupId}
+ smallrye-graphql-client-model
+
+
+
+
+ io.smallrye
+ jandex
+
+
+
+ org.jboss.logging
+ jboss-logging
+ provided
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+
+
+ io.smallrye
+ smallrye-graphql-client-api
+
+
+ org.eclipse.microprofile.graphql
+ microprofile-graphql-api
+
+
+ io.smallrye.reactive
+ mutiny
+
+
+ io.smallrye
+ smallrye-graphql-client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Annotations.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Annotations.java
new file mode 100644
index 000000000..01bf6873d
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Annotations.java
@@ -0,0 +1,664 @@
+package io.smallrye.graphql.client.model;
+
+import static java.util.Collections.emptyMap;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationTarget.Kind;
+import org.jboss.jandex.AnnotationValue;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.FieldInfo;
+import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.MethodParameterInfo;
+import org.jboss.jandex.Type;
+
+import io.smallrye.graphql.client.model.helper.Direction;
+
+/**
+ * All the annotations we care about for a certain context
+ *
+ * There are multiple static methods to create the annotations for the correct context
+ *
+ * @author Phillip Kruger (phillip.kruger@redhat.com)
+ */
+public class Annotations {
+
+ private final Map annotationsMap;
+
+ public final Map parentAnnotations;
+
+ /**
+ * Get used when creating operations.
+ * Operation only have methods (no properties)
+ *
+ * @param methodInfo the java method
+ * @return Annotations for this method and its return-type
+ */
+ public static Annotations getAnnotationsForMethod(MethodInfo methodInfo) {
+ Map annotationMap = new HashMap<>();
+
+ for (AnnotationInstance annotationInstance : methodInfo.annotations()) {
+ DotName name = annotationInstance.name();
+ Kind kind = annotationInstance.target().kind();
+ if (kind.equals(Kind.METHOD)) {
+ annotationMap.put(name, annotationInstance);
+ }
+ }
+
+ final Type type = methodInfo.returnType();
+ if (Classes.isParameterized(type)) {
+ Type wrappedType = type.asParameterizedType().arguments().get(0);
+ for (final AnnotationInstance annotationInstance : wrappedType.annotations()) {
+ DotName name = annotationInstance.name();
+ annotationMap.put(name, annotationInstance);
+ }
+ }
+
+ Map parentAnnotations = getParentAnnotations(methodInfo.declaringClass());
+
+ return new Annotations(annotationMap, parentAnnotations);
+ }
+
+ private static Map getParentAnnotations(FieldInfo fieldInfo, MethodInfo methodInfo) {
+ ClassInfo declaringClass = fieldInfo != null ? fieldInfo.declaringClass() : methodInfo.declaringClass();
+ return getParentAnnotations(declaringClass);
+ }
+
+ private static Map getParentAnnotations(ClassInfo classInfo) {
+ Map parentAnnotations = new HashMap<>();
+
+ for (AnnotationInstance classAnnotation : classInfo.declaredAnnotations()) {
+ parentAnnotations.putIfAbsent(classAnnotation.name(), classAnnotation);
+ }
+
+ Map packageAnnotations = getPackageAnnotations(classInfo);
+ for (DotName dotName : packageAnnotations.keySet()) {
+ parentAnnotations.putIfAbsent(dotName, packageAnnotations.get(dotName));
+ }
+
+ return parentAnnotations;
+ }
+
+ private static Map getPackageAnnotations(ClassInfo classInfo) {
+ Map packageAnnotations = new HashMap<>();
+
+ DotName packageName = packageInfo(classInfo);
+ if (packageName != null) {
+ ClassInfo packageInfo = ScanningContext.getIndex().getClassByName(packageName);
+ if (packageInfo != null) {
+ for (AnnotationInstance packageAnnotation : packageInfo.declaredAnnotations()) {
+ packageAnnotations.putIfAbsent(packageAnnotation.name(), packageAnnotation);
+ }
+ }
+ }
+
+ return packageAnnotations;
+ }
+
+ private static DotName packageInfo(ClassInfo classInfo) {
+ String className = classInfo.name().toString();
+ int index = className.lastIndexOf('.');
+ if (index == -1) {
+ return null;
+ }
+ return DotName.createSimple(className.substring(0, index) + ".package-info");
+ }
+
+ /**
+ * Get used when creating fields on interfaces.
+ * Interfaces only has methods, no properties
+ *
+ * @param methodInfo the java method
+ * @return Annotations for this method
+ */
+ public static Annotations getAnnotationsForInterfaceField(MethodInfo methodInfo) {
+ return getAnnotationsForOutputField(null, methodInfo);
+ }
+
+ /**
+ * Get used when creating fields on inputs and types.
+ * This is used for public fields
+ *
+ * @param direction the direction
+ * @param fieldInfo the java property
+ * @return annotations for this field
+ */
+ public static Annotations getAnnotationsForPojo(Direction direction, FieldInfo fieldInfo) {
+ return getAnnotationsForPojo(direction, fieldInfo, null);
+ }
+
+ /**
+ * Get used when creating fields on inputs and types.
+ * Both has properties and methods and this needs to combined the two
+ *
+ * @param direction the direction
+ * @param fieldInfo the java property
+ * @param methodInfo the java method
+ * @return annotations for this field
+ */
+ public static Annotations getAnnotationsForPojo(Direction direction, FieldInfo fieldInfo, MethodInfo methodInfo) {
+ if (direction.equals(Direction.IN)) {
+ return getAnnotationsForInputField(fieldInfo, methodInfo);
+ } else {
+ return getAnnotationsForOutputField(fieldInfo, methodInfo);
+ }
+ }
+
+ public static Annotations getAnnotationsForInputCreator(MethodInfo method, short position, FieldInfo fieldInfo) {
+ Map annotationsForField = getAnnotationsForField(fieldInfo, null);
+
+ if (fieldInfo != null) {
+ annotationsForField.putAll(getTypeUseAnnotations(fieldInfo.type()));
+ }
+ annotationsForField.putAll(getAnnotationsForArgument(method, position).annotationsMap);
+
+ Map parentAnnotations = getParentAnnotations(fieldInfo, method);
+
+ return new Annotations(annotationsForField, parentAnnotations);
+ }
+
+ /**
+ * Get used when we create types and references to them
+ *
+ * Class level annotation for type creation.
+ *
+ * @param classInfo the java class
+ * @return annotation for this class
+ */
+ public static Annotations getAnnotationsForClass(ClassInfo classInfo) {
+
+ Map annotationMap = new HashMap<>();
+
+ for (AnnotationInstance annotationInstance : classInfo.declaredAnnotations()) {
+ DotName name = annotationInstance.name();
+ annotationMap.put(name, annotationInstance);
+ }
+
+ Map packageAnnotations = getPackageAnnotations(classInfo);
+ for (DotName dotName : packageAnnotations.keySet()) {
+ annotationMap.putIfAbsent(dotName, packageAnnotations.get(dotName));
+ }
+
+ return new Annotations(annotationMap, packageAnnotations);
+ }
+
+ /**
+ * Get used when creating arrays.
+ *
+ * This will contains the annotation on the collection field and method
+ *
+ * @param typeInCollection the field java type
+ * @param methodTypeInCollection the method java type
+ * @return the annotation for this array
+ */
+ public static Annotations getAnnotationsForArray(Type typeInCollection,
+ Type methodTypeInCollection) {
+ Map annotationMap = getAnnotationsForType(typeInCollection);
+ annotationMap.putAll(getAnnotationsForType(methodTypeInCollection));
+ return new Annotations(annotationMap);
+ }
+
+ //
+
+ /**
+ * Used when we are creating operation and arguments for these operations
+ *
+ * @param methodInfo the java method
+ * @param pos the argument position
+ * @return annotation for this argument
+ */
+ public static Annotations getAnnotationsForArgument(MethodInfo methodInfo, short pos) {
+ if (pos >= methodInfo.parametersCount()) {
+ throw new IndexOutOfBoundsException(
+ "Parameter at position " + pos + " not found on method " + methodInfo.name());
+ }
+
+ final Type parameterType = methodInfo.parameterType(pos);
+
+ Map annotationMap = getAnnotations(parameterType);
+
+ for (AnnotationInstance anno : methodInfo.annotations()) {
+ if (anno.target().kind().equals(Kind.METHOD_PARAMETER)) {
+ MethodParameterInfo methodParameter = anno.target().asMethodParameter();
+ short position = methodParameter.position();
+ if (position == pos) {
+ annotationMap.put(anno.name(), anno);
+ }
+ }
+ }
+
+ final Map parentAnnotations = getParentAnnotations(methodInfo.declaringClass());
+
+ return new Annotations(annotationMap, parentAnnotations);
+ }
+
+ public static boolean isJsonBAnnotation(AnnotationInstance instance) {
+ return instance.name().toString().startsWith(JAKARTA_JSONB) || instance.name().toString().startsWith(JAVAX_JSONB);
+ }
+
+ // ------- All static creators done, now the actual class --------
+
+ private Annotations(Map annotations) {
+ this(annotations, new HashMap<>());
+ }
+
+ /**
+ * Create the annotations, mapped by name
+ *
+ * @param annotations the annotation
+ */
+ private Annotations(Map annotations, Map parentAnnotations) {
+ this.annotationsMap = annotations;
+ this.parentAnnotations = parentAnnotations;
+ }
+
+ public Set getAnnotationNames() {
+ return annotationsMap.keySet();
+ }
+
+ public Annotations removeAnnotations(DotName... annotations) {
+ Map newAnnotationsMap = new HashMap<>(annotationsMap);
+ for (DotName annotation : annotations) {
+ newAnnotationsMap.remove(annotation);
+ }
+ return new Annotations(newAnnotationsMap, this.parentAnnotations);
+ }
+
+ /**
+ * Get a specific annotation
+ *
+ * @param annotation the annotation you want
+ * @return the annotation value or null
+ */
+ public AnnotationValue getAnnotationValue(DotName annotation) {
+ return this.annotationsMap.get(annotation).value();
+ }
+
+ /**
+ * Get a specific annotation
+ *
+ * @param annotation the annotation you want
+ * @return the annotation value or null
+ */
+ public AnnotationValue getAnnotationValue(DotName annotation, String name) {
+ return this.annotationsMap.get(annotation).value(name);
+ }
+
+ /**
+ * Check if there is an annotation and it has a valid value
+ *
+ * @param annotation the annotation we are checking
+ * @return true if valid value
+ */
+ public boolean containsKeyAndValidValue(DotName annotation) {
+ return this.annotationsMap.containsKey(annotation) && this.annotationsMap.get(annotation).value() != null;
+ }
+
+ /**
+ * Check if one of these annotations is present
+ *
+ * @param annotations the annotations to check
+ * @return true if it does
+ */
+ public boolean containsOneOfTheseAnnotations(DotName... annotations) {
+ for (DotName name : annotations) {
+ if (this.annotationsMap.containsKey(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean containsOneOfTheseInheritableAnnotations(DotName... annotations) {
+ for (DotName name : annotations) {
+ if (this.parentAnnotations.containsKey(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get on of these annotations
+ *
+ * @param annotations the annotations to check (in order)
+ * @return the annotation potentially or empty if not found
+ */
+ public Optional getOneOfTheseAnnotations(DotName... annotations) {
+ for (DotName name : annotations) {
+ if (this.annotationsMap.containsKey(name)) {
+ return Optional.of(this.annotationsMap.get(name));
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * This go through a list of annotations and find the first one that has a valid value.
+ * If it could not find one, it return empty
+ *
+ * @param annotations the annotations in order
+ * @return the valid annotation value or default value
+ */
+ public Optional getOneOfTheseAnnotationsValue(DotName... annotations) {
+ for (DotName dotName : annotations) {
+ if (dotName != null && containsKeyAndValidValue(dotName)) {
+ return getStringValue(dotName);
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * This go through a list of method annotations and find the first one that has a valid value.
+ * If it could not find one, it return the default value.
+ *
+ * @param annotations the annotations in order
+ * @return the valid annotation value or empty
+ */
+ public Optional getOneOfTheseMethodAnnotationsValue(DotName... annotations) {
+ for (DotName dotName : annotations) {
+ if (dotName != null && hasValidMethodAnnotation(dotName)) {
+ return getStringValue(dotName);
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * This go through a list of method parameter annotations and find the first one that has a valid value.
+ * If it could not find one, it return the default value.
+ *
+ * @param annotations the annotations in order
+ * @return the valid annotation value or empty
+ */
+ public Optional getOneOfTheseMethodParameterAnnotationsValue(DotName... annotations) {
+ for (DotName dotName : annotations) {
+ if (dotName != null && hasValidMethodParameterAnnotation(dotName)) {
+ return getStringValue(dotName);
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Get a stream of that annotation, maybe empty if not present, maybe a stream of one, or maybe several, if it's repeatable.
+ */
+ public Stream resolve(DotName name) {
+ var annotationInstance = annotationsMap.get(name);
+ if (annotationInstance == null) {
+ var repeatableType = ScanningContext.getIndex().getClassByName(name);
+ if (repeatableType.hasAnnotation(REPEATABLE)) {
+ DotName containerName = repeatableType.annotation(REPEATABLE).value().asClass().name();
+ AnnotationInstance containerAnnotation = annotationsMap.get(containerName);
+ if (containerAnnotation != null) {
+ return Stream.of(containerAnnotation.value().asNestedArray());
+ }
+ }
+ return Stream.of();
+ }
+ return Stream.of(annotationInstance);
+ }
+
+ @Override
+ public String toString() {
+ return annotationsMap.toString();
+ }
+
+ private boolean hasValidMethodAnnotation(DotName annotation) {
+ return containsKeyAndValidValue(annotation) && isMethodAnnotation(getAnnotation(annotation));
+ }
+
+ private boolean hasValidMethodParameterAnnotation(DotName annotation) {
+ return containsKeyAndValidValue(annotation) && isMethodParameterAnnotation(getAnnotation(annotation));
+ }
+
+ private AnnotationInstance getAnnotation(DotName key) {
+ return this.annotationsMap.get(key);
+ }
+
+ private Optional getStringValue(DotName annotation) {
+ AnnotationInstance annotationInstance = getAnnotation(annotation);
+ if (annotationInstance != null) {
+ return getStringValue(annotationInstance);
+ }
+ return Optional.empty();
+ }
+
+ private Optional getStringValue(AnnotationInstance annotationInstance) {
+ AnnotationValue value = annotationInstance.value();
+ if (value != null) {
+ return getStringValue(value);
+ }
+ return Optional.empty();
+ }
+
+ private Optional getStringValue(AnnotationValue annotationValue) {
+ String value;
+ if (annotationValue != null) {
+ value = annotationValue.asString();
+ if (value != null && !value.isEmpty()) {
+ return Optional.of(value);
+ }
+ }
+ return Optional.empty();
+ }
+
+ // Private static methods use by the static initializers
+
+ private static boolean isMethodAnnotation(AnnotationInstance instance) {
+ return instance.target().kind().equals(Kind.METHOD);
+ }
+
+ private static boolean isMethodParameterAnnotation(AnnotationInstance instance) {
+ return instance.target().kind().equals(Kind.METHOD_PARAMETER);
+ }
+
+ private static Annotations getAnnotationsForInputField(FieldInfo fieldInfo, MethodInfo methodInfo) {
+ Map annotationsForField = getAnnotationsForField(fieldInfo, methodInfo);
+
+ if (fieldInfo != null) {
+ annotationsForField.putAll(getTypeUseAnnotations(fieldInfo.type()));
+ }
+ if (methodInfo != null) {
+ List parameters = methodInfo.parameterTypes();
+ if (!parameters.isEmpty()) {
+ Type param = parameters.get(ZERO);
+ annotationsForField.putAll(getTypeUseAnnotations(param));
+ }
+ }
+
+ final Map parentAnnotations = getParentAnnotations(fieldInfo, methodInfo);
+
+ return new Annotations(annotationsForField, parentAnnotations);
+ }
+
+ private static Annotations getAnnotationsForOutputField(FieldInfo fieldInfo, MethodInfo methodInfo) {
+ Map annotationsForField = getAnnotationsForField(fieldInfo, methodInfo);
+
+ if (fieldInfo != null) {
+ annotationsForField.putAll(getTypeUseAnnotations(fieldInfo.type()));
+ }
+ if (methodInfo != null) {
+ Type returnType = methodInfo.returnType();
+ if (returnType != null) {
+ annotationsForField.putAll(getTypeUseAnnotations(methodInfo.returnType()));
+ }
+ }
+
+ Map parentAnnotations = getParentAnnotations(fieldInfo, methodInfo);
+
+ return new Annotations(annotationsForField, parentAnnotations);
+ }
+
+ private static Map getTypeUseAnnotations(Type type) {
+ if (type != null) {
+ return getAnnotationsWithFilter(type,
+ Annotations.DATE_FORMAT,
+ Annotations.NUMBER_FORMAT);
+ }
+ return emptyMap();
+ }
+
+ private static Map getAnnotations(Type type) {
+ Map annotationMap = new HashMap<>();
+
+ if (type.kind().equals(Type.Kind.PARAMETERIZED_TYPE)) {
+ Type typeInCollection = type.asParameterizedType().arguments().get(0);
+ annotationMap.putAll(getAnnotations(typeInCollection));
+ } else {
+ List annotations = type.annotations();
+ for (AnnotationInstance annotationInstance : annotations) {
+ annotationMap.put(annotationInstance.name(), annotationInstance);
+ }
+ }
+
+ return annotationMap;
+ }
+
+ private static Map getAnnotationsForType(Type type) {
+ Map annotationMap = new HashMap<>();
+ for (AnnotationInstance annotationInstance : type.annotations()) {
+ DotName name = annotationInstance.name();
+ annotationMap.put(name, annotationInstance);
+ }
+ return annotationMap;
+ }
+
+ private static Map getAnnotationsForField(FieldInfo fieldInfo, MethodInfo methodInfo) {
+ Map annotationMap = new HashMap<>();
+ if (fieldInfo != null)
+ annotationMap.putAll(listToMap(fieldInfo.annotations().stream()
+ .filter(ai -> ai.target().kind() == Kind.FIELD)
+ .collect(Collectors.toList())));
+ if (methodInfo != null)
+ annotationMap.putAll(listToMap(methodInfo.annotations().stream()
+ .filter(ai -> ai.target().kind() == Kind.METHOD || ai.target().kind() == Kind.METHOD_PARAMETER)
+ .collect(Collectors.toList())));
+ return annotationMap;
+ }
+
+ private static Map listToMap(List annotationInstances) {
+ Map annotationMap = new HashMap<>();
+
+ for (AnnotationInstance annotationInstance : annotationInstances) {
+ DotName name = annotationInstance.name();
+ annotationMap.put(name, annotationInstance);
+ }
+ return annotationMap;
+ }
+
+ private static Map getAnnotationsWithFilter(Type type, DotName... filter) {
+ Map annotationMap = new HashMap<>();
+
+ if (type.kind().equals(Type.Kind.PARAMETERIZED_TYPE)) {
+ Type typeInCollection = type.asParameterizedType().arguments().get(0);
+ annotationMap.putAll(getAnnotationsWithFilter(typeInCollection, filter));
+ } else {
+ List annotations = type.annotations();
+ for (AnnotationInstance annotationInstance : annotations) {
+ if (Arrays.asList(filter).contains(annotationInstance.name())) {
+ annotationMap.put(annotationInstance.name(), annotationInstance);
+ }
+ }
+ }
+
+ return annotationMap;
+ }
+
+ private static final short ZERO = 0;
+
+ public static final DotName REPEATABLE = DotName.createSimple("java.lang.annotation.Repeatable");
+
+ // SmallRye Common Annotations
+ public static final DotName BLOCKING = DotName.createSimple("io.smallrye.common.annotation.Blocking");
+ public static final DotName NON_BLOCKING = DotName.createSimple("io.smallrye.common.annotation.NonBlocking");
+
+ // SmallRye GraphQL Annotations (Experimental)
+ public static final DotName TO_SCALAR = DotName.createSimple("io.smallrye.graphql.api.ToScalar"); // TODO: Remove
+ public static final DotName ADAPT_TO_SCALAR = DotName.createSimple("io.smallrye.graphql.api.AdaptToScalar");
+ public static final DotName ADAPT_WITH = DotName.createSimple("io.smallrye.graphql.api.AdaptWith");
+ public static final DotName ERROR_CODE = DotName.createSimple("io.smallrye.graphql.api.ErrorCode");
+ public static final DotName DATAFETCHER = DotName.createSimple("io.smallrye.graphql.api.DataFetcher");
+ public static final DotName SUBCRIPTION = DotName.createSimple("io.smallrye.graphql.api.Subscription");
+ public static final DotName DIRECTIVE = DotName.createSimple("io.smallrye.graphql.api.Directive");
+ public static final DotName DEFAULT_NON_NULL = DotName.createSimple("io.smallrye.graphql.api.DefaultNonNull");
+ public static final DotName NULLABLE = DotName.createSimple("io.smallrye.graphql.api.Nullable");
+ public static final DotName KOTLIN_METADATA = DotName.createSimple("kotlin.Metadata");
+
+ // MicroProfile GraphQL Annotations
+ public static final DotName GRAPHQL_CLIENT_API = DotName
+ .createSimple("io.smallrye.graphql.client.typesafe.api.GraphQLClientApi");
+ public static final DotName QUERY = DotName.createSimple("org.eclipse.microprofile.graphql.Query");
+ public static final DotName MUTATION = DotName.createSimple("org.eclipse.microprofile.graphql.Mutation");
+ public static final DotName INPUT = DotName.createSimple("org.eclipse.microprofile.graphql.Input");
+ public static final DotName TYPE = DotName.createSimple("org.eclipse.microprofile.graphql.Type");
+ public static final DotName INTERFACE = DotName.createSimple("org.eclipse.microprofile.graphql.Interface");
+ public static final DotName UNION = DotName.createSimple("io.smallrye.graphql.api.Union");
+
+ public static final DotName MULTIPLE = DotName.createSimple("io.smallrye.graphql.client.typesafe.api.Multiple");
+
+ public static final DotName NESTED_PARAMETER = DotName
+ .createSimple("io.smallrye.graphql.client.typesafe.api.NestedParameter");
+
+ public static final DotName ENUM = DotName.createSimple("org.eclipse.microprofile.graphql.Enum");
+ public static final DotName ID = DotName.createSimple("org.eclipse.microprofile.graphql.Id");
+ public static final DotName DESCRIPTION = DotName.createSimple("org.eclipse.microprofile.graphql.Description");
+ public static final DotName DATE_FORMAT = DotName.createSimple("org.eclipse.microprofile.graphql.DateFormat");
+ public static final DotName NUMBER_FORMAT = DotName.createSimple("org.eclipse.microprofile.graphql.NumberFormat");
+ public static final DotName DEFAULT_VALUE = DotName.createSimple("org.eclipse.microprofile.graphql.DefaultValue");
+ public static final DotName IGNORE = DotName.createSimple("org.eclipse.microprofile.graphql.Ignore");
+ public static final DotName NON_NULL = DotName.createSimple("org.eclipse.microprofile.graphql.NonNull");
+ public static final DotName NAME = DotName.createSimple("org.eclipse.microprofile.graphql.Name");
+ public static final DotName SOURCE = DotName.createSimple("org.eclipse.microprofile.graphql.Source");
+
+ // Json-B Annotations
+ public static final String JAVAX_JSONB = "javax.json.bind.annotation.";
+ public static final DotName JAVAX_JSONB_DATE_FORMAT = DotName.createSimple(JAVAX_JSONB + "JsonbDateFormat");
+ public static final DotName JAVAX_JSONB_NUMBER_FORMAT = DotName.createSimple(JAVAX_JSONB + "JsonbNumberFormat");
+ public static final DotName JAVAX_JSONB_PROPERTY = DotName.createSimple(JAVAX_JSONB + "JsonbProperty");
+ public static final DotName JAVAX_JSONB_TRANSIENT = DotName.createSimple(JAVAX_JSONB + "JsonbTransient");
+ public static final DotName JAVAX_JSONB_CREATOR = DotName.createSimple(JAVAX_JSONB + "JsonbCreator");
+ public static final DotName JAVAX_JSONB_TYPE_ADAPTER = DotName.createSimple(JAVAX_JSONB + "JsonbTypeAdapter");
+
+ public static final String JAKARTA_JSONB = "jakarta.json.bind.annotation.";
+ public static final DotName JAKARTA_JSONB_DATE_FORMAT = DotName.createSimple(JAKARTA_JSONB + "JsonbDateFormat");
+ public static final DotName JAKARTA_JSONB_NUMBER_FORMAT = DotName.createSimple(JAKARTA_JSONB + "JsonbNumberFormat");
+ public static final DotName JAKARTA_JSONB_PROPERTY = DotName.createSimple(JAKARTA_JSONB + "JsonbProperty");
+ public static final DotName JAKARTA_JSONB_TRANSIENT = DotName.createSimple(JAKARTA_JSONB + "JsonbTransient");
+ public static final DotName JAKARTA_JSONB_CREATOR = DotName.createSimple(JAKARTA_JSONB + "JsonbCreator");
+ public static final DotName JAKARTA_JSONB_TYPE_ADAPTER = DotName.createSimple(JAKARTA_JSONB + "JsonbTypeAdapter");
+
+ // Jackson Annotations
+ public static final DotName JACKSON_IGNORE = DotName.createSimple("com.fasterxml.jackson.annotation.JsonIgnore");
+ public static final DotName JACKSON_PROPERTY = DotName.createSimple("com.fasterxml.jackson.annotation.JsonProperty");
+ public static final DotName JACKSON_CREATOR = DotName.createSimple("com.fasterxml.jackson.annotation.JsonCreator");
+ public static final DotName JACKSON_FORMAT = DotName.createSimple("com.fasterxml.jackson.annotation.JsonFormat");
+
+ // Bean Validation Annotations (SmallRye extra, not part of the spec)
+ public static final DotName JAVAX_BEAN_VALIDATION_NOT_NULL = DotName.createSimple("javax.validation.constraints.NotNull");
+ public static final DotName JAVAX_BEAN_VALIDATION_NOT_EMPTY = DotName.createSimple("javax.validation.constraints.NotEmpty");
+ public static final DotName JAVAX_BEAN_VALIDATION_NOT_BLANK = DotName.createSimple("javax.validation.constraints.NotBlank");
+
+ public static final DotName JAKARTA_BEAN_VALIDATION_NOT_NULL = DotName
+ .createSimple("jakarta.validation.constraints.NotNull");
+ public static final DotName JAKARTA_BEAN_VALIDATION_NOT_EMPTY = DotName
+ .createSimple("jakarta.validation.constraints.NotEmpty");
+ public static final DotName JAKARTA_BEAN_VALIDATION_NOT_BLANK = DotName
+ .createSimple("jakarta.validation.constraints.NotBlank");
+
+ public static final DotName JAKARTA_NON_NULL = DotName.createSimple("jakarta.validation.constraints.NotNull");
+
+ //Kotlin NotNull
+ public static final DotName KOTLIN_NOT_NULL = DotName.createSimple("org.jetbrains.annotations.NotNull");
+
+}
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Classes.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Classes.java
new file mode 100644
index 000000000..f4c65bce9
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Classes.java
@@ -0,0 +1,428 @@
+package io.smallrye.graphql.client.model;
+
+import static io.smallrye.graphql.client.model.ScanningContext.getIndex;
+
+import java.io.Serializable;
+import java.lang.reflect.Modifier;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.Period;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Deque;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import java.util.Queue;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.Stack;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.Vector;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.Type;
+
+import io.smallrye.graphql.client.typesafe.api.ErrorOr;
+import io.smallrye.graphql.client.typesafe.api.TypesafeResponse;
+import io.smallrye.mutiny.Multi;
+import io.smallrye.mutiny.Uni;
+
+/**
+ * Class helper
+ *
+ * @author Phillip Kruger (phillip.kruger@redhat.com)
+ */
+public class Classes {
+
+ private Classes() {
+ }
+
+ public static boolean isWrapper(Type type) {
+ return isParameterized(type) || isArray(type);
+ }
+
+ /**
+ * Check if this is a Parameterized type
+ *
+ * @param type
+ * @return
+ */
+ public static boolean isParameterized(Type type) {
+ return type.kind().equals(Type.Kind.PARAMETERIZED_TYPE);
+ }
+
+ public static boolean isTypeVariable(Type type) {
+ return type.kind().equals(Type.Kind.TYPE_VARIABLE);
+ }
+
+ /**
+ * Check if a certain type is Optional
+ *
+ * @param type the type
+ * @return true if it is
+ */
+ public static boolean isOptional(Type type) {
+ return isParameterized(type) && type.name().equals(OPTIONAL) // Normal Optional
+ || type.name().equals(LONG_OPTIONAL)
+ || type.name().equals(DOUBLE_OPTIONAL)
+ || type.name().equals(INTEGER_OPTIONAL);
+ }
+
+ public static boolean isOptionalInt(Type type) {
+ return type.name().equals(INTEGER_OPTIONAL);
+ }
+
+ public static boolean isOptionalLong(Type type) {
+ return type.name().equals(LONG_OPTIONAL);
+ }
+
+ public static boolean isOptionalDouble(Type type) {
+ return type.name().equals(DOUBLE_OPTIONAL);
+ }
+
+ /**
+ * Check if a certain class is an interface
+ *
+ * @param classInfo the class to check
+ * @return true if it is
+ */
+ public static boolean isInterface(ClassInfo classInfo) {
+ if (classInfo == null)
+ return false;
+ return Modifier.isInterface(classInfo.flags());
+ }
+
+ public static boolean isEnum(Type type) {
+ if (Classes.isClass(type)) {
+ ClassInfo clazz = getIndex().getClassByName(type.asClassType().name());
+ return clazz != null && clazz.isEnum();
+ }
+ return false;
+ }
+
+ /**
+ * Check if this type is a Number (or collection of numbers)
+ *
+ * @param type the type to check
+ * @return true if it is
+ */
+ public static boolean isNumberLikeTypeOrContainedIn(Type type) {
+ return isTypeOrContainedIn(type, BYTE, BYTE_PRIMATIVE, SHORT, SHORT_PRIMATIVE, INTEGER, INTEGER_PRIMATIVE,
+ BIG_INTEGER, DOUBLE, DOUBLE_PRIMATIVE, BIG_DECIMAL, LONG, LONG_PRIMATIVE, FLOAT, FLOAT_PRIMATIVE,
+ INTEGER_OPTIONAL, DOUBLE_OPTIONAL, LONG_OPTIONAL, INTEGER_ATOMIC, LONG_ATOMIC);
+ }
+
+ /**
+ * Check if this type is a Date (or collection of numbers)
+ *
+ * @param type the type to check
+ * @return true if it is
+ */
+ public static boolean isDateLikeTypeOrContainedIn(Type type) {
+ return isTypeOrContainedIn(type, LOCALDATE, LOCALTIME, LOCALDATETIME, ZONEDDATETIME, OFFSETDATETIME, OFFSETTIME,
+ UTIL_DATE, SQL_DATE, SQL_TIMESTAMP, SQL_TIME, INSTANT, CALENDAR, GREGORIAN_CALENDAR);
+ }
+
+ private static boolean isTypeOrContainedIn(Type type, DotName... valid) {
+ switch (type.kind()) {
+ case PARAMETERIZED_TYPE:
+ // Container
+ Type typeInCollection = type.asParameterizedType().arguments().get(0);
+ return isTypeOrContainedIn(typeInCollection, valid);
+ case ARRAY:
+ // Array
+ Type typeInArray = type.asArrayType().component();
+ return isTypeOrContainedIn(typeInArray, valid);
+ default:
+ for (DotName dotName : valid) {
+ if (type.name().toString().equals(dotName.toString())) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ public static boolean isAsync(Type type) {
+ return isUni(type)
+ || isMulti(type);
+ }
+
+ public static boolean isUni(Type type) {
+ return type.name().equals(UNI);
+ }
+
+ public static boolean isMulti(Type type) {
+ return type.name().equals(MULTI);
+ }
+
+ public static boolean isPrimitive(Type type) {
+ return type.kind().equals(Type.Kind.PRIMITIVE);
+ }
+
+ public static boolean isVoid(Type type) {
+ return type.kind().equals(Type.Kind.VOID);
+ }
+
+ public static boolean isClass(Type type) {
+ return type.kind().equals(Type.Kind.CLASS);
+ }
+
+ /**
+ * Return true if type is java array, or it is Collection type which is handled as GraphQL array
+ *
+ * @param type to check
+ * @return if this is a collection or array
+ */
+ public static boolean isCollectionOrArray(Type type) {
+ return type.kind().equals(Type.Kind.ARRAY) || isCollection(type);
+ }
+
+ /**
+ * Return true if this is an array
+ *
+ * @param type
+ * @return
+ */
+ public static boolean isArray(Type type) {
+ return type.kind().equals(Type.Kind.ARRAY);
+ }
+
+ /**
+ * Return true if type is java Collection type which is handled as GraphQL array
+ *
+ * @param type to check
+ * @return if this is a collection
+ */
+ public static boolean isCollection(Type type) {
+ if (isParameterized(type)) {
+
+ ClassInfo clazz = ScanningContext.getIndex().getClassByName(type.name());
+ if (clazz == null) {
+ // use classloader instead of jandex to handle basic java classes/interfaces
+ try {
+ Class> clazzLoaded = Classes.class.getClassLoader().loadClass(type.name().toString());
+ return Collection.class.isAssignableFrom(clazzLoaded);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException("Info not found in Jandex index nor classpath for class name:" + type.name());
+ }
+ }
+ if (KNOWN_COLLECTIONS.contains(clazz.name())) {
+ return true;
+ }
+
+ // we have to go recursively over all super-interfaces as Jandex provides only direct interfaces
+ // implemented in the class itself
+ for (Type intf : clazz.interfaceTypes()) {
+ if (isCollection(intf)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public static boolean isMap(Type type) {
+ if (isParameterized(type)) {
+
+ ClassInfo clazz = ScanningContext.getIndex().getClassByName(type.name());
+ if (clazz == null) {
+ // use classloader instead of jandex to handle basic java classes/interfaces
+ try {
+ Class> clazzLoaded = Classes.class.getClassLoader().loadClass(type.name().toString());
+ return Map.class.isAssignableFrom(clazzLoaded);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException("Info not found in Jandex index nor classpath for class name:" + type.name());
+ }
+ }
+ if (KNOWN_MAPS.contains(clazz.name())) {
+ return true;
+ }
+
+ // we have to go recursively over all super-interfaces as Jandex provides only direct interfaces
+ // implemented in the class itself
+ for (Type intf : clazz.interfaceTypes()) {
+ if (isMap(intf)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return true if given type is parametrized type unwrapped/handled by the runtime before the serialization
+ * (Optional<>, CompletableFutur<>, CompletionStage<> etc)
+ *
+ * @param type to be checked
+ * @return true if type is unwrapped by the runtime
+ */
+ @Deprecated
+ public static boolean isUnwrappedType(Type type) {
+ return isParameterized(type) && (isOptional(type) // Optional<>
+ || isAsync(type)) // CompletableFuture or CompletionStage
+ ;
+ }
+
+ private static final DotName UNI = DotName.createSimple(Uni.class.getName());
+ private static final DotName MULTI = DotName.createSimple(Multi.class.getName());
+
+ public static final DotName SERIALIZABLE = DotName.createSimple(Serializable.class.getName());
+ public static final DotName OBJECT = DotName.createSimple(Object.class.getName());
+
+ public static final DotName COLLECTION = DotName.createSimple(Collection.class.getName());
+ public static final DotName LIST = DotName.createSimple(List.class.getName());
+ public static final DotName LINKED_LIST = DotName.createSimple(LinkedList.class.getName());
+ public static final DotName VECTOR = DotName.createSimple(Vector.class.getName());
+ public static final DotName ARRAY_LIST = DotName.createSimple(ArrayList.class.getName());
+ public static final DotName STACK = DotName.createSimple(Stack.class.getName());
+
+ public static final DotName SET = DotName.createSimple(Set.class.getName());
+ public static final DotName HASH_SET = DotName.createSimple(HashSet.class.getName());
+ public static final DotName SORTED_SET = DotName.createSimple(SortedSet.class.getName());
+ public static final DotName TREE_SET = DotName.createSimple(TreeSet.class.getName());
+
+ public static final DotName QUEUE = DotName.createSimple(Queue.class.getName());
+ public static final DotName DEQUE = DotName.createSimple(Deque.class.getName());
+
+ public static final DotName MAP = DotName.createSimple(Map.class.getName());
+ public static final DotName HASH_MAP = DotName.createSimple(HashMap.class.getName());
+ public static final DotName TREE_MAP = DotName.createSimple(TreeMap.class.getName());
+ public static final DotName HASHTABLE = DotName.createSimple(Hashtable.class.getName());
+ public static final DotName SORTED_MAP = DotName.createSimple(SortedMap.class.getName());
+
+ public static final DotName ENTRY = DotName.createSimple("io.smallrye.graphql.api.Entry");
+
+ public static final DotName OPTIONAL = DotName.createSimple(Optional.class.getName());
+
+ public static final DotName ENUM = DotName.createSimple(Enum.class.getName());
+
+ public static final DotName RECORD = DotName.createSimple("java.lang.Record");
+
+ public static final DotName TYPESAFE_RESPONSE = DotName.createSimple(TypesafeResponse.class.getName());
+ public static final DotName ERROR_OR = DotName.createSimple(ErrorOr.class.getName());
+
+ public static final DotName LOCALDATE = DotName.createSimple(LocalDate.class.getName());
+ public static final DotName LOCALDATETIME = DotName.createSimple(LocalDateTime.class.getName());
+ public static final DotName LOCALTIME = DotName.createSimple(LocalTime.class.getName());
+ public static final DotName ZONEDDATETIME = DotName.createSimple(ZonedDateTime.class.getName());
+ public static final DotName OFFSETDATETIME = DotName.createSimple(OffsetDateTime.class.getName());
+ public static final DotName OFFSETTIME = DotName.createSimple(OffsetTime.class.getName());
+ public static final DotName INSTANT = DotName.createSimple(Instant.class.getName());
+
+ public static final DotName CALENDAR = DotName.createSimple(Calendar.class.getName());
+ public static final DotName GREGORIAN_CALENDAR = DotName.createSimple(GregorianCalendar.class.getName());
+
+ public static final DotName PERIOD = DotName.createSimple(Period.class.getName());
+ public static final DotName DURATION = DotName.createSimple(Duration.class.getName());
+
+ public static final DotName UTIL_DATE = DotName.createSimple(Date.class.getName());
+ public static final DotName SQL_DATE = DotName.createSimple(java.sql.Date.class.getName());
+ public static final DotName SQL_TIMESTAMP = DotName.createSimple(java.sql.Timestamp.class.getName());
+ public static final DotName SQL_TIME = DotName.createSimple(java.sql.Time.class.getName());
+
+ private static final DotName BYTE = DotName.createSimple(Byte.class.getName());
+ private static final DotName BYTE_PRIMATIVE = DotName.createSimple(byte.class.getName());
+
+ private static final DotName SHORT = DotName.createSimple(Short.class.getName());
+ private static final DotName SHORT_PRIMATIVE = DotName.createSimple(short.class.getName());
+
+ private static final DotName INTEGER = DotName.createSimple(Integer.class.getName());
+ private static final DotName INTEGER_PRIMATIVE = DotName.createSimple(int.class.getName());
+ private static final DotName INTEGER_OPTIONAL = DotName.createSimple(OptionalInt.class.getName());
+ private static final DotName INTEGER_ATOMIC = DotName.createSimple(AtomicInteger.class.getName());
+
+ private static final DotName BIG_INTEGER = DotName.createSimple(BigInteger.class.getName());
+
+ private static final DotName DOUBLE = DotName.createSimple(Double.class.getName());
+ private static final DotName DOUBLE_PRIMATIVE = DotName.createSimple(double.class.getName());
+ private static final DotName DOUBLE_OPTIONAL = DotName.createSimple(OptionalDouble.class.getName());
+
+ private static final DotName BIG_DECIMAL = DotName.createSimple(BigDecimal.class.getName());
+
+ private static final DotName LONG = DotName.createSimple(Long.class.getName());
+ private static final DotName LONG_PRIMATIVE = DotName.createSimple(long.class.getName());
+ private static final DotName LONG_OPTIONAL = DotName.createSimple(OptionalLong.class.getName());
+ private static final DotName LONG_ATOMIC = DotName.createSimple(AtomicLong.class.getName());
+
+ private static final DotName FLOAT = DotName.createSimple(Float.class.getName());
+ private static final DotName FLOAT_PRIMATIVE = DotName.createSimple(float.class.getName());
+
+ // Adapters
+ public static final DotName JAVAX_JSONB_ADAPTER = DotName.createSimple("javax.json.bind.adapter.JsonbAdapter");
+ public static final DotName JAKARTA_JSONB_ADAPTER = DotName.createSimple("jakarta.json.bind.adapter.JsonbAdapter");
+ public static final DotName ADAPTER = DotName.createSimple("io.smallrye.graphql.api.Adapter");
+
+ // Validation
+ public static final DotName JAVAX_VALIDATION_ANNOTATION_EMAIL = DotName.createSimple("javax.validation.constraints.Email");
+ public static final DotName JAVAX_VALIDATION_ANNOTATION_MAX = DotName.createSimple("javax.validation.constraints.Max");
+ public static final DotName JAVAX_VALIDATION_ANNOTATION_DECIMAL_MAX = DotName
+ .createSimple("javax.validation.constraints.DecimalMax");
+ public static final DotName JAVAX_VALIDATION_ANNOTATION_MIN = DotName.createSimple("javax.validation.constraints.Min");
+ public static final DotName JAVAX_VALIDATION_ANNOTATION_DECIMAL_MIN = DotName
+ .createSimple("javax.validation.constraints.DecimalMin");
+ public static final DotName JAVAX_VALIDATION_ANNOTATION_PATTERN = DotName
+ .createSimple("javax.validation.constraints.Pattern");
+ public static final DotName JAVAX_VALIDATION_ANNOTATION_SIZE = DotName.createSimple("javax.validation.constraints.Size");
+
+ public static final DotName JAKARTA_VALIDATION_ANNOTATION_EMAIL = DotName
+ .createSimple("jakarta.validation.constraints.Email");
+ public static final DotName JAKARTA_VALIDATION_ANNOTATION_MAX = DotName.createSimple("jakarta.validation.constraints.Max");
+ public static final DotName JAKARTA_VALIDATION_ANNOTATION_DECIMAL_MAX = DotName
+ .createSimple("jakarta.validation.constraints.DecimalMax");
+ public static final DotName JAKARTA_VALIDATION_ANNOTATION_MIN = DotName.createSimple("jakarta.validation.constraints.Min");
+ public static final DotName JAKARTA_VALIDATION_ANNOTATION_DECIMAL_MIN = DotName
+ .createSimple("jakarta.validation.constraints.DecimalMin");
+ public static final DotName JAKARTA_VALIDATION_ANNOTATION_PATTERN = DotName
+ .createSimple("jakarta.validation.constraints.Pattern");
+ public static final DotName JAKARTA_VALIDATION_ANNOTATION_SIZE = DotName
+ .createSimple("jakarta.validation.constraints.Size");
+
+ private static final List KNOWN_COLLECTIONS = new ArrayList<>();
+ private static final List KNOWN_MAPS = new ArrayList<>();
+ static {
+ KNOWN_COLLECTIONS.add(COLLECTION);
+ KNOWN_COLLECTIONS.add(LIST);
+ KNOWN_COLLECTIONS.add(LINKED_LIST);
+ KNOWN_COLLECTIONS.add(VECTOR);
+ KNOWN_COLLECTIONS.add(ARRAY_LIST);
+ KNOWN_COLLECTIONS.add(STACK);
+ KNOWN_COLLECTIONS.add(SET);
+ KNOWN_COLLECTIONS.add(HASH_SET);
+ KNOWN_COLLECTIONS.add(SORTED_SET);
+ KNOWN_COLLECTIONS.add(TREE_SET);
+ KNOWN_COLLECTIONS.add(QUEUE);
+ KNOWN_COLLECTIONS.add(DEQUE);
+
+ KNOWN_MAPS.add(MAP);
+ KNOWN_MAPS.add(HASH_MAP);
+ KNOWN_MAPS.add(TREE_MAP);
+ KNOWN_MAPS.add(HASHTABLE);
+ KNOWN_MAPS.add(SORTED_MAP);
+ }
+}
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ClientModelBuilder.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ClientModelBuilder.java
new file mode 100644
index 000000000..54c4f96f1
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ClientModelBuilder.java
@@ -0,0 +1,82 @@
+package io.smallrye.graphql.client.model;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.IndexView;
+import org.jboss.jandex.MethodInfo;
+import org.jboss.logging.Logger;
+
+import io.smallrye.graphql.client.model.helper.TypeAutoNameStrategy;
+
+public class ClientModelBuilder {
+ private static final Logger LOG = Logger.getLogger(ClientModelBuilder.class.getName());
+
+ /**
+ * This builds the Model from Jandex
+ *
+ * @param index the Jandex index
+ * @return the Schema
+ */
+ public static ClientModel build(IndexView index) {
+ return build(index, TypeAutoNameStrategy.Default);
+ }
+
+ /**
+ * This builds the Schema from Jandex
+ *
+ * @param index the Jandex index
+ * @param autoNameStrategy the naming strategy
+ * @return the Schema
+ */
+ public static ClientModel build(IndexView index, TypeAutoNameStrategy autoNameStrategy) {
+ ScanningContext.register(index);
+ return new ClientModelBuilder(autoNameStrategy).generateClientModel();
+ }
+
+ private ClientModelBuilder(TypeAutoNameStrategy autoNameStrategy) {
+ }
+
+ private ClientModel generateClientModel() {
+ ClientModel clientModel = new ClientModel();
+ Map> clientModelMap = new HashMap<>();
+
+ // Get all the @GraphQLClientApi annotations
+ Collection graphQLApiAnnotations = ScanningContext.getIndex()
+ .getAnnotations(Annotations.GRAPHQL_CLIENT_API);
+
+ // TODO: DIRECTIVES and HEADERS (static, dynamic)
+ graphQLApiAnnotations.forEach(graphQLApiAnnotation -> {
+ Map operationMap = new HashMap<>();
+ ClassInfo apiClass = graphQLApiAnnotation.target().asClass();
+ List methods = getAllMethodsIncludingFromSuperClasses(apiClass);
+ methods.stream().forEach(method -> operationMap.put(method.genericSignature(), new QueryBuilder(method).build()));
+ clientModelMap.put(apiClass.name().toString(), operationMap);
+ });
+ clientModel.setOperationMap(clientModelMap);
+ return clientModel;
+ }
+
+ //
+ private List getAllMethodsIncludingFromSuperClasses(ClassInfo classInfo) {
+ ClassInfo current = classInfo;
+ IndexView index = ScanningContext.getIndex();
+ List methods = new ArrayList<>();
+ while (current != null) {
+ current.methods().stream().filter(methodInfo -> !methodInfo.isSynthetic()).forEach(methods::add);
+ DotName superName = classInfo.superName();
+ if (superName != null) {
+ current = index.getClassByName(current.superName());
+ } else {
+ current = null;
+ }
+ }
+ return methods;
+ }
+}
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/QueryBuilder.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/QueryBuilder.java
new file mode 100644
index 000000000..5cf0ffa90
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/QueryBuilder.java
@@ -0,0 +1,57 @@
+package io.smallrye.graphql.client.model;
+
+import static io.smallrye.graphql.client.model.helper.MethodModel.of;
+import static java.util.stream.Collectors.joining;
+
+import org.jboss.jandex.MethodInfo;
+
+import io.smallrye.graphql.client.model.helper.DirectiveInstance;
+import io.smallrye.graphql.client.model.helper.MethodModel;
+
+public class QueryBuilder {
+ private final MethodModel method;
+
+ public QueryBuilder(MethodInfo method) {
+ this.method = of(method);
+ }
+
+ public String build() {
+ StringBuilder request = new StringBuilder();
+ switch (method.getOperationType()) {
+ case QUERY:
+ request.append("query ");
+ break;
+ case MUTATION:
+ request.append("mutation ");
+ break;
+ case SUBSCRIPTION:
+ request.append("subscription ");
+ break;
+ }
+ request.append(method.getName()); // operationName
+ if (method.hasValueParameters()) {
+ request.append(method.valueParameters().stream().map(method::declare).collect(joining(", ", "(", ")")));
+ }
+
+ if (method.isSingle()) {
+ request.append(" { ");
+ request.append(method.getName());
+ if (method.hasRootParameters()) {
+ request.append(method.rootParameters().stream()
+ .map(method::bind)
+ .collect(joining(", ", "(", ")")));
+ }
+
+ if (method.hasDirectives()) {
+ request.append(method.getDirectives().stream().map(DirectiveInstance::buildDirective).collect(joining()));
+ }
+ }
+
+ request.append(method.fields(method.getReturnType()));
+
+ if (method.isSingle())
+ request.append(" }");
+
+ return request.toString();
+ }
+}
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Scalars.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Scalars.java
new file mode 100644
index 000000000..a7075190c
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Scalars.java
@@ -0,0 +1,144 @@
+package io.smallrye.graphql.client.model;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URI;
+import java.net.URL;
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.Period;
+import java.time.ZonedDateTime;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Here we keep all the scalars we know about
+ *
+ * @author Phillip Kruger (phillip.kruger@redhat.com)
+ */
+public class Scalars {
+ private static final Map scalarMap = new HashMap<>();
+ private static final String STRING = "String";
+ private static final String BOOLEAN = "Boolean";
+ private static final String INTEGER = "Int";
+ private static final String FLOAT = "Float";
+ private static final String BIGINTEGER = "BigInteger";
+ private static final String BIGDECIMAL = "BigDecimal";
+ private static final String DATE = "Date";
+ private static final String TIME = "Time";
+ private static final String DATETIME = "DateTime";
+ private static final String ID = "ID";
+ private static final String PERIOD = "Period";
+ private static final String DURATION = "Duration";
+ private static final String VOID = "Void";
+
+ private Scalars() {
+ }
+
+ public static boolean isScalar(String className) {
+ return scalarMap.containsKey(className);
+ }
+
+ public static String getScalar(String identifier) {
+ return scalarMap.get(identifier);
+ }
+
+ static {
+ // The main java type should go first.
+
+ // Strings
+ populateScalar(String.class.getName(), STRING);
+ populateScalar(char.class.getName(), STRING);
+ populateScalar(Character.class.getName(), STRING);
+ populateScalar(UUID.class.getName(), STRING);
+ populateScalar(URL.class.getName(), STRING);
+ populateScalar(URI.class.getName(), STRING);
+ populateScalar("org.bson.types.ObjectId", STRING);
+ populateScalar("javax.json.JsonObject", STRING);
+ populateScalar("javax.json.JsonArray", STRING);
+ populateScalar("jakarta.json.JsonObject", STRING);
+ populateScalar("jakarta.json.JsonArray", STRING);
+
+ // Boolean
+ populateScalar(Boolean.class.getName(), BOOLEAN);
+ populateScalar(boolean.class.getName(), BOOLEAN);
+ populateScalar(AtomicBoolean.class.getName(), BOOLEAN);
+
+ // Integer
+ populateScalar(Integer.class.getName(), INTEGER);
+ populateScalar(int.class.getName(), INTEGER);
+ populateScalar(Short.class.getName(), INTEGER);
+ populateScalar(short.class.getName(), INTEGER);
+ populateScalar(Byte.class.getName(), INTEGER);
+ populateScalar(byte.class.getName(), INTEGER);
+ populateScalar(OptionalInt.class.getName(), INTEGER);
+ populateScalar(AtomicInteger.class.getName(), INTEGER);
+
+ // Float
+ populateScalar(Float.class.getName(), FLOAT);
+ populateScalar(float.class.getName(), FLOAT);
+ populateScalar(Double.class.getName(), FLOAT);
+ populateScalar(double.class.getName(), FLOAT);
+ populateScalar(OptionalDouble.class.getName(), FLOAT);
+
+ // BigInteger
+ populateScalar(BigInteger.class.getName(), BIGINTEGER);
+ populateScalar(Long.class.getName(), BIGINTEGER);
+ populateScalar(long.class.getName(), BIGINTEGER);
+ populateScalar(OptionalLong.class.getName(), BIGINTEGER);
+ populateScalar(AtomicLong.class.getName(), BIGINTEGER);
+
+ // BigDecimal
+ populateScalar(BigDecimal.class.getName(), BIGDECIMAL);
+
+ // Date
+ populateScalar(LocalDate.class.getName(), DATE);
+ populateScalar(Date.class.getName(), DATE);
+
+ // Time
+ populateScalar(LocalTime.class.getName(), TIME);
+ populateScalar(Time.class.getName(), TIME);
+ populateScalar(OffsetTime.class.getName(), TIME);
+
+ // DateTime
+ populateScalar(LocalDateTime.class.getName(), DATETIME);
+ populateScalar(java.util.Date.class.getName(), DATETIME);
+ populateScalar(Timestamp.class.getName(), DATETIME);
+ populateScalar(ZonedDateTime.class.getName(), DATETIME);
+ populateScalar(OffsetDateTime.class.getName(), DATETIME);
+ populateScalar(Instant.class.getName(), DATETIME);
+ populateScalar(Calendar.class.getName(), DATETIME);
+ populateScalar(GregorianCalendar.class.getName(), DATETIME);
+
+ // Duration
+ populateScalar(Duration.class.getName(), DURATION);
+ // Period
+ populateScalar(Period.class.getName(), PERIOD);
+
+ // Void
+ populateScalar(Void.class.getName(), VOID);
+ populateScalar(void.class.getName(), VOID);
+ }
+
+ private static void populateScalar(String className, String scalarName) {
+ scalarMap.put(className, scalarName);
+ }
+
+}
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ScanningContext.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ScanningContext.java
new file mode 100644
index 000000000..ce718bf0d
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ScanningContext.java
@@ -0,0 +1,33 @@
+package io.smallrye.graphql.client.model;
+
+import org.jboss.jandex.IndexView;
+
+/**
+ * A simple registry to hold the current scanning info
+ *
+ * At this point we only keep the index in the context
+ *
+ * @author Phillip Kruger (phillip.kruger@redhat.com)
+ */
+public class ScanningContext {
+ private static final ThreadLocal current = new ThreadLocal<>();
+
+ public static void register(IndexView index) {
+ ScanningContext registry = new ScanningContext(index);
+ current.set(registry);
+ }
+
+ public static IndexView getIndex() {
+ return current.get().index;
+ }
+
+ public static void remove() {
+ current.remove();
+ }
+
+ private final IndexView index;
+
+ private ScanningContext(final IndexView index) {
+ this.index = index;
+ }
+}
\ No newline at end of file
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/Direction.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/Direction.java
new file mode 100644
index 000000000..c2607b341
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/Direction.java
@@ -0,0 +1,11 @@
+package io.smallrye.graphql.client.model.helper;
+
+/**
+ * Indicating the direction
+ *
+ * @author Phillip Kruger (phillip.kruger@redhat.com)
+ */
+public enum Direction {
+ IN,
+ OUT
+}
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/DirectiveHelper.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/DirectiveHelper.java
new file mode 100644
index 000000000..427a44292
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/DirectiveHelper.java
@@ -0,0 +1,53 @@
+package io.smallrye.graphql.client.model.helper;
+
+import static io.smallrye.graphql.client.model.Annotations.DIRECTIVE;
+import static io.smallrye.graphql.client.model.Annotations.REPEATABLE;
+import static io.smallrye.graphql.client.model.ScanningContext.getIndex;
+import static java.util.Arrays.stream;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationTarget;
+import org.jboss.jandex.ClassInfo;
+
+public class DirectiveHelper {
+
+ public static Stream resolveDirectives(Stream annotationInstances,
+ String directiveLocation,
+ AnnotationTarget.Kind targetKind) {
+ return resolveDirectives(annotationInstances
+ .filter(annotation -> annotation.target().kind() == targetKind), directiveLocation);
+ }
+
+ public static Stream resolveDirectives(Stream annotationInstances,
+ String directiveLocation) {
+ return annotationInstances
+ .flatMap(annotationInstance -> {
+ ClassInfo scannedAnnotation = getIndex().getClassByName(annotationInstance.name());
+ if (scannedAnnotation != null) {
+ if (scannedAnnotation.hasAnnotation(DIRECTIVE)) {
+ return Stream.of(annotationInstance);
+ }
+ Optional repeatableAnnotation = getIndex()
+ .getAnnotations(REPEATABLE).stream()
+ .filter(annotation -> annotation.target().hasAnnotation(DIRECTIVE)
+ && annotation.value().asClass().name().equals(annotationInstance.name()))
+ .findFirst();
+ if (repeatableAnnotation.isPresent()) {
+ return Stream.of(annotationInstance.value().asNestedArray());
+ }
+ }
+ return Stream.empty();
+ })
+ .filter(annotation -> directiveFilter(annotation, directiveLocation));
+
+ }
+
+ private static boolean directiveFilter(AnnotationInstance annotation, String directiveLocation) {
+ return stream(
+ getIndex().getClassByName(annotation.name()).annotation(DIRECTIVE).value("on").asEnumArray())
+ .anyMatch(directiveLocation::equals);
+ }
+}
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/DirectiveInstance.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/DirectiveInstance.java
new file mode 100644
index 000000000..b6cea778f
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/DirectiveInstance.java
@@ -0,0 +1,61 @@
+package io.smallrye.graphql.client.model.helper;
+
+import static java.util.stream.Collectors.toUnmodifiableMap;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationValue;
+
+public class DirectiveInstance {
+ private final String name;
+ private final Map values;
+
+ DirectiveInstance(AnnotationInstance annotationInstance) {
+ this.name = toDirectiveName(annotationInstance.name().withoutPackagePrefix());
+ this.values = annotationInstance.values().stream().collect(
+ toUnmodifiableMap(AnnotationValue::name, AnnotationValue::value));
+ }
+
+ public static DirectiveInstance of(AnnotationInstance annotationInstance) {
+ return new DirectiveInstance(annotationInstance);
+ }
+
+ public String buildDirective() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(" @") // space at the beginning for chaining multiple directives.
+ .append(name);
+
+ if (!values.isEmpty()) {
+ builder
+ .append("(")
+ .append(buildDirectiveArgs())
+ .append(")");
+ }
+
+ return builder.toString();
+ }
+
+ private String buildDirectiveArgs() {
+ return values.entrySet().stream()
+ .map(entry -> new StringBuilder()
+ .append(entry.getKey())
+ .append(": ")
+ .append(entry.getValue() instanceof String ? "\"" + entry.getValue() + "\"" : entry.getValue())
+ .toString())
+ .collect(Collectors.joining(", "));
+
+ }
+
+ private String toDirectiveName(String name) {
+ if (Character.isUpperCase(name.charAt(0)))
+ name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return "DirectiveInstance{" + "type=" + name + ", values=" + values + '}';
+ }
+}
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/FieldModel.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/FieldModel.java
new file mode 100644
index 000000000..cdd587769
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/FieldModel.java
@@ -0,0 +1,97 @@
+package io.smallrye.graphql.client.model.helper;
+
+import static io.smallrye.graphql.client.model.Annotations.NAME;
+import static java.util.stream.Collectors.toList;
+
+import java.lang.reflect.Modifier;
+import java.util.List;
+import java.util.Optional;
+
+import org.eclipse.microprofile.graphql.Name;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.FieldInfo;
+
+import io.smallrye.graphql.client.model.Classes;
+
+public class FieldModel implements NamedElement {
+ private FieldInfo field;
+
+ private TypeModel rawType; // only if the Type is TypeVariable (in a custom created Class)
+
+ private List directives;
+
+ public static FieldModel of(FieldInfo field, TypeModel rawType) {
+ return new FieldModel(field, rawType);
+ }
+
+ FieldModel(FieldInfo field, TypeModel rawType) {
+ this.field = field;
+ this.rawType = rawType;
+ this.directives = DirectiveHelper.resolveDirectives(field.annotations().stream(), getDirectiveLocation())
+ .map(DirectiveInstance::of)
+ .collect(toList());
+ }
+
+ /** If the field is renamed with a {@link Name} annotation, the real field name is used as an alias. */
+ public Optional getAlias() {
+ if (field.hasAnnotation(NAME)) {
+ return Optional.of(getRawName());
+ }
+ return Optional.empty();
+ }
+
+ public String getName() {
+ if (field.hasAnnotation(NAME)) {
+ return field.annotation(NAME).value().asString();
+ }
+ return getRawName();
+ }
+
+ public String getRawName() {
+ return field.name();
+ }
+
+ @Override
+ public String getDirectiveLocation() {
+ return "FIELD";
+ }
+
+ public boolean hasAnnotation(DotName annotation) {
+ return field.hasAnnotation(annotation);
+ }
+
+ public boolean isStatic() {
+ return Modifier.isStatic(field.flags());
+ }
+
+ public boolean isTransient() {
+ return Modifier.isTransient(field.flags());
+ }
+
+ public boolean isSynthetic() {
+ return field.asField().isSynthetic();
+ }
+
+ public boolean isParameterized() {
+ return Classes.isParameterized(field.type());
+ }
+
+ public boolean isTypeVariable() {
+ return Classes.isTypeVariable(field.type());
+ }
+
+ public TypeModel getType() {
+ if (TypeModel.of(field.type()).isTypeVariable()) {
+ return rawType.getFirstRawType();
+ }
+ return TypeModel.of(field.type());
+ }
+
+ public boolean hasDirectives() {
+ return !directives.isEmpty();
+ }
+
+ public List getDirectives() {
+ return directives;
+ }
+}
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/MethodModel.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/MethodModel.java
new file mode 100644
index 000000000..f40a72ea9
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/MethodModel.java
@@ -0,0 +1,224 @@
+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.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.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.MethodInfo;
+
+import io.smallrye.graphql.client.core.OperationType;
+import io.smallrye.graphql.client.impl.SmallRyeGraphQLClientMessages;
+
+public class MethodModel implements NamedElement {
+ private final MethodInfo method;
+ private final List parameters;
+ private final Stack typeStack = new Stack<>();
+ private final Stack expressionStack = new Stack<>();
+ private final List directives;
+
+ public static MethodModel of(MethodInfo method) {
+ return new MethodModel(method);
+ }
+
+ MethodModel(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());
+ }
+
+ public String fields(TypeModel type) {
+ if (typeStack.contains(type.getName()))
+ throw SmallRyeGraphQLClientMessages.msg.fieldRecursionFound();
+ try {
+ typeStack.push(type.getName());
+ return recursionCheckedFields(type);
+ } finally {
+ typeStack.pop();
+ }
+ }
+
+ 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());
+ // FIXME: unnecessary whitespaces...
+ return " { key " + keyFields + " value " + valueFields + "}";
+ }
+ return type.fields()
+ .map(this::field)
+ .collect(joining(" ", " { ", " }"));
+ }
+
+ public String field(FieldModel field) {
+ TypeModel type = field.getType();
+ 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();
+ }
+
+ public String declare(ParameterModel parameter) {
+ return "$" + parameter.getRawName() + ": " + parameter.graphQlInputTypeName() +
+ ((parameter.hasDirectives()) ? parameter.getDirectiveInstances()
+ .stream()
+ .map(DirectiveInstance::buildDirective)
+ .collect(joining())
+ : "");
+ }
+
+ public String bind(ParameterModel parameter) {
+ return parameter.getName() + ": $" + parameter.getRawName();
+ }
+
+ public String nestedExpressionPrefix() {
+ return expressionStack.isEmpty() ? "" : expressionStack.peek() + ".";
+ }
+
+ public OperationType getOperationType() {
+ if (method.hasAnnotation(MUTATION)) {
+ return OperationType.MUTATION;
+ }
+ if (method.hasAnnotation(SUBCRIPTION)) {
+ return OperationType.SUBSCRIPTION;
+ }
+ return OperationType.QUERY;
+ }
+
+ public String getName() {
+ return queryName()
+ .orElseGet(() -> mutationName()
+ .orElseGet(() -> subscriptionName()
+ .orElseGet(this::getRawName)));
+ }
+
+ public Optional queryName() {
+ AnnotationInstance queryAnnotation = method.returnType().annotation(QUERY);
+ if (queryAnnotation != null && queryAnnotation.value() != null)
+ return Optional.of(queryAnnotation.value().asString());
+ AnnotationInstance nameAnnotation = method.returnType().annotation(NAME);
+ if (nameAnnotation != null)
+ return Optional.of(nameAnnotation.value().asString());
+ return Optional.empty();
+ }
+
+ public Optional mutationName() {
+ AnnotationInstance mutationAnnotation = method.returnType().annotation(MUTATION);
+ if (mutationAnnotation != null && mutationAnnotation.value() != null)
+ return Optional.of(mutationAnnotation.value().asString());
+ AnnotationInstance nameAnnotation = method.returnType().annotation(NAME);
+ if (nameAnnotation != null)
+ return Optional.of(nameAnnotation.value().asString());
+ return Optional.empty();
+ }
+
+ public Optional subscriptionName() {
+ AnnotationInstance subscriptionAnnotation = method.returnType().annotation(SUBCRIPTION);
+ if (subscriptionAnnotation != null && subscriptionAnnotation.value() != null)
+ return Optional.of(subscriptionAnnotation.value().asString());
+ AnnotationInstance nameAnnotation = method.returnType().annotation(NAME);
+ if (nameAnnotation != null)
+ return Optional.of(nameAnnotation.value().asString());
+ return Optional.empty();
+ }
+
+ 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() {
+ switch (getOperationType()) {
+ case MUTATION:
+ return "MUTATION";
+ case SUBSCRIPTION:
+ return "SUBSCRIPTION";
+ default:
+ return "QUERY";
+ }
+ }
+
+ public List valueParameters() {
+ return parameters.stream().filter(ParameterModel::isValueParameter).collect(Collectors.toList());
+ }
+
+ public List rootParameters() {
+ return parameters.stream().filter(ParameterModel::isRootParameter).collect(Collectors.toList());
+ }
+
+ public List nestedParameters(String path) {
+ return parameters.stream()
+ .filter(ParameterModel::isNestedParameter)
+ .filter(parameter -> parameter
+ .getNestedParameterNames()
+ .anyMatch(path::equals))
+ .collect(toList());
+ }
+
+ public TypeModel getReturnType() {
+ return TypeModel.of(method.returnType());
+ }
+
+ public boolean hasValueParameters() {
+ return !valueParameters().isEmpty();
+ }
+
+ public boolean hasRootParameters() {
+ return !rootParameters().isEmpty();
+ }
+
+ public boolean isSingle() {
+ return getReturnType().isScalar()
+ || getReturnType().isParametrized()
+ || !getIndex().getClassByName(method.returnType().name()).hasAnnotation(MULTIPLE);
+ }
+
+ public boolean hasDirectives() {
+ return !directives.isEmpty();
+ }
+
+ public List getDirectives() {
+ return directives;
+ }
+}
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/NamedElement.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/NamedElement.java
new file mode 100644
index 000000000..5544059b0
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/NamedElement.java
@@ -0,0 +1,13 @@
+package io.smallrye.graphql.client.model.helper;
+
+public interface NamedElement {
+ String getName();
+
+ String getRawName();
+
+ String getDirectiveLocation();
+
+ default boolean isRenamed() {
+ return !getName().equals(getRawName());
+ }
+}
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/ParameterModel.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/ParameterModel.java
new file mode 100644
index 000000000..5516e03de
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/ParameterModel.java
@@ -0,0 +1,129 @@
+package io.smallrye.graphql.client.model.helper;
+
+import static io.smallrye.graphql.client.model.Annotations.ID;
+import static io.smallrye.graphql.client.model.Annotations.INPUT;
+import static io.smallrye.graphql.client.model.Annotations.NAME;
+import static io.smallrye.graphql.client.model.Annotations.NESTED_PARAMETER;
+import static java.util.stream.Collectors.toList;
+
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import org.jboss.jandex.MethodParameterInfo;
+
+import io.smallrye.graphql.client.model.Scalars;
+import io.smallrye.graphql.client.typesafe.api.Header;
+
+public class ParameterModel implements NamedElement {
+ private MethodParameterInfo parameter;
+ private TypeModel type;
+ private List directives;
+
+ ParameterModel(MethodParameterInfo parameter) {
+ this.parameter = parameter;
+ this.type = TypeModel.of(parameter.type());
+ this.directives = DirectiveHelper.resolveDirectives(parameter.annotations().stream(), getDirectiveLocation())
+ .map(DirectiveInstance::of)
+ .collect(toList());
+ }
+
+ public static ParameterModel of(MethodParameterInfo parameter) {
+ return new ParameterModel(parameter);
+ }
+
+ private boolean isHeaderParameter() {
+ return parameter.hasAnnotation(Header.class);
+ }
+
+ public boolean isValueParameter() {
+ return isRootParameter() || isNestedParameter();
+ }
+
+ public boolean isRootParameter() {
+ return !isHeaderParameter() && !isNestedParameter();
+ }
+
+ public boolean isNestedParameter() {
+ return parameter.hasAnnotation(NESTED_PARAMETER);
+ }
+
+ public Stream getNestedParameterNames() {
+ return Stream.of(parameter.annotation(NESTED_PARAMETER).value().asStringArray());
+ }
+
+ public String getName() {
+ if (parameter.hasAnnotation(NAME))
+ return parameter.annotation(NAME).value().asString();
+ if (parameter.name() == null) // ???
+ throw new RuntimeException("Missing name information for " + this + ".\n" +
+ "You can either annotate all parameters with @Name, " +
+ "or compile your source code with the -parameters options, " +
+ "so the parameter names are compiled into the class file and available at runtime.");
+ return getRawName();
+ }
+
+ public String getRawName() {
+ return parameter.name();
+ }
+
+ @Override
+ public String getDirectiveLocation() {
+ return "VARIABLE_DEFINITION";
+ }
+
+ public String graphQlInputTypeName() {
+ if (parameter.hasAnnotation(ID)) {
+ if (type.isCollectionOrArray()) {
+ return "[ID" + arrayOrCollectionHelper(this::optionalExclamationMark) + "]" + optionalExclamationMark(type);
+ }
+ return "ID" + optionalExclamationMark(type);
+ } else if (type.isCollectionOrArray()) {
+ return "[" + arrayOrCollectionHelper(this::withExclamationMark) + "]" + optionalExclamationMark(type);
+ } else if (type.isMap()) {
+ var keyType = type.getMapKeyType();
+ var valueType = type.getMapValueType();
+ return "[Entry_" + withExclamationMark(keyType)
+ + "_" + withExclamationMark(valueType) + "Input]"
+ + optionalExclamationMark(type);
+ } else {
+ return withExclamationMark(type);
+ }
+ }
+
+ private String withExclamationMark(TypeModel type) {
+ return graphQlInputTypeName(type) + optionalExclamationMark(type);
+ }
+
+ private String graphQlInputTypeName(TypeModel type) {
+ if (type.hasAnnotation(INPUT)) {
+ String value = type.getAnnotation(INPUT).value().asString();
+ if (!value.isEmpty()) {
+ return value;
+ }
+ }
+ if (type.hasAnnotation(NAME))
+ return type.getAnnotation(NAME).value().asString();
+ if (Scalars.isScalar(type.getName())) {
+ return Scalars.getScalar(type.getName()); // returns simplified name
+ }
+ return type.getSimpleName() + (type.isEnum() ? "" : "Input");
+ }
+
+ // method.returnType()
+ private String optionalExclamationMark(TypeModel type) {
+ return type.isNonNull() ? "!" : "";
+ }
+
+ private String arrayOrCollectionHelper(Function supplier) {
+ return supplier.apply((type.isArray() ? type.getArrayElementType() : type.getCollectionElementType()));
+ }
+
+ public boolean hasDirectives() {
+ return !directives.isEmpty();
+ }
+
+ public List getDirectiveInstances() {
+ return directives;
+ }
+}
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeAutoNameStrategy.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeAutoNameStrategy.java
new file mode 100644
index 000000000..a3c435aca
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeAutoNameStrategy.java
@@ -0,0 +1,12 @@
+package io.smallrye.graphql.client.model.helper;
+
+/**
+ * Naming strategy for type
+ *
+ * @author Phillip Kruger (phillip.kruger@redhat.com)
+ */
+public enum TypeAutoNameStrategy {
+ Default, // Spec compliant
+ MergeInnerClass, // Inner class prefix parent name
+ Full // Use fully qualified name
+}
diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeModel.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeModel.java
new file mode 100644
index 000000000..e0f394b4a
--- /dev/null
+++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeModel.java
@@ -0,0 +1,208 @@
+package io.smallrye.graphql.client.model.helper;
+
+import static io.smallrye.graphql.client.model.Annotations.IGNORE;
+import static io.smallrye.graphql.client.model.Annotations.JACKSON_IGNORE;
+import static io.smallrye.graphql.client.model.Annotations.JAKARTA_JSONB_TRANSIENT;
+import static io.smallrye.graphql.client.model.Classes.ERROR_OR;
+import static io.smallrye.graphql.client.model.Classes.OBJECT;
+import static io.smallrye.graphql.client.model.Classes.OPTIONAL;
+import static io.smallrye.graphql.client.model.Classes.TYPESAFE_RESPONSE;
+import static io.smallrye.graphql.client.model.Classes.isParameterized;
+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.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.FieldInfo;
+import org.jboss.jandex.Type;
+
+import io.smallrye.graphql.client.model.Annotations;
+import io.smallrye.graphql.client.model.Classes;
+import io.smallrye.graphql.client.model.Scalars;
+
+public class TypeModel {
+ private Type type;
+
+ public static TypeModel of(Type type) {
+ return new TypeModel(type);
+ }
+
+ TypeModel(Type type) {
+ this.type = type;
+ }
+
+ public boolean isPrimitive() {
+ return Classes.isPrimitive(type);
+ }
+
+ public boolean isMap() {
+ return Classes.isMap(type);
+ }
+
+ public boolean isCollectionOrArray() {
+ return isArray() || isCollection();
+ }
+
+ public boolean isClassType() {
+ return isSimpleClassType() || isParametrized() || type.name().equals(OBJECT);
+ }
+
+ public boolean isSimpleClassType() {
+ return Classes.isClass(type) && !isPrimitive();
+ }
+
+ public TypeModel getMapKeyType() {
+ if (!isMap()) {
+ throw new IllegalArgumentException("Expected type to be a Map");
+ }
+ return of(type.asParameterizedType().arguments().get(0));
+ }
+
+ public TypeModel getMapValueType() {
+ if (!isMap()) {
+ throw new IllegalArgumentException("Expected type to be a Map");
+ }
+ return of(type.asParameterizedType().arguments().get(1));
+ }
+
+ public boolean isNonNull() {
+ return isPrimitive() ||
+ type.hasAnnotation(Annotations.NON_NULL) ||
+ type.hasAnnotation(Annotations.JAKARTA_NON_NULL);
+ }
+
+ public boolean isArray() {
+ return Classes.isArray(type);
+ }
+
+ public boolean isCollection() {
+ return Classes.isCollection(type);
+ }
+
+ public boolean hasAnnotation(DotName annotation) {
+ return type.hasAnnotation(annotation);
+ }
+
+ public AnnotationInstance getAnnotation(DotName annotation) {
+ return type.annotation(annotation);
+ }
+
+ public String getSimpleName() {
+ return type.name().withoutPackagePrefix();
+ }
+
+ public String getName() {
+ String name = type.name().toString();
+ if (isParameterized(type)) {
+ return name + "<" + getItemTypes().map(TypeModel::getName).collect(Collectors.joining(", ")) + ">";
+ }
+ return name;
+ }
+
+ public Stream getItemTypes() {
+ if (!Classes.isParameterized(type)) {
+ throw new IllegalArgumentException("Type " + getName() + " is not parametrized, cannot get its Item Types");
+ }
+ return type.asParameterizedType().arguments().stream().map(TypeModel::of);
+ }
+
+ public TypeModel getArrayElementType() {
+ if (!isArray()) {
+ throw new IllegalArgumentException("Type " + getName() + " is not an array type, cannot get its Element Type");
+ }
+ return of(type.asArrayType().elementType());
+ }
+
+ public TypeModel getItemTypeOrElementType() {
+ if (isArray()) {
+ return getArrayElementType();
+ }
+ return getFirstRawType();
+ }
+
+ public TypeModel getCollectionElementType() {
+ return getFirstRawType();
+ }
+
+ public TypeModel getFirstRawType() {
+ return getItemTypes().collect(toList()).get(0);
+
+ }
+
+ public boolean isEnum() {
+ return Classes.isEnum(type);
+ }
+
+ public boolean isScalar() {
+ return Scalars.isScalar(getName()) || isEnum();
+ }
+
+ public boolean isTypesafeResponse() {
+ return isParametrized() && type.name().equals(TYPESAFE_RESPONSE);
+ }
+
+ public boolean isErrorOr() {
+ return isParametrized() && type.name().equals(ERROR_OR);
+ }
+
+ public boolean isOptional() { // Optional* classes are considered as Scalars, we check here if type is a `Optional`
+ return isParametrized() && type.name().equals(OPTIONAL);
+ }
+
+ public boolean isAsync() {
+ return Classes.isAsync(type);
+ }
+
+ public boolean isParametrized() {
+ return Classes.isParameterized(type);
+ }
+
+ public boolean isTypeVariable() {
+ return Classes.isTypeVariable(type);
+ }
+
+ public Stream fields() {
+ if (!isClassType()) {
+ throw new IllegalArgumentException(
+ "Expected type " + type.name().toString() + " to be Class type, cannot get fields from non-class type");
+ }
+ return fields(getIndex().getClassByName(type.name()));
+ }
+
+ private Stream fields(ClassInfo clazz) {
+ return (clazz == null) ? Stream.of()
+ : Stream.concat(
+ fields(getIndex().getClassByName(clazz.superClassType().asClassType().name())), // to superClass
+ fieldsHelper(clazz)
+ .map(field -> FieldModel.of(field, TypeModel.of(type)))
+ .filter(this::isGraphQlField));
+
+ }
+
+ private Stream fieldsHelper(ClassInfo clazz) {
+ if (System.getSecurityManager() == null) {
+ return clazz.fields().stream();
+ }
+ return AccessController.doPrivileged((PrivilegedAction>) () -> clazz.fields().stream());
+
+ }
+
+ private boolean isGraphQlField(FieldModel field) {
+ return !field.isStatic() &&
+ !field.isSynthetic() &&
+ !field.isTransient() &&
+ !isAnnotatedBy(field, IGNORE, JAKARTA_JSONB_TRANSIENT, JACKSON_IGNORE);
+ }
+
+ private boolean isAnnotatedBy(FieldModel field, DotName... annotationClasses) {
+ return Arrays.stream(annotationClasses).anyMatch(field::hasAnnotation);
+ }
+}
diff --git a/client/model/pom.xml b/client/model/pom.xml
new file mode 100644
index 000000000..7762b5f65
--- /dev/null
+++ b/client/model/pom.xml
@@ -0,0 +1,13 @@
+
+
+ 4.0.0
+
+ io.smallrye
+ smallrye-graphql-client-parent
+ 2.6.1-SNAPSHOT
+
+
+ smallrye-graphql-client-model
+ SmallRye: GraphQL Client :: model
+
+
\ No newline at end of file
diff --git a/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModel.java b/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModel.java
new file mode 100644
index 000000000..95c0b5162
--- /dev/null
+++ b/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModel.java
@@ -0,0 +1,27 @@
+package io.smallrye.graphql.client.model;
+
+import java.util.Map;
+
+public class ClientModel {
+ // {className: {methodSignature: query}}
+ private Map> operationMap;
+
+ public ClientModel() {
+ }
+
+ public void setOperationMap(Map> operationMap) {
+ this.operationMap = operationMap;
+ }
+
+ public Map> getOperationMap() {
+ return operationMap;
+ }
+
+ // for testing purposes...
+ @Override
+ public String toString() {
+ return "ClientModel{" +
+ "operationMap=" + operationMap +
+ '}';
+ }
+}
diff --git a/client/pom.xml b/client/pom.xml
index bacc135bb..7c299a94f 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -21,6 +21,8 @@
generator
generator-test
tck
+ model
+ model-builder
diff --git a/pom.xml b/pom.xml
index ea925f197..fb3a06974 100644
--- a/pom.xml
+++ b/pom.xml
@@ -352,6 +352,16 @@
smallrye-graphql-client-implementation-vertx
${project.version}
+
+ ${project.groupId}
+ smallrye-graphql-client-model
+ ${project.version}
+
+
+ ${project.groupId}
+ smallrye-graphql-client-model-builder
+ ${project.version}
+
com.graphql-java
diff --git a/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToDynamicClientTest.java b/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToDynamicClientTest.java
index 0f8454dbd..8780bafd5 100644
--- a/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToDynamicClientTest.java
+++ b/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToDynamicClientTest.java
@@ -10,7 +10,6 @@
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
-import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -45,7 +44,6 @@ public void testSimpleRecord() throws Exception {
assertEquals("a", result.a());
assertEquals("b", result.b());
}
-
}
/**
diff --git a/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToTypesafeClientTest.java b/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToTypesafeClientTest.java
index adb092364..7fc7970dd 100644
--- a/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToTypesafeClientTest.java
+++ b/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToTypesafeClientTest.java
@@ -9,9 +9,7 @@
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
-import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
-import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;