Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/Distelli/graphql-apigen

Generate Java APIs with GraphQL Schemas
https://github.com/Distelli/graphql-apigen

Last synced: 26 days ago
JSON representation

Generate Java APIs with GraphQL Schemas

Awesome Lists containing this project

README

        

# graphql-apigen

Generate Java APIs with GraphQL Schemas in order to facilitate "schema first" development.

### Posts Example

Create a file to define your schema. In this example we are creating the `schema/posts.graphql` file:

```graphql
type Author @java(package:"com.distelli.posts") {
id: Int! # the ! means that every author object _must_ have an id
firstName: String
lastName: String
posts: [Post] # the list of Posts by this author
}

type Post @java(package:"com.distelli.posts") {
id: Int!
title: String
author: Author
votes: Int
}

# the schema allows the following query:
type QueryPosts @java(package:"com.distelli.posts") {
posts: [Post]
}

input InputPost @java(package:"com.distelli.posts") {
title: String
authorId: Int!
}

# this schema allows the following mutation:
type MutatePosts @java(package:"com.distelli.posts") {
createPost(post:InputPost): Post
upvotePost(
postId: Int!
): Post
}
```

Notice that we annotate the types with a java package name. The above schema
will generate the following java **interfaces** in `target/generated-sources/apigen`
(in the `com.distelli.posts` package):

* `Author` and `Author.Resolver`
* `Post` and `Post.Resolver`
* `QueryPosts`
* `InputPost`
* `MutatePosts`

The `*.Resolver` interfaces are only generated if their is a field named "id". This
interface may be implemented to resolve a `*.Unresolved` (only the id field defined)
into a fully resolved implementation (all fields defined). All interface methods
have "default" implementations that return null.

Each of these interfaces also have a default inner class named `*.Builder` and
`*.Impl`. The `*.Builder` will have a no-argument constructor and a constructor
that takes the parent interface as an argument. The `*.Builder` will also have a
method `with()` for each no-arg field which returns the
builder and a `build()` method that creates a `*.Impl`.

Any field that takes arguments will cause a `*.Args` interface to be
generated with methods for each input field.

Any field that does NOT take arguments will generate method names prefixed with
"get".

Finally, the above schema also generates a Guice module `PostsModule` which adds to
a `Map` multibinder (the name "PostsModule" comes from the
filename which defines the schema). See below for information about using Spring for
Dependency Injection.

Putting this all together, we can implement the `QueryPosts` implementation as such:

```java
public class QueryPostsImpl implements QueryPosts {
private Map posts;
public QueryPostsImpl(Map posts) {
this.posts = posts;
}
@Override
public List getPosts() {
return new ArrayList<>(posts.values());
}
}
```

...and the `MutatePosts` implementation as such:

```java
public class MutatePostsImpl implements MutatePosts {
private AtomicInteger nextPostId = new AtomicInteger(1);
private Map posts;
public MutatePostsImpl(Map posts) {
this.posts = posts;
}
@Override
public Post createPost(MutatePosts.CreatePostArgs args) {
InputPost req = args.getPost();
Post.Builder postBuilder = new Post.Builder()
.withTitle(req.getTitle())
.withAuthor(new Author.Unresolved(req.getAuthorId()));
Post post;
synchronized ( posts ) {
Integer id = nextPostId.incrementAndGet();
post = postBuilder.withId(id).build();
posts.put(id, post);
}
return post;
}

@Override
public Post upvotePost(MutatePosts.UpvotePostArgs args) {
synchronized ( posts ) {
Post post = posts.get(args.getPostId());
if ( null == post ) {
throw new NoSuchEntityException("PostId="+args.getPostId());
}
Post upvoted = new Post.Builder(post)
.withVotes(post.getVotes()+1)
.build();
posts.put(args.getPostId(), upvoted);
return upvoted;
}
}
}
```

...and the `Author.Resolver` interface as such:

```java
public class AuthorResolver implements Author.Resolver {
private Map authors;
public AuthorResolver(Map authors) {
this.authors = authors;
}
@Override
public List resolve(List unresolvedList) {
List result = new ArrayList<>();
for ( Author unresolved : unresolvedList ) {
// In a real app we would check if it is instanceof Author.Unresolved
result.add(authors.get(unresolved.getId()));
}
return result;
}
}
```

...and the `Post.Resolver` interface as such:

```java
public class PostResolver implements Post.Resolver {
private Map posts;
public PostResolver(Map posts) {
this.posts = posts;
}
@Override
public List resolve(List unresolvedList) {
List result = new ArrayList<>();
for ( Post unresolved : unresolvedList ) {
if ( null == unresolved ) {
result.add(null);
} else {
result.add(posts.get(unresolved.getId()));
}
}
return result;
}
}
```

...and you can use Guice to wire it all together as such (see below on
using this from Spring):

```java
public class MainModule implements AbstractModule {
@Override
protected void configure() {
// Create the "data" used by the implementations:
Map posts = new LinkedHashMap<>();
Map authors = new LinkedHashMap<>();
// Install the generated module:
install(new PostsModule());
// Declare our implementations:
bind(Author.Resolver.class)
.toInstance(new AuthorResolver(authors));
bind(Post.Resolver.class)
.toInstance(new PostResolver(posts));
bind(MutatePosts.class)
.toInstance(new MutatePostsImpl(posts));
bind(QueryPosts.class)
.toInstance(new QueryPostsImpl(posts));
}
}
```

...and to use it:

```java
public class GraphQLServlet extends HttpServlet {
private static ObjectMapper OM = new ObjectMapper();
private GraphQL graphQL;
@Inject
protected void GraphQLServlet(Map types) {
GraphQLSchema schema = GraphQLSchema.newSchema()
.query((GraphQLObjectType)types.get("QueryPosts"))
.mutation((GraphQLObjectType)types.get("MutatePosts"))
.build(new HashSet<>(types.values()));
graphQL = new GraphQL(schema, new BatchedExecutionStrategy());
}
protected void service(HttpServletRequest req, HttpServletResponse resp) {
ExecutionResult result = graphQL.execute(req.getParameter("query"));
OM.writeValue(resp.getOutputStream(), result);
}
}
```

This example is also a unit test which can be found
[here](apigen/src/test/projects/posts/src/test/java/com/disteli/posts/PostsTest.java)

### Using Spring instead of Guice

If you want to use Spring to wire the components together instead of Guice, you need to
instruct Spring to include the generated code in a package-scan. Spring will find the `@Named`
annotated components and will inject any dependencies (the type resolvers you implement, etc)

For example, if your code was generated into the package `com.distelli.posts`, the spring
configuration would look like this:

```java
@ComponentScan("com.distelli.posts")
@Configuration
public class MyAppConfig {
...
}
```

To generate a mapping similar to the guice code above, you can add this to your spring
configuration:

```java
@Bean
public Map graphqlTypeMap(List> typeList) {
return typeList.stream().map(Provider::get).collect(Collectors.toMap(GraphQLType::getName, Function.identity()));
}
```

This will take any GraphQLTypes and generate a map of their string name to their implementation.

### Getting started

#### How to use the latest release with Maven

Generate the code with the following maven:

```xml

...

4.0.0



...

com.distelli.graphql
graphql-apigen
${apigen.version}


com.example.my.MyGuiceModule

com.example.my

schema/folder

output/folder



java-apigen

apigen






...


com.distelli.graphql
graphql-apigen-deps
${apigen.version}



com.google.inject
guice
4.0


com.google.inject.extensions
guice-multibindings
4.0

```

Be sure to replace the values above with the correct values (and remove unnecessary configuration properties if the
defaults are satisfactory).

### Customizing the Output

You can customize the generated Java source by copying the [graphql-apigen.stg](apigen/src/main/resources/graphql-apigen.stg)
file to the base directory of your project and making any necessary changes. The plugin will automatically use it
instead of the one distributed with the library. The template uses the [StringTemplate](https://github.com/antlr/stringtemplate4/blob/master/doc/index.md)
template language. The model used for the template is defined in [STModel.java](apigen/src/main/java/com/distelli/graphql/apigen/STModel.java).