未验证 提交 7ab8cc84 编写于 作者: B Bogdan Kobylynskyi 提交者: GitHub

Fix GraphQL query serialization and add Gradle plugin for client-codegen #37 (#59)

* Introduce codegen for client code - Part 1. #37

* Remove redundant null-checks

* Minor fixes in Request class generation + improve code coverage. #37

* Customizable suffix of ResponseProjection classes. #37

* Code coverage for MappingConfig #37

* Fix method names #37 #53

* Add codegen properties to GraphqlCodegenGradleTask #37

* Rename example module to example-server #37

* Change example-server project for gradle plugin #37

* Add GraphQLResult class which holds response data+errors #37

* Fix GraphQLRequestSerializer #37

* Add example-client project for gradle plugin #37

* Add example-client project to CI and README.md #37

* Code coverage for GraphQLResult #37

* Downgrade all dependencies level to 'implementation' #37

* Fix GraphQL query characters escape + fix errors serialization #37

* Fix GraphQL request serialization #37

* Fix toString() generation for request input classes #37

* Remove redundant apache-commons dependency #37

* Use jackson lib to construct GraphQL query json #37
上级 ce90069d
......@@ -96,8 +96,11 @@ jobs:
name: Build plugin and run unit tests
command: cd ~/repo/plugins/gradle/graphql-java-codegen-gradle-plugin && gradle build publishToMavenLocal
- run:
name: Build example plugin project
command: cd ~/repo/plugins/gradle/example && gradle test
name: Build example-server plugin project
command: cd ~/repo/plugins/gradle/example-server && gradle test
- run:
name: Build example-client plugin project
command: cd ~/repo/plugins/gradle/example-client && gradle test
workflows:
build-library-and-plugins:
jobs:
......
......@@ -50,4 +50,4 @@ Please follow the steps below in order to make the changes:
mvn clean install
```
7. Make sure that `example` project is compiling and running.
\ No newline at end of file
7. Make sure that `example` projects are compiling and running.
\ No newline at end of file
......@@ -17,11 +17,11 @@ repositories {
}
dependencies {
compile "org.freemarker:freemarker:2.3.28"
compile "com.graphql-java:graphql-java:13.0"
compile "com.google.code.gson:gson:2.8.6"
implementation "org.freemarker:freemarker:2.3.28"
implementation "com.graphql-java:graphql-java:13.0"
implementation "com.fasterxml.jackson.core:jackson-databind:2.9.9"
compileOnly "org.projectlombok:lombok:1.18.8"
implementation "org.projectlombok:lombok:1.18.8"
annotationProcessor "org.projectlombok:lombok:1.18.8"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.5.1"
......
......@@ -171,7 +171,8 @@ Access to classes from your code as normal Kotlin classes.
### Example
[example](example)
* GraphQL server code generation: [example-server](example-server)
* GraphQL client code generation: [example-client](example-client)
### Inspired by
......
import io.github.kobylynskyi.graphql.codegen.gradle.GraphqlCodegenGradleTask
plugins {
id "java"
id "idea"
id "application"
id "net.ltgt.apt" version "0.20"
id "io.github.kobylynskyi.graphql.codegen" version "1.6.0-SNAPSHOT"
}
mainClassName = "io.github.kobylynskyi.bikeshop.Application"
dependencies {
compile "org.springframework.boot:spring-boot-starter-web:2.1.7.RELEASE"
compile "org.springframework.boot:spring-boot-starter-data-mongodb:2.1.7.RELEASE"
compile "org.springframework.data:spring-data-commons:2.1.10.RELEASE"
compile "com.graphql-java-kickstart:graphql-java-tools:5.6.1"
compile "com.graphql-java-kickstart:graphql-spring-boot-starter:5.10.0"
compile "com.graphql-java-kickstart:graphiql-spring-boot-starter:5.10.0"
compile "io.github.kobylynskyi:graphql-java-codegen:1.6.0-SNAPSHOT"
compile "org.apache.httpcomponents:httpclient:4.5.12"
implementation "org.mapstruct:mapstruct:1.3.0.Final"
annotationProcessor "org.mapstruct:mapstruct-processor:1.3.0.Final"
compileOnly "org.projectlombok:lombok:1.18.8"
annotationProcessor "org.projectlombok:lombok:1.18.8"
}
/**
* Generate requests and model from external service
*/
compileJava.dependsOn "graphqlCodegenProductService"
sourceSets.main.java.srcDirs "$buildDir/generated-client"
task graphqlCodegenProductService(type: GraphqlCodegenGradleTask) {
graphqlSchemaPaths = ["$projectDir/src/main/resources/external/schema-product-service.graphqls".toString()]
outputDir = new File("$buildDir/generated-client")
modelPackageName = "io.github.kobylynskyi.product.graphql.model"
customTypesMapping = [
DateTime: "java.util.Date"
]
modelNameSuffix = "TO"
generateRequests = true
generateApis = false
}
/**
* Generate apis and model
*/
compileJava.dependsOn "graphqlCodegenOrderService"
sourceSets.main.java.srcDirs "$buildDir/generated-server"
task graphqlCodegenOrderService(type: GraphqlCodegenGradleTask) {
graphqlSchemaPaths = ["$projectDir/src/main/resources/schema.graphqls".toString()]
outputDir = new File("$buildDir/generated-server")
apiPackageName = "io.github.kobylynskyi.order.graphql.api"
modelPackageName = "io.github.kobylynskyi.order.graphql.model"
customTypesMapping = [
DateTime: "java.util.Date"
]
modelNameSuffix = "TO"
}
repositories {
jcenter()
mavenCentral()
mavenLocal()
}
......@@ -5,4 +5,4 @@ pluginManagement {
}
}
rootProject.name = "example"
\ No newline at end of file
rootProject.name = "example-client"
\ No newline at end of file
package io.github.kobylynskyi.bikeshop;
package io.github.kobylynskyi.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
......
package io.github.kobylynskyi.order.external;
import io.github.kobylynskyi.order.model.Product;
import io.github.kobylynskyi.product.graphql.model.ProductTO;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface ExternalProductMapper {
Product map(ProductTO from);
}
package io.github.kobylynskyi.order.external;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLRequest;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLResult;
import io.github.kobylynskyi.order.model.Product;
import io.github.kobylynskyi.order.model.UnableToRetrieveProductException;
import io.github.kobylynskyi.product.graphql.model.ProductByIdQueryRequest;
import io.github.kobylynskyi.product.graphql.model.ProductResponseProjection;
import io.github.kobylynskyi.product.graphql.model.ProductTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
@Service
public class ProductServiceGraphQLClient {
@Autowired
private ExternalProductMapper productMapper;
@Autowired
private RestTemplate restTemplate;
@Value("${external.service.product.url}")
private String productUrl;
public Product getProduct(String productId) throws UnableToRetrieveProductException {
ProductByIdQueryRequest getProductRequest = new ProductByIdQueryRequest();
getProductRequest.setId(productId);
GraphQLRequest request = new GraphQLRequest(getProductRequest,
new ProductResponseProjection()
.id()
.title()
.price());
GraphQLResult<Map<String, ProductTO>> result = restTemplate.exchange(URI.create(productUrl),
HttpMethod.POST,
httpEntity(request),
new ParameterizedTypeReference<GraphQLResult<Map<String, ProductTO>>>() {
}).getBody();
assert result != null;
if (result.hasErrors()) {
throw new UnableToRetrieveProductException(productId, result.getErrors().get(0).getMessage());
}
return productMapper.map(result.getData().get(getProductRequest.getOperationName()));
}
private static HttpEntity<String> httpEntity(Object request) {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
return new HttpEntity<>(request.toString(), headers);
}
}
package io.github.kobylynskyi.order.external.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestClientsConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate(new HttpComponentsClientHttpRequestFactory());
}
}
package io.github.kobylynskyi.bikeshop.graphql;
package io.github.kobylynskyi.order.graphql;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.ExecutionInput;
......
package io.github.kobylynskyi.bikeshop.graphql.config;
package io.github.kobylynskyi.order.graphql.config;
import graphql.GraphQL;
import graphql.execution.AsyncExecutionStrategy;
......
package io.github.kobylynskyi.order.graphql.mappers;
import io.github.kobylynskyi.order.graphql.model.ItemTO;
import io.github.kobylynskyi.order.graphql.model.OrderTO;
import io.github.kobylynskyi.order.model.Item;
import io.github.kobylynskyi.order.model.Order;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface OrderMapper {
OrderTO map(Order from);
ItemTO map(Item from);
}
package io.github.kobylynskyi.bikeshop.graphql.resolvers;
package io.github.kobylynskyi.order.graphql.resolvers;
import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import io.github.kobylynskyi.bikeshop.graphql.api.Mutation;
import io.github.kobylynskyi.bikeshop.graphql.mappers.BikeMapper;
import io.github.kobylynskyi.bikeshop.graphql.model.BikeInputTO;
import io.github.kobylynskyi.bikeshop.graphql.model.BikeTO;
import io.github.kobylynskyi.bikeshop.model.Bike;
import io.github.kobylynskyi.bikeshop.service.BikeService;
import io.github.kobylynskyi.order.graphql.api.Mutation;
import io.github.kobylynskyi.order.graphql.mappers.OrderMapper;
import io.github.kobylynskyi.order.graphql.model.OrderTO;
import io.github.kobylynskyi.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
......@@ -14,13 +12,17 @@ import org.springframework.stereotype.Component;
public class MutationsResolver implements Mutation, GraphQLMutationResolver {
@Autowired
private BikeService service;
private OrderService service;
@Autowired
private BikeMapper mapper;
private OrderMapper mapper;
@Override
public BikeTO newBike(BikeInputTO bikeInputTO) {
Bike savedBike = service.create(mapper.mapInput(bikeInputTO));
return mapper.map(savedBike);
public OrderTO create() {
return mapper.map(service.create());
}
@Override
public OrderTO addProductToOrder(String orderId, String productId, Integer quantity) throws Exception {
return mapper.map(service.addProduct(orderId, productId, quantity));
}
}
package io.github.kobylynskyi.bikeshop.graphql.resolvers;
package io.github.kobylynskyi.order.graphql.resolvers;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import io.github.kobylynskyi.bikeshop.graphql.api.Query;
import io.github.kobylynskyi.bikeshop.graphql.mappers.BikeMapper;
import io.github.kobylynskyi.bikeshop.graphql.model.BikeTO;
import io.github.kobylynskyi.bikeshop.graphql.model.BikeTypeTO;
import io.github.kobylynskyi.bikeshop.model.BikeType;
import io.github.kobylynskyi.bikeshop.service.BikeService;
import io.github.kobylynskyi.order.graphql.api.Query;
import io.github.kobylynskyi.order.graphql.mappers.OrderMapper;
import io.github.kobylynskyi.order.graphql.model.OrderTO;
import io.github.kobylynskyi.order.model.OrderNotFoundException;
import io.github.kobylynskyi.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
......@@ -18,22 +17,17 @@ import static java.util.stream.Collectors.toList;
public class QueriesResolver implements Query, GraphQLQueryResolver {
@Autowired
private BikeService service;
private OrderService service;
@Autowired
private BikeMapper mapper;
private OrderMapper mapper;
@Override
public Collection<BikeTO> bikes() {
return service.findAll().stream()
.map(mapper::map)
.collect(toList());
public Collection<OrderTO> orders() {
return service.getOrders().stream().map(mapper::map).collect(toList());
}
@Override
public Collection<BikeTO> bikesByType(BikeTypeTO bikeTypeTO) {
BikeType bikeType = mapper.mapInputType(bikeTypeTO);
return service.findByType(bikeType).stream()
.map(mapper::map)
.collect(toList());
public OrderTO orderById(String id) throws OrderNotFoundException {
return mapper.map(service.getOrderById(id));
}
}
package io.github.kobylynskyi.order.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Item {
private String productId;
private int quantity;
private BigDecimal total;
}
package io.github.kobylynskyi.order.model;
import lombok.Data;
import org.springframework.data.annotation.Id;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Data
public class Order {
@Id
private String id;
private List<Item> items = new ArrayList<>();
public BigDecimal getTotal() {
return getItems().stream()
.map(Item::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
package io.github.kobylynskyi.order.model;
public class OrderNotFoundException extends Exception {
public OrderNotFoundException(String id) {
super(String.format("Order with id '%s' does not exist", id));
}
}
package io.github.kobylynskyi.order.model;
import lombok.Data;
import org.springframework.data.annotation.Id;
import java.math.BigDecimal;
@Data
public class Product {
@Id
private String id;
private String title;
private BigDecimal price;
}
package io.github.kobylynskyi.order.model;
public enum StockStatus {
IN_STOCK,
SPECIAL_ORDER,
BACK_ORDERED,
COMING_SOON,
SOLD_OUT,
DISCONTINUED
}
package io.github.kobylynskyi.order.model;
public class UnableToRetrieveProductException extends Exception {
public UnableToRetrieveProductException(String id, String message) {
super(String.format("Unable to retrieve product with id '%s': %s", id, message));
}
}
package io.github.kobylynskyi.order.repository;
import io.github.kobylynskyi.order.model.Order;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface OrderRepository extends MongoRepository<Order, String> {
}
package io.github.kobylynskyi.order.service;
import io.github.kobylynskyi.order.external.ProductServiceGraphQLClient;
import io.github.kobylynskyi.order.model.*;
import io.github.kobylynskyi.order.repository.OrderRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Collection;
@Slf4j
@Service
public class OrderService {
@Autowired
private OrderRepository repository;
@Autowired
private ProductServiceGraphQLClient productService;
public Collection<Order> getOrders() {
return repository.findAll();
}
public Order getOrderById(String id) throws OrderNotFoundException {
return repository.findById(id).orElseThrow(() -> new OrderNotFoundException(id));
}
public Order create() {
Order saved = repository.save(new Order());
log.info("Created new order: {}", saved);
return saved;
}
public Order addProduct(String orderId, String productId, int quantity) throws OrderNotFoundException, UnableToRetrieveProductException {
Order order = getOrderById(orderId);
Product product = productService.getProduct(productId);
Item item = order.getItems().stream()
.filter(p -> p.getProductId().equals(productId))
.findFirst()
.orElseGet(() -> {
Item newItem = new Item(productId, 0, BigDecimal.ZERO);
order.getItems().add(newItem);
return newItem;
});
item.setQuantity(item.getQuantity() + quantity);
item.setTotal(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
repository.save(order);
log.info("Added product [id: {}] to the order [id: {}]", product.getId(), order.getId());
return order;
}
}
server.port=8082
external.service.product.url=http://localhost:8081/graphql
\ No newline at end of file
schema {
query: Query
mutation: Mutation
}
type Query {
products: [Product]
productById(id: ID!): Product
productsByIds(ids: [ID!]!): [Product]
}
type Mutation {
create(productInput: ProductInput!): Product
}
type Product {
id: ID!
title: String!
description: String
price: BigDecimal!
sku: String!
stockStatus: StockStatus
addedDateTime: DateTime!
}
input ProductInput {
title: String!
description: String
price: BigDecimal
sku: String!
stockStatus: StockStatus
}
enum StockStatus {
IN_STOCK
SPECIAL_ORDER
BACK_ORDERED
COMING_SOON
SOLD_OUT
DISCONTINUED
}
scalar DateTime
scalar BigDecimal
\ No newline at end of file
schema {
query: Query
mutation: Mutation
}
type Query {
orders: [Order]
orderById(id: ID!): Order
}
type Mutation {
create: Order
addProductToOrder(orderId: ID!, productId: ID!, quantity: Int = 0): Order
}
type Order {
id: ID!
items: [Item!]!
total: BigDecimal!
}
type Item {
quantity: Int!
productId: ID!
total: BigDecimal!
}
scalar DateTime
scalar BigDecimal
\ No newline at end of file
......@@ -31,8 +31,8 @@ sourceSets.main.java.srcDir "$buildDir/generated"
graphqlCodegen {
graphqlSchemaPaths = ["$projectDir/src/main/resources/schema.graphqls".toString()]
outputDir = new File("$buildDir/generated")
apiPackageName = "io.github.kobylynskyi.bikeshop.graphql.api"
modelPackageName = "io.github.kobylynskyi.bikeshop.graphql.model"
apiPackageName = "io.github.kobylynskyi.product.graphql.api"
modelPackageName = "io.github.kobylynskyi.product.graphql.model"
customTypesMapping = [
DateTime: "java.util.Date"
]
......
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
}
}
rootProject.name = "example-server"
\ No newline at end of file
package io.github.kobylynskyi.product;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
\ No newline at end of file
package io.github.kobylynskyi.product.graphql;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.GraphQL;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
/**
* @author bogdankobylinsky
*/
@Slf4j
@RestController
public class GraphQLController {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private GraphQL graphQL;
@RequestMapping(
value = "/graphql",
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public ExecutionResult execute(@RequestBody ExecutionInput executionInput) throws IOException {
return graphQL.execute(executionInput);
}
}
\ No newline at end of file
package io.github.kobylynskyi.product.graphql.config;
import graphql.GraphQL;
import graphql.execution.AsyncExecutionStrategy;
import graphql.language.StringValue;
import graphql.schema.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author bogdankobylinsky
*/
@Configuration
public class GraphQLConfiguration {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
@Bean
public GraphQL graphQL(GraphQLSchema graphQLSchema) {
return GraphQL.newGraphQL(graphQLSchema)
.queryExecutionStrategy(new AsyncExecutionStrategy())
.mutationExecutionStrategy(new AsyncExecutionStrategy())
.build();
}
@Bean
public GraphQLScalarType dateGraphQLScalarType() {
return GraphQLScalarType.newScalar()
.name("DateTime")
.coercing(new Coercing() {
@Override
public Object serialize(Object o) throws CoercingSerializeException {
return DATE_FORMAT.format((Date) o);
}
@Override
public Object parseValue(Object o) throws CoercingParseValueException {
return serialize(o);
}
@Override
public Object parseLiteral(Object o) throws CoercingParseLiteralException {
try {
return DATE_FORMAT.parse(((StringValue) o).getValue());
} catch (ParseException e) {
return null;
}
}
}).build();
}
}
\ No newline at end of file
package io.github.kobylynskyi.bikeshop.graphql.mappers;
package io.github.kobylynskyi.product.graphql.mappers;
import io.github.kobylynskyi.bikeshop.model.Bike;
import io.github.kobylynskyi.bikeshop.model.BikeType;
import io.github.kobylynskyi.bikeshop.graphql.model.BikeInputTO;
import io.github.kobylynskyi.bikeshop.graphql.model.BikeTO;
import io.github.kobylynskyi.bikeshop.graphql.model.BikeTypeTO;
import io.github.kobylynskyi.product.graphql.model.ProductInputTO;
import io.github.kobylynskyi.product.graphql.model.ProductTO;
import io.github.kobylynskyi.product.model.Product;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface BikeMapper {
public interface ProductMapper {
BikeTO map(Bike from);
ProductTO map(Product from);
@Mapping(target = "id", ignore = true) // auto-generated
@Mapping(target = "addedDateTime", ignore = true)
// set in the service
Bike mapInput(BikeInputTO from);
BikeType mapInputType(BikeTypeTO type);
Product mapInput(ProductInputTO from);
}
package io.github.kobylynskyi.product.graphql.resolvers;
import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import io.github.kobylynskyi.product.graphql.api.Mutation;
import io.github.kobylynskyi.product.graphql.model.ProductInputTO;
import io.github.kobylynskyi.product.graphql.model.ProductTO;
import io.github.kobylynskyi.product.graphql.mappers.ProductMapper;
import io.github.kobylynskyi.product.model.Product;
import io.github.kobylynskyi.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MutationsResolver implements Mutation, GraphQLMutationResolver {
@Autowired
private ProductService service;
@Autowired
private ProductMapper mapper;
@Override
public ProductTO create(ProductInputTO ProductInputTO) {
Product savedProduct = service.create(mapper.mapInput(ProductInputTO));
return mapper.map(savedProduct);
}
}
package io.github.kobylynskyi.product.graphql.resolvers;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import io.github.kobylynskyi.product.graphql.api.Query;
import io.github.kobylynskyi.product.graphql.mappers.ProductMapper;
import io.github.kobylynskyi.product.graphql.model.ProductTO;
import io.github.kobylynskyi.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collection;
import static java.util.stream.Collectors.toList;
@Component
public class QueriesResolver implements Query, GraphQLQueryResolver {
@Autowired
private ProductService service;
@Autowired
private ProductMapper mapper;
@Override
public Collection<ProductTO> products() {
return service.findAll().stream().map(mapper::map).collect(toList());
}
@Override
public ProductTO productById(String id) throws Exception {
return mapper.map(service.findById(id));
}
@Override
public Collection<ProductTO> productsByIds(Collection<String> ids) {
return service.findByIds(ids).stream().map(mapper::map).collect(toList());
}
}
package io.github.kobylynskyi.bikeshop.model;
package io.github.kobylynskyi.product.model;
import lombok.Data;
import org.springframework.data.annotation.Id;
......@@ -7,14 +7,15 @@ import java.math.BigDecimal;
import java.util.Date;
@Data
public class Bike {
public class Product {
@Id
private String id;
private BikeType type;
private String brand;
private String size;
private Integer year;
private String title;
private String description;
private BigDecimal price;
private String sku;
private StockStatus stockStatus;
private Date addedDateTime;
}
package io.github.kobylynskyi.product.model;
public class ProductNotFoundException extends Exception {
public ProductNotFoundException(String id) {
super(String.format("Product with id '%s' does not exist", id));
}
}
package io.github.kobylynskyi.product.model;
public enum StockStatus {
IN_STOCK,
SPECIAL_ORDER,
BACK_ORDERED,
COMING_SOON,
SOLD_OUT,
DISCONTINUED
}
package io.github.kobylynskyi.product.repository;
import io.github.kobylynskyi.product.model.Product;
import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.Collection;
public interface ProductRepository extends MongoRepository<Product, String> {
@Override
Collection<Product> findAllById(Iterable<String> ids);
}
package io.github.kobylynskyi.product.service;
import io.github.kobylynskyi.product.model.Product;
import io.github.kobylynskyi.product.model.ProductNotFoundException;
import io.github.kobylynskyi.product.repository.ProductRepository;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Date;
@Slf4j
@Service
public class ProductService {
@Autowired
private ProductRepository repository;
public Collection<Product> findAll() {
return repository.findAll();
}
public Collection<Product> findByIds(Collection<String> ids) {
return repository.findAllById(ids);
}
public Product findById(String id) throws ProductNotFoundException {
return repository.findById(id).orElseThrow(() -> new ProductNotFoundException(id));
}
public Product create(@NonNull Product input) {
input.setAddedDateTime(new Date());
Product saved = repository.save(input);
log.info("Created new product: {}", saved);
return saved;
}
}
schema {
query: Query
mutation: Mutation
}
type Query {
products: [Product]
productById(id: ID!): Product
productsByIds(ids: [ID!]!): [Product]
}
type Mutation {
create(productInput: ProductInput!): Product
}
type Product {
id: ID!
title: String!
description: String
price: BigDecimal!
sku: String!
stockStatus: StockStatus
addedDateTime: DateTime!
}
input ProductInput {
title: String!
description: String
price: BigDecimal!
sku: String!
stockStatus: StockStatus
}
enum StockStatus {
IN_STOCK
SPECIAL_ORDER
BACK_ORDERED
COMING_SOON
SOLD_OUT
DISCONTINUED
}
scalar DateTime
scalar BigDecimal
\ No newline at end of file
package io.github.kobylynskyi.bikeshop.model;
public enum BikeType {
ROAD,
TOURING,
TRIAL,
TRACK,
MOUNTAIN,
HYBRID,
BMX
}
package io.github.kobylynskyi.bikeshop.repository;
import io.github.kobylynskyi.bikeshop.model.Bike;
import io.github.kobylynskyi.bikeshop.model.BikeType;
import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.Collection;
public interface BikeRepository extends MongoRepository<Bike, String> {
Collection<Bike> findByType(BikeType type);
}
package io.github.kobylynskyi.bikeshop.service;
import io.github.kobylynskyi.bikeshop.model.Bike;
import io.github.kobylynskyi.bikeshop.model.BikeType;
import io.github.kobylynskyi.bikeshop.repository.BikeRepository;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Date;
@Slf4j
@Service
public class BikeService {
@Autowired
private BikeRepository repository;
public Collection<Bike> findAll() {
return repository.findAll();
}
public Collection<Bike> findByType(@NonNull BikeType bikeType) {
return repository.findByType(bikeType);
}
public Bike create(@NonNull Bike bikeInput) {
bikeInput.setAddedDateTime(new Date());
Bike savedBike = repository.save(bikeInput);
log.info("Created new bike: {}", savedBike);
return savedBike;
}
}
schema {
query: Query
mutation: Mutation
}
type Query {
bikes: [Bike]
bikesByType(type: BikeType!): [Bike]
}
type Mutation {
newBike(bike: BikeInput!): Bike
}
type Bike {
id: ID
type: BikeType
brand: String
size: String
year: Int
price: BigDecimal
addedDateTime: DateTime
}
input BikeInput {
type: BikeType!
brand: String!
size: String!
year: Int!
price: BigDecimal
}
enum BikeType {
ROAD
TOURING
TRIAL
TRACK
MOUNTAIN
HYBRID
BMX
}
# Format: "yyyy-MM-dd HH:mm:ss Z"
scalar DateTime
scalar BigDecimal
\ No newline at end of file
......@@ -47,6 +47,9 @@ public class GraphqlCodegenGradleTask extends DefaultTask {
private Boolean generateAsyncApi = false;
private Boolean generateParameterizedFieldsResolvers = true;
private Set<String> fieldsWithResolvers = new HashSet<>();
private Boolean generateRequests;
private String requestSuffix;
private String responseProjectionSuffix;
private String jsonConfigurationFile;
@TaskAction
......@@ -68,6 +71,9 @@ public class GraphqlCodegenGradleTask extends DefaultTask {
mappingConfig.setGenerateAsyncApi(generateAsyncApi);
mappingConfig.setGenerateParameterizedFieldsResolvers(generateParameterizedFieldsResolvers);
mappingConfig.setFieldsWithResolvers(fieldsWithResolvers);
mappingConfig.setGenerateRequests(generateRequests);
mappingConfig.setRequestSuffix(requestSuffix);
mappingConfig.setResponseProjectionSuffix(responseProjectionSuffix);
new GraphqlCodegen(getSchemas(), outputDir, mappingConfig, buildJsonSupplier()).generate();
}
......@@ -302,6 +308,36 @@ public class GraphqlCodegenGradleTask extends DefaultTask {
this.fieldsWithResolvers = fieldsWithResolvers;
}
@Input
@Optional
public Boolean getGenerateRequests() {
return generateRequests;
}
public void setGenerateRequests(Boolean generateRequests) {
this.generateRequests = generateRequests;
}
@Input
@Optional
public String getRequestSuffix() {
return requestSuffix;
}
public void setRequestSuffix(String requestSuffix) {
this.requestSuffix = requestSuffix;
}
@Input
@Optional
public String getResponseProjectionSuffix() {
return responseProjectionSuffix;
}
public void setResponseProjectionSuffix(String responseProjectionSuffix) {
this.responseProjectionSuffix = responseProjectionSuffix;
}
@Input
@Optional
public String getJsonConfigurationFile() {
......
......@@ -37,6 +37,7 @@ public class FieldDefinitionToRequestDataModelMapper {
dataModel.put(BUILDER, mappingConfig.getGenerateBuilder());
dataModel.put(EQUALS_AND_HASH_CODE, mappingConfig.getGenerateEqualsAndHashCode());
dataModel.put(TO_STRING, mappingConfig.getGenerateToString());
dataModel.put(TO_STRING_ESCAPE_JSON, mappingConfig.getGenerateRequests());
return dataModel;
}
......
package com.kobylynskyi.graphql.codegen.mapper;
import com.kobylynskyi.graphql.codegen.model.MappingConfig;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperation;
import com.kobylynskyi.graphql.codegen.utils.Utils;
import graphql.language.ListType;
import graphql.language.NonNullType;
......@@ -11,8 +12,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static graphql.language.OperationDefinition.Operation;
/**
* Map GraphQL type to Java type
*
......@@ -183,7 +182,7 @@ class GraphqlTypeToJavaTypeMapper {
* @return Java type wrapped into the subscriptionReturnType
*/
private static String wrapIntoSubscriptionIfRequired(MappingConfig mappingConfig, String javaTypeName, String parentTypeName) {
if (parentTypeName.equalsIgnoreCase(Operation.SUBSCRIPTION.name())
if (parentTypeName.equalsIgnoreCase(GraphQLOperation.SUBSCRIPTION.name())
&& !Utils.isBlank(mappingConfig.getSubscriptionReturnType())) {
return String.format("%s<%s>", mappingConfig.getSubscriptionReturnType(), javaTypeName);
}
......
......@@ -33,6 +33,7 @@ public class InputDefinitionToDataModelMapper {
dataModel.put(BUILDER, mappingConfig.getGenerateBuilder());
dataModel.put(EQUALS_AND_HASH_CODE, mappingConfig.getGenerateEqualsAndHashCode());
dataModel.put(TO_STRING, mappingConfig.getGenerateToString());
dataModel.put(TO_STRING_ESCAPE_JSON, mappingConfig.getGenerateRequests());
return dataModel;
}
......
package com.kobylynskyi.graphql.codegen.mapper;
import com.kobylynskyi.graphql.codegen.model.MappingConfig;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperation;
import com.kobylynskyi.graphql.codegen.utils.Utils;
import graphql.language.Document;
import graphql.language.NamedNode;
import graphql.language.OperationDefinition.Operation;
import graphql.language.UnionTypeDefinition;
import java.util.Arrays;
......@@ -201,7 +201,7 @@ public class MapperUtils {
*/
static Set<String> getImportsForRequests(MappingConfig mappingConfig, String packageName) {
Set<String> imports = getImports(mappingConfig, packageName);
imports.add("graphql.language");
imports.add(GraphQLOperation.class.getPackage().getName());
return imports;
}
......@@ -215,7 +215,8 @@ public class MapperUtils {
static boolean isAsyncQueryOrMutation(MappingConfig mappingConfig, String objectTypeName) {
boolean isAsyncApi = mappingConfig.getGenerateAsyncApi() != null && mappingConfig.getGenerateAsyncApi();
return isAsyncApi && (Operation.QUERY.name().equalsIgnoreCase(objectTypeName) || Operation.MUTATION.name()
.equalsIgnoreCase(objectTypeName));
return isAsyncApi && (
GraphQLOperation.QUERY.name().equalsIgnoreCase(objectTypeName) ||
GraphQLOperation.MUTATION.name().equalsIgnoreCase(objectTypeName));
}
}
......@@ -46,6 +46,7 @@ public class TypeDefinitionToDataModelMapper {
dataModel.put(BUILDER, mappingConfig.getGenerateBuilder());
dataModel.put(EQUALS_AND_HASH_CODE, mappingConfig.getGenerateEqualsAndHashCode());
dataModel.put(TO_STRING, mappingConfig.getGenerateToString());
dataModel.put(TO_STRING_ESCAPE_JSON, mappingConfig.getGenerateRequests());
return dataModel;
}
......@@ -65,7 +66,7 @@ public class TypeDefinitionToDataModelMapper {
Map<String, Object> dataModel = new HashMap<>();
String packageName = MapperUtils.getModelPackageName(mappingConfig);
dataModel.put(PACKAGE, packageName);
dataModel.put(IMPORTS, MapperUtils.getImports(mappingConfig, packageName));
dataModel.put(IMPORTS, MapperUtils.getImportsForRequests(mappingConfig, packageName));
dataModel.put(CLASS_NAME, Utils.capitalize(typeDefinition.getName()) + mappingConfig.getResponseProjectionSuffix());
dataModel.put(FIELDS, getProjectionFields(mappingConfig, typeDefinition, document, typeNames));
dataModel.put(BUILDER, mappingConfig.getGenerateBuilder());
......
......@@ -17,6 +17,7 @@ public final class DataModelFields {
public static final String BUILDER = "builder";
public static final String EQUALS_AND_HASH_CODE = "equalsAndHashCode";
public static final String TO_STRING = "toString";
public static final String TO_STRING_ESCAPE_JSON = "toStringEscapeJson";
public static final String OPERATION_TYPE = "operationType";
public static final String OPERATION_NAME = "operationName";
......
package com.kobylynskyi.graphql.codegen.model.graphql;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GraphQLError {
private String message;
private List<GraphQLErrorSourceLocation> locations;
private GraphQLErrorType errorType;
private List<Object> path;
private Map<String, Object> extensions;
}
\ No newline at end of file
package com.kobylynskyi.graphql.codegen.model.graphql;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GraphQLErrorSourceLocation {
private int line;
private int column;
private String sourceName;
}
package com.kobylynskyi.graphql.codegen.model.graphql;
public enum GraphQLErrorType {
InvalidSyntax,
ValidationError,
DataFetchingException,
OperationNotSupported,
ExecutionAborted
}
package com.kobylynskyi.graphql.codegen.model.graphql;
public enum GraphQLOperation {
QUERY, MUTATION, SUBSCRIPTION
}
package com.kobylynskyi.graphql.codegen.model.graphql;
import java.util.Map;
/**
* The contract for GraphQL request
*/
public interface GraphQLOperationRequest {
/**
* Type of GraphQL operation.
* Can be one of {@link GraphQLOperation}
*
* @return type of GraphQL operation
*/
GraphQLOperation getOperationType();
/**
* Name of GraphQL operation.
*
* @return name of GraphQL operation
*/
String getOperationName();
/**
* Input for for GraphQL operation. Where:
* - key is input field name
* - value is input field value
*
* @return input data for GraphQL operation
*/
Map<String, Object> getInput();
}
package com.kobylynskyi.graphql.codegen.model.request;
package com.kobylynskyi.graphql.codegen.model.graphql;
/**
* Class which represents GraphQL Request
*/
public class GraphQLRequest {
private final GraphQLOperationRequest request;
......
package com.kobylynskyi.graphql.codegen.model.request;
package com.kobylynskyi.graphql.codegen.model.graphql;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Collection;
import java.util.Iterator;
......@@ -7,12 +11,13 @@ import java.util.stream.Collectors;
public class GraphQLRequestSerializer {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static String serialize(GraphQLRequest graphQLRequest) {
if (graphQLRequest == null || graphQLRequest.getRequest() == null) {
return null;
}
StringBuilder builder = new StringBuilder();
builder.append("{\"query\":\"");
builder.append(graphQLRequest.getRequest().getOperationType().name().toLowerCase());
builder.append(" { ");
builder.append(graphQLRequest.getRequest().getOperationName());
......@@ -37,8 +42,17 @@ public class GraphQLRequestSerializer {
builder.append(graphQLRequest.getResponseProjection().toString());
}
builder.append(" }");
builder.append("\"}");
return builder.toString();
return buildJsonQuery(builder.toString());
}
private static String buildJsonQuery(String queryString) {
ObjectNode objectNode = OBJECT_MAPPER.createObjectNode();
objectNode.put("query", queryString);
try {
return OBJECT_MAPPER.writeValueAsString(objectNode);
} catch (JsonProcessingException e) {
throw new UnableToBuildJsonQueryException(e);
}
}
private static String getEntry(Object input) {
......@@ -51,10 +65,50 @@ public class GraphQLRequestSerializer {
if (input instanceof Enum<?>) {
return input.toString();
} else if (input instanceof String) {
return "\"" + input.toString() + "\"";
return "\"" + escapeJsonString(input.toString()) + "\"";
} else {
return input.toString();
}
}
/**
* Encodes the value as a JSON string according to http://json.org/ rules
*
* @param stringValue the value to encode as a JSON string
* @return the encoded string
*/
public static String escapeJsonString(String stringValue) {
int len = stringValue.length();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
char ch = stringValue.charAt(i);
switch (ch) {
case '"':
sb.append("\\\"");
break;
case '\\':
sb.append("\\\\");
break;
case '\b':
sb.append("\\b");
break;
case '\f':
sb.append("\\f");
break;
case '\n':
sb.append("\\n");
break;
case '\r':
sb.append("\\r");
break;
case '\t':
sb.append("\\t");
break;
default:
sb.append(ch);
}
}
return sb.toString();
}
}
package com.kobylynskyi.graphql.codegen.model.graphql;
/**
* The implementation class should basically contain the fields of the particular type which
* should be returned back to the client.
*/
public interface GraphQLResponseProjection {
}
package com.kobylynskyi.graphql.codegen.model.graphql;
import lombok.Data;
import java.util.List;
/**
* GraphQL response. Contains data and errors
*
* @param <T> type of response
*/
@Data
public class GraphQLResult<T> {
private T data;
private List<GraphQLError> errors;
public GraphQLResult() {
}
public GraphQLResult(T data, List<GraphQLError> errors) {
this.data = data;
this.errors = errors;
}
/**
* @return true if there are any errors present
*/
public boolean hasErrors() {
return errors != null && !errors.isEmpty();
}
}
package com.kobylynskyi.graphql.codegen.model.graphql;
public class UnableToBuildJsonQueryException extends IllegalArgumentException {
public UnableToBuildJsonQueryException(Exception e) {
super(e);
}
}
package com.kobylynskyi.graphql.codegen.model.request;
import graphql.language.OperationDefinition;
import java.util.Map;
public interface GraphQLOperationRequest {
OperationDefinition.Operation getOperationType();
String getOperationName();
Map<String, Object> getInput();
}
package com.kobylynskyi.graphql.codegen.model.request;
public interface GraphQLResponseProjection {
}
package com.kobylynskyi.graphql.codegen.supplier;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import com.google.gson.GsonBuilder;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kobylynskyi.graphql.codegen.model.MappingConfig;
/**
......@@ -14,6 +13,8 @@ import com.kobylynskyi.graphql.codegen.model.MappingConfig;
*/
public class JsonMappingConfigSupplier implements MappingConfigSupplier {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private String jsonConfigFile;
/**
......@@ -29,8 +30,8 @@ public class JsonMappingConfigSupplier implements MappingConfigSupplier {
public MappingConfig get() {
if (jsonConfigFile != null && !jsonConfigFile.isEmpty()) {
try {
return new GsonBuilder().create().fromJson(new FileReader(new File(jsonConfigFile)), MappingConfig.class);
} catch (FileNotFoundException e) {
return OBJECT_MAPPER.readValue(new File(jsonConfigFile), MappingConfig.class);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
......
......@@ -6,7 +6,7 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import graphql.language.OperationDefinition;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperation;
/**
* Various utilities
......@@ -26,9 +26,9 @@ public final class Utils {
*/
public static boolean isGraphqlOperation(String typeDef) {
String typeDefNormalized = typeDef.toUpperCase();
return typeDefNormalized.equals(OperationDefinition.Operation.QUERY.name()) ||
typeDefNormalized.equals(OperationDefinition.Operation.MUTATION.name()) ||
typeDefNormalized.equals(OperationDefinition.Operation.SUBSCRIPTION.name());
return typeDefNormalized.equals(GraphQLOperation.QUERY.name()) ||
typeDefNormalized.equals(GraphQLOperation.MUTATION.name()) ||
typeDefNormalized.equals(GraphQLOperation.SUBSCRIPTION.name());
}
/**
......
......@@ -6,9 +6,9 @@ package ${package};
import ${import}.*;
</#list>
public class ${className} implements com.kobylynskyi.graphql.codegen.model.request.GraphQLOperationRequest {
public class ${className} implements GraphQLOperationRequest {
private static final OperationDefinition.Operation OPERATION_TYPE = OperationDefinition.Operation.${operationType};
private static final GraphQLOperation OPERATION_TYPE = GraphQLOperation.${operationType};
private static final String OPERATION_NAME = "${operationName}";
private Map<String, Object> input = new LinkedHashMap<>();
......@@ -23,7 +23,7 @@ public class ${className} implements com.kobylynskyi.graphql.codegen.model.reque
</#list>
@Override
public OperationDefinition.Operation getOperationType() {
public GraphQLOperation getOperationType() {
return OPERATION_TYPE;
}
......
......@@ -6,7 +6,7 @@ package ${package};
import ${import}.*;
</#list>
public class ${className} implements com.kobylynskyi.graphql.codegen.model.request.GraphQLResponseProjection {
public class ${className} implements GraphQLResponseProjection {
private Map<String, Object> fields = new LinkedHashMap<>();
......
......@@ -67,7 +67,11 @@ public class ${className} <#if implements?has_content>implements <#list implemen
<#list fields as field>
if (${field.name} != null) {
<#if field.type == "String">
<#if toStringEscapeJson>
joiner.add("${field.name}: \"" + com.kobylynskyi.graphql.codegen.model.graphql.GraphQLRequestSerializer.escapeJsonString(${field.name}) + "\"");
<#else>
joiner.add("${field.name}: \"" + ${field.name} + "\"");
</#if>
<#else>
joiner.add("${field.name}: " + ${field.name});
</#if>
......
......@@ -296,6 +296,10 @@ class GraphqlCodegenTest {
assertThat(content, StringContains.containsString("public String toString()"));
}
}
assertEquals(Utils.getFileContent("src/test/resources/expected-classes/EventPropertyTO_toString.java.txt"),
Utils.getFileContent(
Arrays.stream(files).filter(f -> f.getName().equals("EventPropertyTO.java")).map(File::getPath)
.findFirst().orElseThrow(FileNotFoundException::new)));
}
@Test
......
package com.kobylynskyi.graphql.codegen.model.request;
package com.kobylynskyi.graphql.codegen.model.graphql;
import com.kobylynskyi.graphql.codegen.model.request.data.*;
import com.kobylynskyi.graphql.codegen.utils.Utils;
import com.kobylynskyi.graphql.codegen.model.graphql.data.*;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertEquals;
......@@ -24,16 +21,14 @@ class GraphQLRequestSerializerTest {
}
@Test
void serialize_noResponseProjection() throws IOException {
String fileContent = getExpectedQueryString("versionQuery.txt");
void serialize_noResponseProjection() {
GraphQLRequest graphQLRequest = new GraphQLRequest(new VersionQueryRequest());
String serializedQuery = graphQLRequest.toString().replaceAll(" +", " ").trim();
assertEquals(fileContent, serializedQuery);
assertEquals(expected("query { version }"), serializedQuery);
}
@Test
void serialize_withResponseProjection() throws IOException {
String fileContent = getExpectedQueryString("eventsByCategoryAndStatusQuery.txt");
void serialize_withResponseProjection() {
EventsByCategoryAndStatusQueryRequest request = new EventsByCategoryAndStatusQueryRequest();
request.setCategoryId("categoryIdValue1");
request.setStatus(Status.OPEN);
......@@ -51,12 +46,19 @@ class GraphQLRequestSerializerTest {
.status()
);
String serializedQuery = graphQLRequest.toString().replaceAll(" +", " ").trim();
assertEquals(fileContent, serializedQuery);
assertEquals(expected("query { " +
"eventsByCategoryAndStatus(categoryId: \\\"categoryIdValue1\\\", status: OPEN){ " +
"id " +
"active " +
"properties { " +
"floatVal child { intVal parent { id } } booleanVal } " +
"status " +
"} " +
"}"), serializedQuery);
}
@Test
void serialize_complexRequestWithDefaultData() throws IOException {
String fileContent = getExpectedQueryString("updateIssueMutation.txt");
void serialize_complexRequestWithDefaultData() {
UpdateIssueMutationRequest requestWithDefaultData = new UpdateIssueMutationRequest();
requestWithDefaultData.setInput(new UpdateIssueInput());
GraphQLRequest graphQLRequest = new GraphQLRequest(requestWithDefaultData,
......@@ -66,28 +68,35 @@ class GraphQLRequestSerializerTest {
.activeLockReason())
);
String serializedQuery = graphQLRequest.toString().replaceAll(" +", " ").trim();
assertEquals(fileContent, serializedQuery);
assertEquals(expected("mutation { updateIssue(input: { " +
"floatVal: 1.23, booleanVal: false, intVal: 42, " +
"stringVal: \\\"default \\\\\\\" \\\\\\\\ \\\\b \\\\f \\\\n \\\\r \\\\t ሴ \\\", " +
"enumVal: OPEN, intList: [1, 2, 3], intListEmptyDefault: [] }){ " +
"clientMutationId issue { activeLockReason } } }"), serializedQuery);
}
@Test
void serialize_collectionRequest() throws IOException {
String fileContent = getExpectedQueryString("eventsByIdsQuery.txt");
void serialize_collectionRequest() {
EventsByIdsQueryRequest request = new EventsByIdsQueryRequest();
request.setIds(Arrays.asList("4", "5", "6"));
request.setIds(Arrays.asList("\"", "\\", "\b", "\f", "\n", "\r", "\t", "\u1234"));
GraphQLRequest graphQLRequest = new GraphQLRequest(request,
new EventResponseProjection()
.id()
);
String serializedQuery = graphQLRequest.toString().replaceAll(" +", " ").trim();
assertEquals(fileContent, serializedQuery);
assertEquals(expected("query { eventsByIds(ids: [ " +
"\\\"\\\\\\\"\\\", " +
"\\\"\\\\\\\\\\\", " +
"\\\"\\\\b\\\", " +
"\\\"\\\\f\\\", " +
"\\\"\\\\n\\\", " +
"\\\"\\\\r\\\", " +
"\\\"\\\\t\\\", " +
"\\\"ሴ\\\" ]){ id } }"), serializedQuery);
}
private static String getExpectedQueryString(final String fileName) throws IOException {
String trimmedContent = Utils.getFileContent(
new File("src/test/resources/expected-classes/request/graphql-query/" + fileName).getPath())
.replaceAll(System.lineSeparator(), " ")
.replaceAll(" +", " ").trim();
return String.format("{\"query\":\"%s\"}", trimmedContent);
private static String expected(String expectedQuery) {
return String.format("{\"query\":\"%s\"}", expectedQuery);
}
}
\ No newline at end of file
package com.kobylynskyi.graphql.codegen.model.graphql;
import com.kobylynskyi.graphql.codegen.model.graphql.data.UpdateIssueInput;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.*;
class GraphQLResultTest {
@Test
void noErrorsEmpty() {
assertFalse(new GraphQLResult<>(new UpdateIssueInput(), emptyList()).hasErrors());
}
@Test
void noErrorsNull() {
assertFalse(new GraphQLResult<>().hasErrors());
}
@Test
void someErrors1() {
assertTrue(new GraphQLResult<>(new UpdateIssueInput(),
singletonList(new GraphQLError())).hasErrors());
}
@Test
void someErrors2() {
GraphQLResult<UpdateIssueInput> result = new GraphQLResult<>();
result.setData(new UpdateIssueInput());
result.setErrors(singletonList(getGraphQLErrorO()));
assertNotNull(result.getData());
assertEquals(1, result.getErrors().size());
assertEquals(getGraphQLErrorO(), result.getErrors().get(0));
}
private static GraphQLError getGraphQLErrorO() {
GraphQLError graphQLError = new GraphQLError();
graphQLError.setErrorType(GraphQLErrorType.ValidationError);
graphQLError.setExtensions(new HashMap<>());
graphQLError.setLocations(singletonList(new GraphQLErrorSourceLocation(1, 2, "3")));
graphQLError.setMessage("something went wrong");
graphQLError.setPath(singletonList("/order/items[0]/product"));
return graphQLError;
}
}
\ No newline at end of file
package com.kobylynskyi.graphql.codegen.model.request.data;
package com.kobylynskyi.graphql.codegen.model.graphql.data;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLResponseProjection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.StringJoiner;
public class EventPropertyResponseProjection implements com.kobylynskyi.graphql.codegen.model.request.GraphQLResponseProjection {
public class EventPropertyResponseProjection implements GraphQLResponseProjection {
private Map<String, Object> fields = new LinkedHashMap<>();
......
package com.kobylynskyi.graphql.codegen.model.request.data;
package com.kobylynskyi.graphql.codegen.model.graphql.data;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLResponseProjection;
import java.util.*;
public class EventResponseProjection implements com.kobylynskyi.graphql.codegen.model.request.GraphQLResponseProjection {
public class EventResponseProjection implements GraphQLResponseProjection {
private Map<String, Object> fields = new LinkedHashMap<>();
......
package com.kobylynskyi.graphql.codegen.model.request.data;
package com.kobylynskyi.graphql.codegen.model.graphql.data;
import java.util.*;
import graphql.language.*;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperation;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperationRequest;
public class EventsByCategoryAndStatusQueryRequest implements com.kobylynskyi.graphql.codegen.model.request.GraphQLOperationRequest {
import java.util.LinkedHashMap;
import java.util.Map;
private static final OperationDefinition.Operation OPERATION_TYPE = OperationDefinition.Operation.QUERY;
public class EventsByCategoryAndStatusQueryRequest implements GraphQLOperationRequest {
private static final GraphQLOperation OPERATION_TYPE = GraphQLOperation.QUERY;
private static final String OPERATION_NAME = "eventsByCategoryAndStatus";
private Map<String, Object> input = new LinkedHashMap<>();
......@@ -22,7 +25,7 @@ public class EventsByCategoryAndStatusQueryRequest implements com.kobylynskyi.gr
}
@Override
public OperationDefinition.Operation getOperationType() {
public GraphQLOperation getOperationType() {
return OPERATION_TYPE;
}
......
package com.kobylynskyi.graphql.codegen.model.request.data;
package com.kobylynskyi.graphql.codegen.model.graphql.data;
import java.util.*;
import graphql.language.*;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperation;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperationRequest;
public class EventsByIdsQueryRequest implements com.kobylynskyi.graphql.codegen.model.request.GraphQLOperationRequest {
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
private static final OperationDefinition.Operation OPERATION_TYPE = OperationDefinition.Operation.QUERY;
public class EventsByIdsQueryRequest implements GraphQLOperationRequest {
private static final GraphQLOperation OPERATION_TYPE = GraphQLOperation.QUERY;
private static final String OPERATION_NAME = "eventsByIds";
private Map<String, Object> input = new LinkedHashMap<>();
......@@ -18,7 +23,7 @@ public class EventsByIdsQueryRequest implements com.kobylynskyi.graphql.codegen.
}
@Override
public OperationDefinition.Operation getOperationType() {
public GraphQLOperation getOperationType() {
return OPERATION_TYPE;
}
......
package com.kobylynskyi.graphql.codegen.model.request.data;
package com.kobylynskyi.graphql.codegen.model.graphql.data;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLResponseProjection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.StringJoiner;
public class IssueResponseProjection implements com.kobylynskyi.graphql.codegen.model.request.GraphQLResponseProjection {
public class IssueResponseProjection implements GraphQLResponseProjection {
private Map<String, Object> fields = new LinkedHashMap<>();
......
package com.kobylynskyi.graphql.codegen.model.request.data;
package com.kobylynskyi.graphql.codegen.model.graphql.data;
public enum Status {
OPEN
......
package com.kobylynskyi.graphql.codegen.model.request.data;
package com.kobylynskyi.graphql.codegen.model.graphql.data;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLRequestSerializer;
import java.util.Arrays;
import java.util.Collection;
......@@ -10,7 +12,7 @@ public class UpdateIssueInput {
private Double floatVal = 1.23;
private Boolean booleanVal = false;
private Integer intVal = 42;
private String stringVal = "my-default";
private String stringVal = "default \" \\ \b \f \n \r \t \u1234 ";
private Status enumVal = Status.OPEN;
private UpdateIssueInput objectWithNullDefault = null;
private Collection<Integer> intList = Arrays.asList(1, 2, 3);
......@@ -43,7 +45,7 @@ public class UpdateIssueInput {
joiner.add("intVal: " + intVal);
}
if (stringVal != null) {
joiner.add("stringVal: \"" + stringVal + "\"");
joiner.add("stringVal: \"" + GraphQLRequestSerializer.escapeJsonString(stringVal) + "\"");
}
if (enumVal != null) {
joiner.add("enumVal: " + enumVal);
......
package com.kobylynskyi.graphql.codegen.model.request.data;
package com.kobylynskyi.graphql.codegen.model.graphql.data;
import graphql.language.OperationDefinition;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperation;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperationRequest;
import java.util.LinkedHashMap;
import java.util.Map;
public class UpdateIssueMutationRequest implements com.kobylynskyi.graphql.codegen.model.request.GraphQLOperationRequest {
public class UpdateIssueMutationRequest implements GraphQLOperationRequest {
private static final OperationDefinition.Operation OPERATION_TYPE = OperationDefinition.Operation.MUTATION;
private static final GraphQLOperation OPERATION_TYPE = GraphQLOperation.MUTATION;
private static final String OPERATION_NAME = "updateIssue";
private Map<String, Object> input = new LinkedHashMap<>();
......@@ -20,7 +21,7 @@ public class UpdateIssueMutationRequest implements com.kobylynskyi.graphql.codeg
}
@Override
public OperationDefinition.Operation getOperationType() {
public GraphQLOperation getOperationType() {
return OPERATION_TYPE;
}
......
package com.kobylynskyi.graphql.codegen.model.request.data;
package com.kobylynskyi.graphql.codegen.model.graphql.data;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLResponseProjection;
import java.util.*;
public class UpdateIssuePayloadResponseProjection implements com.kobylynskyi.graphql.codegen.model.request.GraphQLResponseProjection {
public class UpdateIssuePayloadResponseProjection implements GraphQLResponseProjection {
private Map<String, Object> fields = new LinkedHashMap<>();
......
package com.kobylynskyi.graphql.codegen.model.request.data;
package com.kobylynskyi.graphql.codegen.model.graphql.data;
import java.util.*;
import graphql.language.*;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperation;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperationRequest;
public class VersionQueryRequest implements com.kobylynskyi.graphql.codegen.model.request.GraphQLOperationRequest {
import java.util.LinkedHashMap;
import java.util.Map;
private static final OperationDefinition.Operation OPERATION_TYPE = OperationDefinition.Operation.QUERY;
public class VersionQueryRequest implements GraphQLOperationRequest {
private static final GraphQLOperation OPERATION_TYPE = GraphQLOperation.QUERY;
private static final String OPERATION_NAME = "version";
private Map<String, Object> input = new LinkedHashMap<>();
......@@ -14,7 +17,7 @@ public class VersionQueryRequest implements com.kobylynskyi.graphql.codegen.mode
}
@Override
public OperationDefinition.Operation getOperationType() {
public GraphQLOperation getOperationType() {
return OPERATION_TYPE;
}
......
package com.kobylynskyi.graphql.test1;
import java.util.*;
public class EventPropertyTO {
private Double floatVal;
private Boolean booleanVal;
private Integer intVal;
private String stringVal;
private Collection<EventPropertyTO> child;
private EventTO parent;
public EventPropertyTO() {
}
public EventPropertyTO(Double floatVal, Boolean booleanVal, Integer intVal, String stringVal, Collection<EventPropertyTO> child, EventTO parent) {
this.floatVal = floatVal;
this.booleanVal = booleanVal;
this.intVal = intVal;
this.stringVal = stringVal;
this.child = child;
this.parent = parent;
}
public Double getFloatVal() {
return floatVal;
}
public void setFloatVal(Double floatVal) {
this.floatVal = floatVal;
}
public Boolean getBooleanVal() {
return booleanVal;
}
public void setBooleanVal(Boolean booleanVal) {
this.booleanVal = booleanVal;
}
public Integer getIntVal() {
return intVal;
}
public void setIntVal(Integer intVal) {
this.intVal = intVal;
}
public String getStringVal() {
return stringVal;
}
public void setStringVal(String stringVal) {
this.stringVal = stringVal;
}
public Collection<EventPropertyTO> getChild() {
return child;
}
public void setChild(Collection<EventPropertyTO> child) {
this.child = child;
}
public EventTO getParent() {
return parent;
}
public void setParent(EventTO parent) {
this.parent = parent;
}
@Override
public String toString() {
StringJoiner joiner = new StringJoiner(", ", "{ ", " }");
if (floatVal != null) {
joiner.add("floatVal: " + floatVal);
}
if (booleanVal != null) {
joiner.add("booleanVal: " + booleanVal);
}
if (intVal != null) {
joiner.add("intVal: " + intVal);
}
if (stringVal != null) {
joiner.add("stringVal: \"" + stringVal + "\"");
}
if (child != null) {
joiner.add("child: " + child);
}
if (parent != null) {
joiner.add("parent: " + parent);
}
return joiner.toString();
}
public static class Builder {
private Double floatVal;
private Boolean booleanVal;
private Integer intVal;
private String stringVal;
private Collection<EventPropertyTO> child;
private EventTO parent;
public Builder() {
}
public Builder setFloatVal(Double floatVal) {
this.floatVal = floatVal;
return this;
}
public Builder setBooleanVal(Boolean booleanVal) {
this.booleanVal = booleanVal;
return this;
}
public Builder setIntVal(Integer intVal) {
this.intVal = intVal;
return this;
}
public Builder setStringVal(String stringVal) {
this.stringVal = stringVal;
return this;
}
public Builder setChild(Collection<EventPropertyTO> child) {
this.child = child;
return this;
}
public Builder setParent(EventTO parent) {
this.parent = parent;
return this;
}
public EventPropertyTO build() {
return new EventPropertyTO(floatVal, booleanVal, intVal, stringVal, child, parent);
}
}
}
......@@ -45,13 +45,13 @@ public class AcceptTopicSuggestionInput {
public String toString() {
StringJoiner joiner = new StringJoiner(", ", "{ ", " }");
if (clientMutationId != null) {
joiner.add("clientMutationId: \"" + clientMutationId + "\"");
joiner.add("clientMutationId: \"" + com.kobylynskyi.graphql.codegen.model.graphql.GraphQLRequestSerializer.escapeJsonString(clientMutationId) + "\"");
}
if (name != null) {
joiner.add("name: \"" + name + "\"");
joiner.add("name: \"" + com.kobylynskyi.graphql.codegen.model.graphql.GraphQLRequestSerializer.escapeJsonString(name) + "\"");
}
if (repositoryId != null) {
joiner.add("repositoryId: \"" + repositoryId + "\"");
joiner.add("repositoryId: \"" + com.kobylynskyi.graphql.codegen.model.graphql.GraphQLRequestSerializer.escapeJsonString(repositoryId) + "\"");
}
return joiner.toString();
}
......
package com.github.graphql;
import java.util.*;
import com.kobylynskyi.graphql.codegen.model.graphql.*;
public class CodeOfConductResponseProjection implements com.kobylynskyi.graphql.codegen.model.request.GraphQLResponseProjection {
public class CodeOfConductResponseProjection implements GraphQLResponseProjection {
private Map<String, Object> fields = new LinkedHashMap<>();
......
package com.github.graphql;
import java.util.*;
import com.kobylynskyi.graphql.codegen.model.graphql.*;
public class EventPropertyResponseProjection implements com.kobylynskyi.graphql.codegen.model.request.GraphQLResponseProjection {
public class EventPropertyResponseProjection implements GraphQLResponseProjection {
private Map<String, Object> fields = new LinkedHashMap<>();
......
package com.github.graphql;
import java.util.*;
import com.kobylynskyi.graphql.codegen.model.graphql.*;
public class EventResponseProjection implements com.kobylynskyi.graphql.codegen.model.request.GraphQLResponseProjection {
public class EventResponseProjection implements GraphQLResponseProjection {
private Map<String, Object> fields = new LinkedHashMap<>();
......
package com.github.graphql;
import java.util.*;
import graphql.language.*;
import com.kobylynskyi.graphql.codegen.model.graphql.*;
public class EventsByCategoryAndStatusQueryRequest implements com.kobylynskyi.graphql.codegen.model.request.GraphQLOperationRequest {
public class EventsByCategoryAndStatusQueryRequest implements GraphQLOperationRequest {
private static final OperationDefinition.Operation OPERATION_TYPE = OperationDefinition.Operation.QUERY;
private static final GraphQLOperation OPERATION_TYPE = GraphQLOperation.QUERY;
private static final String OPERATION_NAME = "eventsByCategoryAndStatus";
private Map<String, Object> input = new LinkedHashMap<>();
......@@ -22,7 +22,7 @@ public class EventsByCategoryAndStatusQueryRequest implements com.kobylynskyi.gr
}
@Override
public OperationDefinition.Operation getOperationType() {
public GraphQLOperation getOperationType() {
return OPERATION_TYPE;
}
......
package com.github.graphql;
import java.util.*;
import graphql.language.*;
import com.kobylynskyi.graphql.codegen.model.graphql.*;
public class EventsByCategoryAndStatusQueryRequest implements com.kobylynskyi.graphql.codegen.model.request.GraphQLOperationRequest {
public class EventsByCategoryAndStatusQueryRequest implements GraphQLOperationRequest {
private static final OperationDefinition.Operation OPERATION_TYPE = OperationDefinition.Operation.QUERY;
private static final GraphQLOperation OPERATION_TYPE = GraphQLOperation.QUERY;
private static final String OPERATION_NAME = "eventsByCategoryAndStatus";
private Map<String, Object> input = new LinkedHashMap<>();
......@@ -22,7 +22,7 @@ public class EventsByCategoryAndStatusQueryRequest implements com.kobylynskyi.gr
}
@Override
public OperationDefinition.Operation getOperationType() {
public GraphQLOperation getOperationType() {
return OPERATION_TYPE;
}
......
package com.github.graphql;
import java.util.*;
import graphql.language.*;
import com.kobylynskyi.graphql.codegen.model.graphql.*;
public class EventsByIdsQueryRequest implements com.kobylynskyi.graphql.codegen.model.request.GraphQLOperationRequest {
public class EventsByIdsQueryRequest implements GraphQLOperationRequest {
private static final OperationDefinition.Operation OPERATION_TYPE = OperationDefinition.Operation.QUERY;
private static final GraphQLOperation OPERATION_TYPE = GraphQLOperation.QUERY;
private static final String OPERATION_NAME = "eventsByIds";
private Map<String, Object> input = new LinkedHashMap<>();
......@@ -18,7 +18,7 @@ public class EventsByIdsQueryRequest implements com.kobylynskyi.graphql.codegen.
}
@Override
public OperationDefinition.Operation getOperationType() {
public GraphQLOperation getOperationType() {
return OPERATION_TYPE;
}
......
package com.github.graphql;
import java.util.*;
import graphql.language.*;
import com.kobylynskyi.graphql.codegen.model.graphql.*;
public class UpdateRepositoryMutationRequest implements com.kobylynskyi.graphql.codegen.model.request.GraphQLOperationRequest {
public class UpdateRepositoryMutationRequest implements GraphQLOperationRequest {
private static final OperationDefinition.Operation OPERATION_TYPE = OperationDefinition.Operation.MUTATION;
private static final GraphQLOperation OPERATION_TYPE = GraphQLOperation.MUTATION;
private static final String OPERATION_NAME = "updateRepository";
private Map<String, Object> input = new LinkedHashMap<>();
......@@ -18,7 +18,7 @@ public class UpdateRepositoryMutationRequest implements com.kobylynskyi.graphql.
}
@Override
public OperationDefinition.Operation getOperationType() {
public GraphQLOperation getOperationType() {
return OPERATION_TYPE;
}
......
package com.github.graphql;
import java.util.*;
import graphql.language.*;
import com.kobylynskyi.graphql.codegen.model.graphql.*;
public class VersionQueryRequest implements com.kobylynskyi.graphql.codegen.model.request.GraphQLOperationRequest {
public class VersionQueryRequest implements GraphQLOperationRequest {
private static final OperationDefinition.Operation OPERATION_TYPE = OperationDefinition.Operation.QUERY;
private static final GraphQLOperation OPERATION_TYPE = GraphQLOperation.QUERY;
private static final String OPERATION_NAME = "version";
private Map<String, Object> input = new LinkedHashMap<>();
......@@ -14,7 +14,7 @@ public class VersionQueryRequest implements com.kobylynskyi.graphql.codegen.mode
}
@Override
public OperationDefinition.Operation getOperationType() {
public GraphQLOperation getOperationType() {
return OPERATION_TYPE;
}
......
query {
eventsByCategoryAndStatus(categoryId: "categoryIdValue1", status: OPEN){
id
active
properties {
floatVal
child {
intVal
parent {
id
}
}
booleanVal
}
status
}
}
\ No newline at end of file
query {
eventsByIds(ids: [ "4", "5", "6" ]){
id
}
}
\ No newline at end of file
mutation {
updateIssue(input: {
floatVal: 1.23,
booleanVal: false,
intVal: 42,
stringVal: "my-default",
enumVal: OPEN,
intList: [1, 2, 3],
intListEmptyDefault: []
}){
clientMutationId
issue {
activeLockReason
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册