Skip to content

Latest commit

 

History

History
790 lines (704 loc) · 31 KB

ReadMe.md

File metadata and controls

790 lines (704 loc) · 31 KB

Build Status Quality Gate Status Code Coverage Maven Central Gradle Plugin Portal

GraphQL Request Body Generator

This utility library helps to generate request body for GraphQL queries using POJOs.

Table of contents

Getting started

To add library to your project perform next steps:

Maven

Add the following dependency to your pom.xml:

<dependency>
      <groupId>com.github.vladislavsevruk</groupId>
      <artifactId>graphql-request-body-generator</artifactId>
      <version>1.0.16</version>
</dependency>

Gradle

Add the following dependency to your build.gradle:

implementation 'com.github.vladislavsevruk:graphql-request-body-generator:1.0.16'

Usage

First of all we need to choose field marking strategy that will be used for GraphQL operation generation.

Field marking strategy

There are two predefined field marking strategy that define the way how builder determine if field should be used for GraphQL operation generation:

Current strategy for both operation selection set and mutation input object values can be set using FieldMarkingStrategySourceManager:

FieldMarkingStrategySourceManager.selectionSet().useAllExceptIgnoredFieldsStrategy();
// or
FieldMarkingStrategySourceManager.input().useOnlyMarkedFieldsStrategy();

However, you can set your own custom FieldMarkingStrategy:

FieldMarkingStrategySourceManager.selectionSet().useCustomStrategy(field -> true);

Prepare POJO model

Then we need to prepare POJO models that will be used for GraphQL operation generation according to chosen field marking strategy.

Automatic generation

For automatic generation of POJO models at your project you can use following options:

Gradle plugin

Add the following plugin to your build.gradle:

plugins {
  id 'io.github.vladislavsevruk.graphql-model-generator-plugin' version '1.0.0'
}

This plugin will add additional generateGraphqlModels task before javaCompile that will automatically generate POJO models based on GraphQL schema file. Generation results can be customized using followed options of graphqlModelGenerator extension:

import com.github.vladislavsevruk.generator.model.graphql.constant.*

graphqlModelGenerator {
  addJacksonAnnotations = false
  entitiesPrefix = ''
  entitiesPostfix = ''
  pathToSchemaFile = '/path/to/schema.graphqls'
  targetPackage = 'com.myorg'
  treatArrayAs = ElementSequence.LIST
  treatFloatAs = GqlFloatType.DOUBLE
  treatIdAs = GqlIntType.STRING
  treatIntAs = GqlIntType.INTEGER
  updateNamesToJavaStyle = true
  useLombokAnnotations = false
  usePrimitivesInsteadOfWrappers = false
  useStringsInsteadOfEnums = false
}
  • addJacksonAnnotations reflects if jackson annotations should be added to fields for proper mapping using Jackson library. Default value is false;
  • entitiesPrefix is used for adding specific prefix to generated POJO model names. Default value is empty string;
  • entitiesPostfix is used for adding specific postfix to generated POJO model names. Default value is empty string;
  • pathToSchemaFile is used for setting location of GraphQL schema file. Default location is src/main/resources/graphql/schema.graphqls;
  • targetPackage is used for setting specific package name for generated POJO models. Default value is com.github.vladislavsevruk.model;
  • treatArrayAs is used for setting entity that should be used for GraphQL array type generation. Can be one of
  • following values: ARRAY, COLLECTION, ITERABLE, LIST, SET. Default value is LIST;
  • treatFloatAs is used for setting entity that should be used for GraphQL array type generation. Can be one of
  • following values: BIG_DECIMAL, DOUBLE, FLOAT, STRING. Default value is DOUBLE;
  • treatIdAs is used for setting entity that should be used for GraphQL array type generation. Can be one of
  • following values: BIG_INTEGER, INTEGER, LONG, STRING. Default value is STRING;
  • treatIntAs is used for setting entity that should be used for GraphQL array type generation. Can be one of
  • following values: BIG_INTEGER, INTEGER, LONG, STRING. Default value is INTEGER;
  • updateNamesToJavaStyle reflects if entities and fields from GraphQL schema should be updated to follow default java convention (upper camel case for classes and lower camel case for fields). Default value is true;
  • useLombokAnnotations reflects if lombok annotations should be used for POJO methods generation instead of ones generated by this plugin. Default value is false;
  • usePrimitivesInsteadOfWrappers reflects if java primitives should be used for GraphQL scalar types instead of java primitive wrapper classes. Default value is false;
  • useStringsInsteadOfEnums reflects if GraphQL enum fields at type entities should be converted to string type. Default value is false;

Manual generation

However, you still can create and customize model by yourself following rules described below.

Selection set

Selection set generation is based only on fields that are declared at class or it superclasses and doesn't involve declared methods.

GqlField

GqlField annotation marks model fields that should be treated as GraphQL field:

public class User {
    @GqlField
    private Long id;
}

By default field name will be used for generation but you can override it using name method:

public class User {
    @GqlField(name = "wishListItemsUrls")
    private List<String> wishListItems;
}

Some fields may contain a selection set so if you mark field using withSelectionSet method it will be treated as field that have nested fields:

public class User {
    @GqlField(withSelectionSet = true)
    private Contacts contacts;
    @GqlField(withSelectionSet = true)
    private List<Order> orders;
}

public class Contacts {
    @GqlField
    private String email;
    @GqlField
    private String phoneNumber;
}

public class Order {
    @GqlField
    private Long id;
    @GqlField
    private Boolean isDelivered;
}

Annotation also has method nonNull that allow to mark field that was denoted as non-null at GraphQL schema:

public class User {
    @GqlField(nonNull = true)
    private Long id;
}

If field requires arguments for GraphQL operation they can be provided via arguments function:

public class User {
    @GqlField(arguments = { @GqlFieldArgument(name = "width", value = "100"),
                            @GqlFieldArgument(name = "height", value = "50") })
    private String profilePic;
}

Values of GqlFieldArgument annotation will be added to selection set "as is" so you may need to escape quotes for literal values:

public class User {
    @GqlField(withSelectionSet = true,
              arguments = @GqlFieldArgument(name = "sortBy", value = "\"orderDate DESC\""))
    private List<Order> orders;
}

public class Order {
    @GqlField
    private Long id;
    @GqlField
    private Date orderDate;
}

Also alias can be specified for field using self-titled method:

public class User {
    @GqlField(name = "profilePic",
              alias = "smallPic",
              arguments = { @GqlFieldArgument(name = "size", value = "64")})
    private String smallPic;
    @GqlField(name = "profilePic",
              alias = "bigPic",
              arguments = { @GqlFieldArgument(name = "size", value = "1024")})
    private String bigPic;
}

GqlDelegate

GqlDelegate is used for complex fields to treat its inner fields like they are declared at same class where field itself declared:

public class User {
    @GqlDelegate
    private UserInfo userInfo;
} 

public class UserInfo {
    @GqlField
    private String firstName;
    @GqlField
    private String lastName;
}

Code above is equivalent to:

public class User {
    @GqlField
    private String firstName;
    @GqlField
    private String lastName;
}

GqlUnion

GqlUnion annotation is used for fields that should be treated as union:

public class Order {
    @GqlField
    private Long id;
    @GqlField
    private Date orderDate;
    @GqlUnion({ @GqlUnionType(Book.class), @GqlUnionType(value = BoardGame.class, name = "Game") })
    private OrderItem orderItem;
}

public class OrderItem {
    @GqlField
    private Long id;
    @GqlField
    private String title;
}

public class Book extends OrderItem {
    @GqlField
    private Long numberOfPages;
}

public class BoardGame extends OrderItem {
    @GqlField
    private Long numberOfPlayers;
}

GqlIgnore

GqlIgnore is used with "all fields except ignored" field marking strategy for marking field that shouldn't be used for generation:

public class UserInfo {
    private String firstName;
    @GqlIgnore
    private String fullName;
    private String lastName;
}

Input object value

Input object value generation goes through fields that are declared at class or it superclasses and gets values from related getter methods (or field itself if no related getter found).

GqlField

GqlField annotation is also used for input object value generation and marks input values of any type:

public class User {
    @GqlField
    private Long id;
    @GqlField
    private Contacts contacts;
}

public class Contacts {
    @GqlField
    private String email;
    @GqlField
    private String phoneNumber;
}

By default field name will be used for generation but you can override it using name method:

public class User {
    @GqlField(name = "wishListItemsUrls")
    private List<String> wishListItems;
}

GqlInput

GqlInput annotation is used with methods and helps to point to related GraphQL field if method name doesn't match field getter pattern:

public class User {
    @GqlField
    private String name;
    @GqlField
    private String surname;

    @GqlInput(name = "name")
    public String getFirstName() {
        return name;
    }

    @GqlInput(name = "surname")
    public String getLastName() {
        return surname;
    }
}

If provided name doesn't match any of GraphQL fields at model result of method execution will be treated as new field:

public class User {
    @GqlField
    private String firstName;
    @GqlField
    private String lastName;

    @GqlInput(name = "fullName")
    public String getFullName() {
        return firstName + " " + lastName;
    }
}

GqlDelegate

GqlDelegate is used for complex values to treat its inner fields like they are declared at same class where field itself declared:

public class User {
    @GqlDelegate
    private UserInfo userInfo;
} 

public class UserInfo {
    @GqlField
    private String firstName;
    @GqlField
    private String lastName;
}

Models above are equivalent to:

public class User {
    @GqlField
    private String firstName;
    @GqlField
    private String lastName;
}

Like GqlInput GqlDelegate can be applied to methods without related GraphQL field. In this case return value of method will be treated as delegated field:

public class User {
    @GqlDelegate
    public UserInfo getUserInfo() {
        // UserInfo generation
    }
} 

public class UserInfo {
    @GqlField
    private String firstName;
    @GqlField
    private String lastName;
}

Code above is equivalent to:

public class User {
    @GqlField
    private String firstName;
    @GqlField
    private String lastName;
}

GqlIgnore

GqlIgnore is used with "all fields except ignored" field marking strategy for marking field that shouldn't be used for generation:

public class UserInfo {
    private String firstName;
    @GqlIgnore
    private String fullName;
    private String lastName;
}

Generate request body

Once POJO models are ready we can generate GraphQL operation using GqlRequestBodyGenerator:

// query
String query = GqlRequestBodyGenerator.query("allUsers").selectionSet(User.class).generate();

// prepare input model
User newUser = new User();
newUser.setFirstName("John");
newUser.setLastName("Doe");
// mutation
GqlInputArgument input = GqlInputArgument.of(newUser);
String mutation = GqlRequestBodyGenerator.mutation("newUser").arguments(input).selectionSet(User.class)
        .generate();

Generated operation is wrapped into json and can be passed as body to any API Client. If you want to generate pure unwrapped to JSON GraphQL query you can use GqlRequestBodyGenerator.unwrapped() first, all other methods calls remain the same:

// query
String query = GqlRequestBodyGenerator.unwrapped().query("allUsers").selectionSet(User.class)
        .generate();

// prepare input model
User newUser = new User();
newUser.setFirstName("John");
newUser.setLastName("Doe");
// mutation
GqlInputArgument input = GqlInputArgument.of(newUser);
String mutation = GqlRequestBodyGenerator.unwrapped().mutation("newUser").arguments(input)
        .selectionSet(User.class).generate();

Operation selection set

By default, all marked fields will be used for selection set generation. However, you can generate selection set using one of predefined fields picking strategies:

  • pick all marked fields [default]
String query = GqlRequestBodyGenerator.query("allUsers")
        .selectionSet(User.class, SelectionSetGenerationStrategy.allFields()).generate();
// same result as
String query2 = GqlRequestBodyGenerator.query("allUsers").selectionSet(User.class).generate();
  • pick only fields with id name or fields that have nested field with id name
String query = GqlRequestBodyGenerator.query("allUsers")
        .selectionSet(User.class, SelectionSetGenerationStrategy.onlyId()).generate();
  • pick only fields that are marked as non-null
String query = GqlRequestBodyGenerator.query("allUsers")
        .selectionSet(User.class, SelectionSetGenerationStrategy.onlyNonNull()).generate();
String query = GqlRequestBodyGenerator.query("allUsers")
        .selectionSet(User.class, SelectionSetGenerationStrategy.fieldsWithoutSelectionSets()).generate();

Also you can provide your own custom fields picking strategy that implements FieldsPickingStrategy functional interface:

String query = GqlRequestBodyGenerator.query("allUsers")
        .selectionSet(User.class, field -> field.getName().contains("Name")).generate();

If you want to use generic models it's recommended to provide them via TypeProvider:

String query = GqlRequestBodyGenerator.query("allUsers")
        .selectionSet(new TypeProvider<User<UserInfo>>() {}).generate();

Loop breaking strategy

Some models may contain circular type reference on each other, like

public class Parent {
  @GqlField
  private Long id;
  @GqlField(withSelectionSet = true)
  private List<Child> children;
}

public class Child {
  @GqlField
  private Long id;
  @GqlField(withSelectionSet = true)
  private Parent parent;
}

which leads to stack overflow during selection set generation. To avoid this library uses loop breaking mechanism that can be customized using one of predefined loop breaking strategies:

  • do not include detected looped item to selection set [default]
String query = GqlRequestBodyGenerator.query("queryName").selectionSet(Parent.class)
        .generate();

will result to

{"query":"{queryName{id children{id}}}"}
  • allow received nesting level
int nestingLevel = 1;
String query = GqlRequestBodyGenerator.query("queryName")
        .selectionSet(Parent.class, EndlessLoopBreakingStrategy.nestingStrategy(nestingLevel))
        .generate();

will result to

{"query":"{queryName{id children{id parent{id children{id}}}}}"}

This strategy will be used as default during generation but you can set another max nesting level for specific field using maxNestingLoopLevel method at GqlField and GqlUnionType annotations.

public class Parent {
  @GqlField
  private Long id;
  @GqlField(withSelectionSet = true, maxNestingLoopLevel = 1)
  private List<Child> children;
}

public class Child {
  @GqlField
  private Long id;
  @GqlField(withSelectionSet = true)
  private Parent parent;
}

Also you can provide your own custom loop breaking strategy that implements LoopBreakingStrategy functional interface:

String query = GqlRequestBodyGenerator.query("queryName")
        .selectionSet(Parent.class,
                (typeMeta, trace) -> typeMeta.getType().equals(Parent.class))
        .generate();

Arguments

Some operations may require arguments (to pick specific item or filter items list, for example) so you can provide necessary arguments to operation using GqlArgument class:

GqlArgument<Long> idArgument = GqlArgument.of("id", 1L);
String query = GqlRequestBodyGenerator.query("user").arguments(idArgument)
        .selectionSet(User.class).generate();

If you need to provide several arguments you can use varargs:

GqlArgument<List<String>> firstNameArgument = GqlArgument.of("lastName", Arrays.asList("John", "Jane"));
GqlArgument<String> lastNameArgument = GqlArgument.of("lastName", "Doe");
String query = GqlRequestBodyGenerator.query("activeUsers").arguments(firstNameArgument, lastNameArgument)
        .selectionSet(User.class).generate();

or iterables:

GqlArgument<List<String>> firstNameArgument = GqlArgument.of("lastName", Arrays.asList("John", "Jane"));
GqlArgument<String> lastNameArgument = GqlArgument.of("lastName", "Doe");
List<GqlArgument<?>> arguments = Arrays.asList(firstNameArgument, lastNameArgument);
String query = GqlRequestBodyGenerator.query("activeUsers").arguments(arguments)
        .selectionSet(User.class).generate();

Input argument

Mutations usually use input argument to pass complex input objects. You can use GqlInputArgument to pass input object for mutation:

// prepare input model
User newUser = new User();
newUser.setFirstName("John");
newUser.setLastName("Doe");
// mutation
GqlInputArgument<User> inputArgument = GqlInputArgument.of(newUser);
// the same as GqlArgument<User> inputArgument = GqlArgument.of("input", newUser);
String mutation = GqlRequestBodyGenerator.mutation("newUser").arguments(inputArgument)
        .selectionSet(User.class).generate();

By default, input object will be generated using non-null field values but like selection set input object can be generated using predefined fields picking strategies:

  • pick all marked fields
String query = GqlRequestBodyGenerator.mutation("newUser")
        .arguments(InputGenerationStrategy.allFields(), inputArgument).selectionSet(User.class).generate();
  • pick only fields with non-null value
String query = GqlRequestBodyGenerator.mutation("newUser")
        .arguments(InputGenerationStrategy.nonNullsFields(), inputArgument).selectionSet(User.class).generate();

But you can provide your own input fields picking strategy that implements InputFieldsPickingStrategy interface:

InputFieldsPickingStrategy inputFieldsPickingStrategy = field -> field.getName().contains("Name");
String query = GqlRequestBodyGenerator.mutation("newUser")
        .arguments(inputFieldsPickingStrategy, inputArgument).selectionSet(User.class).generate();

Variables

As GraphQL query can be parameterized with variables you can generate GraphQL operation body that way passing values as operation arguments and using one of predefined variables generation strategies:

String variableName = "id";
GqlParameterValue<Integer> variable = GqlVariableArgument.of(variableName, variableName, 1, true);
String query = GqlRequestBodyGenerator.query("getProfile")
        .arguments(VariableGenerationStrategy.byArgumentType(), variable)
        .selectionSet(User.class).generate();
  • generate only for arguments with value type annotated by GqlVariableType type;
@GqlVariableType(variableType = "ContactsData", variableName = "contactsData")
public class Contacts {
  @GqlField
  private String email;
  @GqlField
  private String phoneNumber;

  // getters and setters
    ...
}
Contacts contacts = new Contacts().setEmail("test@domain.com").setPhoneNumber("3751945");
String variableName = "id";
GqlArgument<Contacts> variable = GqlArgument.of("contacts", contacts);
String query = GqlRequestBodyGenerator.mutation("updateContacts")
        .arguments(VariableGenerationStrategy.annotatedArgumentValueType(), variable)
        .selectionSet(Contacts.class).generate();

Or you can provide your own variables generation strategy that implements VariablePickingStrategy interface.

Delegate argument

If query or mutation has big arguments number it may be easier to create object to keep them all at one place and manage together. In that case you can use GqlDelegateArgument to simply pass such object as argument:

Contacts contacts = new Contacts().setEmail("test@domain.com").setPhoneNumber("3751945");
GqlArgument<Contacts> delegateArgument = GqlDelegateArgument.of(contacts);
String query = GqlRequestBodyGenerator.mutation("updateContacts")
        .arguments(delegateArgument).selectionSet(Contacts.class).generate();

If you want to generate variables for delegated values you need to pass flag to GqlDelegateArgument and add GqlVariableType annotations to fields that should be passed as variables:

public class Contacts {
  @GqlField
  @GqlVariableType
  private String email;
  @GqlField
  @GqlVariableType
  private String phoneNumber;

  // getters and setters
    ...
}
Contacts contacts = new Contacts().setEmail("test@domain.com").setPhoneNumber("3751945");
GqlArgument<Contacts> delegateArgument = GqlDelegateArgument.of(contacts, true);
String query = GqlRequestBodyGenerator.mutation("updateContacts")
        .arguments(delegateArgument).selectionSet(Contacts.class).generate();

Mutation argument strategy

By default, only input argument value is treated as complex input objects according to GraphQL specification. However, you can use other model arguments strategies to override this behavior:

  • treat only input argument as potential complex input object
String query = GqlRequestBodyGenerator.mutation("newUser")
        .arguments(ModelArgumentGenerationStrategy.onlyInputArgument(), inputArgument)
        .selectionSet(User.class).generate();
  • treat any argument as potential complex input object
String query = GqlRequestBodyGenerator.mutation("newUser")
        .arguments(ModelArgumentGenerationStrategy.anyArgument(), inputArgument)
        .selectionSet(User.class).generate();

You can also provide your own mutation arguments strategy that implements ModelArgumentStrategy interface:

ModelArgumentStrategy modelArgumentStrategy = argument -> argument.getName().contains("Model");
String query = GqlRequestBodyGenerator.mutation("newUser")
        .arguments(modelArgumentStrategy, inputArgument).selectionSet(User.class).generate();

Operation Alias

When generating operation with variables resulted new operation will be anonymous by default. You can set operation alias for convenience using operationAlias method:

String variableName = "id";
String operationAlias = "id";
GqlParameterValue<Integer> variable = GqlVariableArgument.of(variableName, variableName, 1, true);
String query = GqlRequestBodyGenerator.query("getProfile")
        .operationAlias("getProfileParameterized")
        .arguments(VariableGenerationStrategy.byArgumentType(), variable)
        .selectionSet(User.class).generate();

License

This project is licensed under the MIT License, you can read the full text here.