├── .gitignore ├── apigen ├── src │ ├── test │ │ ├── projects │ │ │ ├── basic │ │ │ │ ├── extends │ │ │ │ │ ├── schema │ │ │ │ │ │ └── extends.graphql │ │ │ │ │ ├── pom.xml │ │ │ │ │ └── src │ │ │ │ │ │ └── test │ │ │ │ │ │ └── java │ │ │ │ │ │ └── test │ │ │ │ │ │ └── TestExtends.java │ │ │ │ ├── base │ │ │ │ │ ├── schema │ │ │ │ │ │ └── base.graphqls │ │ │ │ │ └── pom.xml │ │ │ │ └── pom.xml │ │ │ ├── starwars │ │ │ │ ├── schema │ │ │ │ │ └── starwars.graphql │ │ │ │ └── pom.xml │ │ │ └── posts │ │ │ │ ├── schema │ │ │ │ └── posts.graphql │ │ │ │ ├── pom.xml │ │ │ │ └── src │ │ │ │ └── test │ │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── disteli │ │ │ │ └── posts │ │ │ │ └── PostsTest.java │ │ └── java │ │ │ └── com │ │ │ └── distelli │ │ │ └── graphql │ │ │ └── apigen │ │ │ └── ApiGenMojoTest.java │ └── main │ │ ├── resources │ │ ├── META-INF │ │ │ └── m2e │ │ │ │ └── lifecycle-mapping-metadata.xml │ │ └── graphql-apigen.stg │ │ └── java │ │ └── com │ │ └── distelli │ │ └── graphql │ │ └── apigen │ │ ├── TypeEntry.java │ │ ├── ApiGenMojo.java │ │ ├── ApiGen.java │ │ └── STModel.java └── pom.xml ├── apigen-deps ├── src │ └── main │ │ └── java │ │ └── com │ │ └── distelli │ │ └── graphql │ │ ├── Resolver.java │ │ ├── ResolveDataFetchingEnvironment.java │ │ ├── ResolverDataFetcher.java │ │ └── MethodDataFetcher.java └── pom.xml ├── CONTRIBUTING.md ├── Makefile ├── pom.xml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target/** 2 | */target/** 3 | 4 | .idea/ 5 | *.iml 6 | -------------------------------------------------------------------------------- /apigen/src/test/projects/basic/extends/schema/extends.graphql: -------------------------------------------------------------------------------- 1 | 2 | type Extends @java(package:"test") { 3 | base: Base, 4 | alpha: Alphabet, 5 | } 6 | -------------------------------------------------------------------------------- /apigen/src/test/projects/basic/base/schema/base.graphqls: -------------------------------------------------------------------------------- 1 | 2 | type Base @java(package:"test") { 3 | base: String 4 | } 5 | 6 | enum Alphabet @java(package:"test") { 7 | A B C D E F G H I J 8 | } -------------------------------------------------------------------------------- /apigen-deps/src/main/java/com/distelli/graphql/Resolver.java: -------------------------------------------------------------------------------- 1 | package com.distelli.graphql; 2 | 3 | import java.util.List; 4 | 5 | public interface Resolver { 6 | public List resolve(List unresolved); 7 | } 8 | -------------------------------------------------------------------------------- /apigen-deps/src/main/java/com/distelli/graphql/ResolveDataFetchingEnvironment.java: -------------------------------------------------------------------------------- 1 | package com.distelli.graphql; 2 | 3 | import graphql.schema.DataFetchingEnvironment; 4 | 5 | public interface ResolveDataFetchingEnvironment { 6 | public T resolve(DataFetchingEnvironment env); 7 | } 8 | -------------------------------------------------------------------------------- /apigen/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.distelli.graphql 6 | graphql-apigen 7 | [1.0,) 8 | 9 | apigen 10 | 11 | 12 | 13 | 14 | true 15 | true 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /apigen/src/test/projects/starwars/schema/starwars.graphql: -------------------------------------------------------------------------------- 1 | enum Episode @java(package:"org.distelli.starwars") { 2 | NEWHOPE, 3 | EMPIRE, 4 | JEDI 5 | } 6 | 7 | interface Character @java(package:"org.distelli.starwars") { 8 | id: String!, 9 | name: String, 10 | friends: [Character], 11 | appearsIn: [Episode], 12 | } 13 | 14 | type Human implements Character @java(package:"org.distelli.starwars") { 15 | id: String!, 16 | name: String, 17 | friends: [Character], 18 | appearsIn: [Episode], 19 | homePlanet: String, 20 | } 21 | 22 | type Droid implements Character @java(package:"org.distelli.starwars") { 23 | id: String!, 24 | name: String, 25 | friends: [Character], 26 | appearsIn: [Episode], 27 | primaryFunction: String, 28 | } 29 | -------------------------------------------------------------------------------- /apigen/src/test/projects/posts/schema/posts.graphql: -------------------------------------------------------------------------------- 1 | type Author @java(package:"com.distelli.posts") { 2 | id: Int! # the ! means that every author object _must_ have an id 3 | firstName: String 4 | lastName: String 5 | posts: [Post] # the list of Posts by this author 6 | } 7 | 8 | type Post @java(package:"com.distelli.posts") { 9 | id: Int! 10 | title: String 11 | author: Author 12 | votes: Int 13 | } 14 | 15 | # the schema allows the following query: 16 | type QueryPosts @java(package:"com.distelli.posts") { 17 | posts: [Post] 18 | } 19 | 20 | input InputPost @java(package:"com.distelli.posts") { 21 | title: String 22 | authorId: Int! 23 | } 24 | 25 | # this schema allows the following mutation: 26 | type MutatePosts @java(package:"com.distelli.posts") { 27 | createPost(post:InputPost): Post 28 | upvotePost( 29 | postId: Int! 30 | ): Post 31 | } 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Making Changes 4 | 5 | * Fork the repository on GitHub 6 | * Create a topic branch from where you want to base your work. 7 | * This is usually the master branch. 8 | * Only target release branches if you are certain your fix must be on that 9 | branch. 10 | * To quickly create a topic branch based on master; `git checkout -b 11 | fix/master/my_contribution master`. Please avoid working directly on the 12 | `master` branch. 13 | * Make commits of logical units. 14 | * Check for unnecessary whitespace with `git diff --check` before committing. 15 | * Make sure you have added the necessary tests for your changes. 16 | * Run _all_ the tests to assure nothing else was accidentally broken. 17 | * Be sure to update the documentation. 18 | 19 | ## Submitting Changes 20 | 21 | * Sign the [Contributor License Agreement](https://www.clahub.com/agreements/Distelli/graphql-apigen). 22 | * Push your changes to a topic branch in your fork of the repository. 23 | * Submit a pull request to the repository in the Distelli organization. 24 | 25 | ## Revert Policy 26 | 27 | * We reserve the right to revert commits for any reason, although this is rare. 28 | 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGE_NAME=graphql-apigen-pom 2 | SHELL := /bin/bash 3 | .SILENT: 4 | .PHONY: git-pull-needed git-is-clean git-is-master 5 | all: 6 | mvn -q -U dependency:build-classpath compile -DincludeScope=runtime -Dmdep.outputFile=target/.classpath -Dmaven.compiler.debug=false 7 | 8 | install: 9 | mvn -q install 10 | 11 | test: 12 | mvn -q -Dsurefire.useFile=false test 13 | 14 | clean: 15 | mvn -q clean 16 | 17 | package: 18 | mvn -q -DincludeScope=runtime dependency:copy-dependencies package 19 | 20 | show-deps: 21 | mvn dependency:tree 22 | 23 | git-pull-needed: 24 | git remote update 25 | [ $$(git rev-parse '@{u}') = $$(git merge-base '@' '@{u}') ] 26 | 27 | git-is-clean: 28 | git diff-index --quiet HEAD -- 29 | 30 | git-is-master: 31 | [ master = "$$(git rev-parse --abbrev-ref HEAD)" ] 32 | 33 | NEXT_SNAPSHOT=$$(echo $(NEW_VERSION) | awk -F. '{OFS=".";$$NF=$$(NF)+1;print $$0}')-SNAPSHOT 34 | 35 | publish: git-is-clean git-is-master git-pull-needed 36 | if [ -z "$(NEW_VERSION)" ]; then echo 'Please run `make publish NEW_VERSION=1.1`' 1>&2; false; fi 37 | mvn versions:set -DgenerateBackupPoms=false -DnewVersion=$(NEW_VERSION) && \ 38 | sed -i '' 's!.*!'$(NEW_VERSION)'!' apigen/src/test/projects/*/pom.xml && \ 39 | git commit -am '[skip ci][release:prepare] prepare release $(PACKAGE_NAME)-$(NEW_VERSION)' && \ 40 | git tag -m 'Preparing new release $(PACKAGE_NAME)-$(NEW_VERSION)' -a '$(PACKAGE_NAME)-$(NEW_VERSION)' && \ 41 | mvn clean test deploy -Prelease && \ 42 | mvn versions:set -DgenerateBackupPoms=false -DnewVersion=$(NEXT_SNAPSHOT) && \ 43 | sed -i '' 's!.*!'$(NEXT_SNAPSHOT)'!' apigen/src/test/projects/*/pom.xml && \ 44 | git commit -am '[skip ci][release:perform] prepare for next development iteration' && \ 45 | git push --follow-tags 46 | -------------------------------------------------------------------------------- /apigen/src/test/java/com/distelli/graphql/apigen/ApiGenMojoTest.java: -------------------------------------------------------------------------------- 1 | package com.distelli.graphql.apigen; 2 | 3 | import io.takari.maven.testing.executor.MavenExecutionResult; 4 | import io.takari.maven.testing.TestResources; 5 | import io.takari.maven.testing.TestMavenRuntime; 6 | import org.junit.Rule; 7 | import static org.junit.Assert.*; 8 | import org.junit.Test; 9 | import java.io.File; 10 | import io.takari.maven.testing.executor.junit.MavenJUnitTestRunner; 11 | import org.junit.runner.RunWith; 12 | import io.takari.maven.testing.executor.MavenVersions; 13 | import io.takari.maven.testing.executor.MavenRuntime; 14 | import io.takari.maven.testing.executor.MavenRuntime.MavenRuntimeBuilder; 15 | 16 | @RunWith(MavenJUnitTestRunner.class) 17 | @MavenVersions({"3.2.5"}) 18 | public class ApiGenMojoTest { 19 | @Rule 20 | public TestResources resources = new TestResources(); 21 | 22 | public MavenRuntime mavenRuntime; 23 | 24 | public ApiGenMojoTest(MavenRuntimeBuilder builder) throws Exception { 25 | mavenRuntime = builder.build(); 26 | } 27 | 28 | @Test 29 | public void testBasic() throws Exception { 30 | File basedir = resources.getBasedir("basic"); 31 | MavenExecutionResult result = mavenRuntime 32 | .forProject(basedir) 33 | .execute("clean", "test"); 34 | 35 | result.assertErrorFreeLog(); 36 | } 37 | 38 | @Test 39 | public void testStarwars() throws Exception { 40 | File basedir = resources.getBasedir("starwars"); 41 | MavenExecutionResult result = mavenRuntime 42 | .forProject(basedir) 43 | .execute("clean", "compile"); 44 | 45 | result.assertErrorFreeLog(); 46 | } 47 | 48 | @Test 49 | public void testPosts() throws Exception { 50 | File basedir = resources.getBasedir("posts"); 51 | MavenExecutionResult result = mavenRuntime 52 | .forProject(basedir) 53 | .execute("clean", "test"); 54 | 55 | result.assertErrorFreeLog(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /apigen/src/test/projects/basic/base/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | test 5 | test-base 6 | 1.0-SNAPSHOT 7 | jar 8 | 9 | test-base 10 | 11 | 12 | test 13 | test-pom 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | 20 | org.apache.maven.plugins 21 | maven-compiler-plugin 22 | 23 | 24 | com.distelli.graphql 25 | graphql-apigen 26 | 27 | test.BaseModule 28 | 29 | 30 | 31 | io.takari.maven.plugins 32 | takari-lifecycle-plugin 33 | 34 | 35 | 36 | 37 | 38 | 39 | io.takari.maven.plugins 40 | takari-plugin-integration-testing 41 | 2.9.0 42 | pom 43 | test 44 | 45 | 46 | com.distelli.graphql 47 | graphql-apigen-deps 48 | ${apigen.version} 49 | 50 | 51 | com.google.inject 52 | guice 53 | 4.1.0 54 | 55 | 56 | com.google.inject.extensions 57 | guice-multibindings 58 | 4.1.0 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /apigen/src/test/projects/basic/extends/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | test 5 | test-extends 6 | 1.0-SNAPSHOT 7 | jar 8 | 9 | test-extends 10 | 11 | 12 | test 13 | test-pom 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | 20 | org.apache.maven.plugins 21 | maven-compiler-plugin 22 | 23 | 24 | com.distelli.graphql 25 | graphql-apigen 26 | 27 | test.ExtendsModule 28 | 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-surefire-plugin 33 | 34 | 35 | io.takari.maven.plugins 36 | takari-lifecycle-plugin 37 | 38 | 39 | 40 | 41 | 42 | 43 | io.takari.maven.plugins 44 | takari-plugin-integration-testing 45 | 2.9.0 46 | pom 47 | test 48 | 49 | 50 | test 51 | test-base 52 | 1.0-SNAPSHOT 53 | 54 | 55 | com.distelli.graphql 56 | graphql-apigen-deps 57 | ${apigen.version} 58 | 59 | 60 | com.google.inject 61 | guice 62 | 4.1.0 63 | 64 | 65 | com.google.inject.extensions 66 | guice-multibindings 67 | 4.1.0 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /apigen/src/test/projects/basic/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | test 5 | test-pom 6 | 1.0-SNAPSHOT 7 | pom 8 | 9 | test 10 | 11 | 12 | base 13 | extends 14 | 15 | 16 | 17 | 5.0.1-SNAPSHOT 18 | 19 | 20 | 21 | 22 | 23 | 24 | org.apache.maven.plugins 25 | maven-compiler-plugin 26 | 3.2 27 | 28 | 1.8 29 | 1.8 30 | 31 | 32 | 33 | org.apache.maven.plugins 34 | maven-surefire-plugin 35 | 2.19.1 36 | 37 | false 38 | brief 39 | false 40 | true 41 | 42 | 43 | 44 | com.distelli.graphql 45 | graphql-apigen 46 | ${apigen.version} 47 | 48 | 49 | crap 50 | 51 | apigen 52 | 53 | 54 | 55 | 56 | 57 | io.takari.maven.plugins 58 | takari-lifecycle-plugin 59 | 1.12.2 60 | 61 | 62 | testProperties 63 | process-test-resources 64 | 65 | testProperties 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /apigen/src/test/projects/starwars/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | test 5 | test-starwars 6 | 1.0-SNAPSHOT 7 | jar 8 | 9 | test-starwars 10 | 11 | 12 | 5.0.1-SNAPSHOT 13 | 14 | 15 | 16 | 17 | 18 | org.apache.maven.plugins 19 | maven-compiler-plugin 20 | 3.6.0 21 | 22 | 1.8 23 | 1.8 24 | 25 | 26 | 27 | org.apache.maven.plugins 28 | maven-resources-plugin 29 | 3.0.1 30 | 31 | 32 | com.distelli.graphql 33 | graphql-apigen 34 | ${apigen.version} 35 | 36 | 37 | crap 38 | 39 | apigen 40 | 41 | 42 | 43 | 44 | 45 | io.takari.maven.plugins 46 | takari-lifecycle-plugin 47 | 1.12.2 48 | 49 | 50 | testProperties 51 | process-test-resources 52 | 53 | testProperties 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | io.takari.maven.plugins 64 | takari-plugin-integration-testing 65 | 2.9.0 66 | pom 67 | test 68 | 69 | 70 | com.distelli.graphql 71 | graphql-apigen-deps 72 | ${apigen.version} 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /apigen/src/test/projects/basic/extends/src/test/java/test/TestExtends.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import org.junit.Test; 4 | import org.junit.Before; 5 | import static org.junit.Assert.*; 6 | import com.google.inject.Guice; 7 | import com.google.inject.AbstractModule; 8 | import com.google.inject.Injector; 9 | import com.google.inject.Provides; 10 | import test.ExtendsModule; 11 | import test.BaseModule; 12 | import test.Alphabet; 13 | import javax.inject.Inject; 14 | import java.util.Map; 15 | import java.util.HashSet; 16 | import graphql.schema.GraphQLSchema; 17 | import graphql.schema.GraphQLType; 18 | import graphql.schema.GraphQLObjectType; 19 | import graphql.GraphQL; 20 | import graphql.ExecutionResult; 21 | import graphql.execution.batched.BatchedExecutionStrategy; 22 | import java.util.Optional; 23 | 24 | public class TestExtends { 25 | private static Injector INJECTOR = Guice.createInjector( 26 | new ExtendsModule(), 27 | new BaseModule(), 28 | new AbstractModule() { 29 | @Override 30 | protected void configure() { 31 | Extends instance = new Extends.Builder() 32 | .withAlpha(Alphabet.B) 33 | .build(); 34 | bind(Extends.class) 35 | .toInstance(instance); 36 | } 37 | @Provides 38 | protected GraphQL provideGraphQL(Map types) { 39 | GraphQLSchema schema = GraphQLSchema.newSchema() 40 | .query((GraphQLObjectType)types.get("Extends")) 41 | .build(new HashSet<>(types.values())); 42 | return new GraphQL(schema, new BatchedExecutionStrategy()); 43 | } 44 | }); 45 | @Inject 46 | private GraphQL _graphQL; 47 | 48 | @Before 49 | public void before() { 50 | INJECTOR.injectMembers(this); 51 | } 52 | 53 | @Test 54 | public void test() { 55 | Extends ext = new Extends.Builder() 56 | .withBase(new Base.Builder() 57 | .withBase("string") 58 | .build()) 59 | .build(); 60 | assertEquals("string", ext.getBase().getBase()); 61 | 62 | // Not very exciting, but shows how to copy: 63 | Extends ext2 = new Extends.Builder(ext) 64 | .build(); 65 | assertEquals("string", ext2.getBase().getBase()); 66 | 67 | assertNotSame(ext, ext2); 68 | 69 | // Verify we can inject implementations... 70 | Map result = (Map)_graphQL.execute("{alpha}").getData(); 71 | assertEquals("B", ""+result.get("alpha")); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /apigen-deps/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 3.0 5 | 6 | 7 | com.distelli.graphql 8 | graphql-apigen-deps 9 | 5.0.1-SNAPSHOT 10 | Dependencies for generated code 11 | jar 12 | 13 | 14 | com.distelli.graphql 15 | graphql-apigen-pom 16 | 5.0.1-SNAPSHOT 17 | 18 | 19 | https://github.com/distelli/graphql-apigen 20 | 21 | 22 | 23 | org.apache.maven.plugins 24 | maven-surefire-plugin 25 | 26 | 27 | org.apache.maven.plugins 28 | maven-compiler-plugin 29 | 30 | 31 | org.jacoco 32 | jacoco-maven-plugin 33 | 34 | 35 | org.apache.maven.plugins 36 | maven-gpg-plugin 37 | 38 | 39 | org.sonatype.plugins 40 | nexus-staging-maven-plugin 41 | 42 | 43 | org.apache.maven.plugins 44 | maven-source-plugin 45 | 46 | 47 | org.apache.maven.plugins 48 | maven-javadoc-plugin 49 | 50 | 51 | org.apache.maven.plugins 52 | maven-plugin-plugin 53 | 54 | 55 | 56 | 57 | 58 | 59 | com.graphql-java 60 | graphql-java 61 | ${graphql.version} 62 | 63 | 64 | javax.inject 65 | javax.inject 66 | 1 67 | 68 | 69 | com.google.inject 70 | guice 71 | 4.0 72 | true 73 | 74 | 75 | com.google.inject.extensions 76 | guice-multibindings 77 | 4.0 78 | true 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /apigen/src/test/projects/posts/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | test 5 | test-posts 6 | 1.0-SNAPSHOT 7 | jar 8 | 9 | test-posts 10 | 11 | 12 | 5.0.1-SNAPSHOT 13 | 14 | 15 | 16 | 17 | 18 | org.apache.maven.plugins 19 | maven-compiler-plugin 20 | 3.6.0 21 | 22 | 1.8 23 | 1.8 24 | 25 | 26 | 27 | org.apache.maven.plugins 28 | maven-resources-plugin 29 | 3.0.1 30 | 31 | 32 | com.distelli.graphql 33 | graphql-apigen 34 | ${apigen.version} 35 | 36 | com.distelli.posts.PostsModule 37 | 38 | 39 | 40 | crap 41 | 42 | apigen 43 | 44 | 45 | 46 | 47 | 48 | io.takari.maven.plugins 49 | takari-lifecycle-plugin 50 | 1.12.2 51 | 52 | 53 | testProperties 54 | process-test-resources 55 | 56 | testProperties 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | io.takari.maven.plugins 67 | takari-plugin-integration-testing 68 | 2.9.0 69 | pom 70 | test 71 | 72 | 73 | com.distelli.graphql 74 | graphql-apigen-deps 75 | ${apigen.version} 76 | 77 | 78 | com.fasterxml.jackson.core 79 | jackson-databind 80 | 2.12.6.1 81 | test 82 | 83 | 84 | com.google.inject 85 | guice 86 | 4.0 87 | 88 | 89 | com.google.inject.extensions 90 | guice-multibindings 91 | 4.0 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /apigen/src/main/java/com/distelli/graphql/apigen/TypeEntry.java: -------------------------------------------------------------------------------- 1 | package com.distelli.graphql.apigen; 2 | 3 | import java.util.Collections; 4 | import graphql.language.ObjectTypeDefinition; 5 | import graphql.language.TypeDefinition; 6 | import graphql.language.Definition; 7 | import graphql.language.Argument; 8 | import graphql.language.Directive; 9 | import graphql.language.InterfaceTypeDefinition; 10 | import graphql.language.EnumTypeDefinition; 11 | import graphql.language.ScalarTypeDefinition; 12 | import graphql.language.UnionTypeDefinition; 13 | import graphql.language.InputObjectTypeDefinition; 14 | import graphql.language.SchemaDefinition; 15 | import graphql.Scalars; 16 | import java.util.List; 17 | import java.net.URL; 18 | 19 | public class TypeEntry { 20 | private URL source; 21 | private Definition definition; 22 | private String packageName; 23 | 24 | public TypeEntry(Definition definition, URL source, String defaultPackageName) { 25 | this.source = source; 26 | this.definition = definition; 27 | this.packageName = getPackageName(getDirectives(definition), defaultPackageName); 28 | } 29 | 30 | public URL getSource() { 31 | return source; 32 | } 33 | 34 | // Return nice formatted string for source location: 35 | public String getSourceLocation() { 36 | return source + ":[" + definition.getSourceLocation().getLine() + 37 | ", " + definition.getSourceLocation().getColumn() + "]"; 38 | } 39 | 40 | public String getPackageName() { 41 | return packageName; 42 | } 43 | 44 | public String getName() { 45 | if ( definition instanceof TypeDefinition ) { 46 | return ((TypeDefinition)definition).getName(); 47 | } 48 | return ""; 49 | } 50 | 51 | public Definition getDefinition() { 52 | return definition; 53 | } 54 | 55 | public boolean hasIdField() { 56 | if ( definition instanceof ObjectTypeDefinition) { 57 | return ((ObjectTypeDefinition)definition).getFieldDefinitions() 58 | .stream() 59 | .anyMatch((field) -> "id".equals(field.getName())); 60 | } 61 | return false; 62 | } 63 | 64 | private static List getDirectives(Definition def) { 65 | if ( def instanceof ObjectTypeDefinition ) { 66 | return ((ObjectTypeDefinition)def).getDirectives(); 67 | } if ( def instanceof InterfaceTypeDefinition ) { 68 | return ((InterfaceTypeDefinition)def).getDirectives(); 69 | } if ( def instanceof EnumTypeDefinition ) { 70 | return ((EnumTypeDefinition)def).getDirectives(); 71 | } if ( def instanceof ScalarTypeDefinition ) { 72 | return ((ScalarTypeDefinition)def).getDirectives(); 73 | } if ( def instanceof UnionTypeDefinition ) { 74 | return ((UnionTypeDefinition)def).getDirectives(); 75 | } if ( def instanceof InputObjectTypeDefinition ) { 76 | return ((InputObjectTypeDefinition)def).getDirectives(); 77 | } if ( def instanceof SchemaDefinition ) { 78 | return ((SchemaDefinition)def).getDirectives(); 79 | } 80 | return Collections.emptyList(); 81 | } 82 | 83 | private static String getPackageName(List directives, String defaultPackageName) { 84 | String packageName = null; 85 | for ( Directive directive : directives ) { 86 | if ( ! "java".equals(directive.getName()) ) continue; 87 | for ( Argument arg : directive.getArguments() ) { 88 | if ( ! "package".equals(arg.getName()) ) continue; 89 | packageName = (String)Scalars.GraphQLString.getCoercing().parseLiteral(arg.getValue()); 90 | break; 91 | } 92 | break; 93 | } 94 | return ( null == packageName ) ? defaultPackageName : packageName; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /apigen/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 3.0 5 | 6 | 7 | com.distelli.graphql 8 | graphql-apigen 9 | 5.0.1-SNAPSHOT 10 | Generate Java interfaces given a GraphQL Schema 11 | maven-plugin 12 | 13 | 14 | com.distelli.graphql 15 | graphql-apigen-pom 16 | 5.0.1-SNAPSHOT 17 | 18 | 19 | https://github.com/distelli/graphql-apigen 20 | 21 | 22 | 23 | io.takari.maven.plugins 24 | takari-lifecycle-plugin 25 | 26 | 27 | org.apache.maven.plugins 28 | maven-surefire-plugin 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-compiler-plugin 33 | 34 | 35 | org.jacoco 36 | jacoco-maven-plugin 37 | 38 | 39 | org.apache.maven.plugins 40 | maven-gpg-plugin 41 | 42 | 43 | org.sonatype.plugins 44 | nexus-staging-maven-plugin 45 | 46 | 47 | org.apache.maven.plugins 48 | maven-source-plugin 49 | 50 | 51 | org.apache.maven.plugins 52 | maven-javadoc-plugin 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-plugin-plugin 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.apache.maven 64 | maven-plugin-api 65 | ${maven.version} 66 | 67 | 68 | 69 | org.apache.maven 70 | maven-core 71 | ${maven.version} 72 | 73 | 74 | 75 | org.apache.maven.plugin-tools 76 | maven-plugin-annotations 77 | 3.5 78 | provided 79 | 80 | 81 | 82 | org.antlr 83 | ST4 84 | 4.0.8 85 | 86 | 87 | 88 | com.graphql-java 89 | graphql-java 90 | ${graphql.version} 91 | 92 | 93 | 94 | 95 | junit 96 | junit 97 | 4.13.1 98 | test 99 | 100 | 101 | 102 | io.takari.maven.plugins 103 | takari-plugin-testing 104 | 2.9.0 105 | test 106 | 107 | 108 | 109 | io.takari.maven.plugins 110 | takari-plugin-integration-testing 111 | 2.9.0 112 | pom 113 | test 114 | 115 | 116 | 117 | org.apache.maven 118 | maven-compat 119 | ${maven.version} 120 | test 121 | 122 | 123 | 124 | org.springframework 125 | spring-core 126 | 5.3.20 127 | 128 | 129 | 130 | com.distelli.graphql 131 | graphql-apigen-deps 132 | 5.0.1-SNAPSHOT 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /apigen-deps/src/main/java/com/distelli/graphql/ResolverDataFetcher.java: -------------------------------------------------------------------------------- 1 | package com.distelli.graphql; 2 | 3 | import graphql.schema.DataFetcher; 4 | import graphql.schema.DataFetchingEnvironment; 5 | import graphql.execution.batched.Batched; 6 | import graphql.execution.batched.BatchedDataFetcher; 7 | import graphql.schema.DataFetchingEnvironmentImpl; 8 | 9 | import java.util.List; 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.Iterator; 13 | import java.lang.reflect.Method; 14 | 15 | public class ResolverDataFetcher implements DataFetcher { 16 | private DataFetcher fetcher; 17 | private Resolver resolver; 18 | private boolean isBatched; 19 | private int listDepth; 20 | public ResolverDataFetcher(DataFetcher fetcher, Resolver resolver, int listDepth) { 21 | this.fetcher = fetcher; 22 | this.resolver = resolver; 23 | this.listDepth = listDepth; 24 | if ( fetcher instanceof BatchedDataFetcher ) { 25 | this.isBatched = true; 26 | } else { 27 | try { 28 | Method getMethod = fetcher.getClass() 29 | .getMethod("get", DataFetchingEnvironment.class); 30 | this.isBatched = null != getMethod.getAnnotation(Batched.class); 31 | } catch (NoSuchMethodException e) { 32 | throw new IllegalArgumentException(e); 33 | } 34 | } 35 | } 36 | 37 | @Batched 38 | @Override 39 | public Object get(DataFetchingEnvironment env) { 40 | List unresolved = new ArrayList<>(); 41 | Object result; 42 | int depth = listDepth; 43 | if ( env.getSource() instanceof List ) { // batched. 44 | result = getBatched(env); 45 | if ( null != resolver ) addUnresolved(unresolved, result, ++depth); 46 | } else { 47 | result = getUnbatched(env); 48 | if ( null != resolver ) addUnresolved(unresolved, result, depth); 49 | } 50 | if ( null == resolver ) return result; 51 | return replaceResolved(result, resolver.resolve(unresolved).iterator(), depth); 52 | } 53 | 54 | public Object replaceResolved(Object result, Iterator resolved, int depth) { 55 | if ( depth <= 0 ) { 56 | return resolved.next(); 57 | } 58 | List resolvedResults = new ArrayList<>(); 59 | if ( null == result ) return null; 60 | for ( Object elm : (List)result ) { 61 | resolvedResults.add(replaceResolved(elm, resolved, depth-1)); 62 | } 63 | return resolvedResults; 64 | } 65 | 66 | public void addUnresolved(List unresolved, Object result, int depth) { 67 | if ( depth <= 0 ) { 68 | unresolved.add(result); 69 | return; 70 | } 71 | if ( ! (result instanceof List) ) { 72 | if ( null == result ) return; 73 | throw new IllegalStateException("Fetcher "+fetcher+" expected to return a List for each result, got="+result); 74 | } 75 | for ( Object elm : (List)result ) { 76 | addUnresolved(unresolved, elm, depth-1); 77 | } 78 | } 79 | 80 | public Object getUnbatched(DataFetchingEnvironment env) { 81 | if ( ! isBatched ) { 82 | try { 83 | return fetcher.get(env); 84 | } catch (Exception e) { 85 | throw new IllegalStateException(e); 86 | } 87 | } 88 | DataFetchingEnvironmentImpl.Builder builder = DataFetchingEnvironmentImpl.newDataFetchingEnvironment(env); 89 | 90 | DataFetchingEnvironment envCopy = builder.build(); 91 | 92 | try { 93 | Object result = fetcher.get(envCopy); 94 | if ( !(result instanceof List) || ((List)result).size() != 1 ) { 95 | throw new IllegalStateException("Batched fetcher "+fetcher+" expected to return list of 1"); 96 | } 97 | return ((List)result).get(0); 98 | } catch (Exception e) { 99 | throw new IllegalStateException(e); 100 | } 101 | } 102 | 103 | public List getBatched(DataFetchingEnvironment env) { 104 | List sources = env.getSource(); 105 | if ( isBatched ) { 106 | try { 107 | Object result = fetcher.get(env); 108 | if ( !(result instanceof List) || ((List)result).size() != sources.size() ) { 109 | throw new IllegalStateException("Batched fetcher "+fetcher+" expected to return list of "+sources.size()); 110 | } 111 | return (List)result; 112 | } catch (Exception e) { 113 | throw new IllegalStateException(e); 114 | } 115 | } 116 | List result = new ArrayList<>(); 117 | for ( Object source : sources ) { 118 | DataFetchingEnvironmentImpl.Builder builder = DataFetchingEnvironmentImpl.newDataFetchingEnvironment(env); 119 | builder.source(source); 120 | DataFetchingEnvironment envCopy = builder.build(); 121 | 122 | try { 123 | result.add(fetcher.get(envCopy)); 124 | } catch (Exception e) { 125 | throw new IllegalStateException(e); 126 | } 127 | } 128 | return result; 129 | } 130 | 131 | @Override 132 | public String toString() { 133 | return "ResolverDataFetcher{"+ 134 | "resolver="+resolver+ 135 | ", fetcher="+fetcher+ 136 | ", isBatched="+isBatched+ 137 | ", listDepth="+listDepth+ 138 | "}"; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /apigen/src/main/java/com/distelli/graphql/apigen/ApiGenMojo.java: -------------------------------------------------------------------------------- 1 | package com.distelli.graphql.apigen; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.file.attribute.BasicFileAttributes; 6 | import java.nio.file.FileSystems; 7 | import java.nio.file.FileVisitResult; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.nio.file.PathMatcher; 11 | import java.nio.file.SimpleFileVisitor; 12 | import java.util.List; 13 | import org.apache.maven.plugin.AbstractMojo; 14 | import org.apache.maven.project.MavenProject; 15 | import org.apache.maven.plugins.annotations.Mojo; 16 | import org.apache.maven.plugins.annotations.Parameter; 17 | import org.apache.maven.plugins.annotations.LifecyclePhase; 18 | import org.apache.maven.plugins.annotations.Execute; 19 | import org.apache.maven.plugins.annotations.ResolutionScope; 20 | import java.net.URL; 21 | import java.net.URLClassLoader; 22 | import java.util.Enumeration; 23 | import org.apache.maven.model.Resource; 24 | import java.util.Collections; 25 | import org.springframework.core.io.support.PathMatchingResourcePatternResolver; 26 | import java.util.ArrayList; 27 | import java.util.Arrays; 28 | 29 | @Mojo(name="apigen", 30 | defaultPhase=LifecyclePhase.GENERATE_SOURCES, 31 | requiresDependencyResolution=ResolutionScope.COMPILE) 32 | @Execute(goal="apigen") 33 | public class ApiGenMojo extends AbstractMojo { 34 | @Parameter(defaultValue="${project}", readonly=true) 35 | private MavenProject project; 36 | 37 | @Parameter(name="sourceDirectory", 38 | defaultValue="schema") 39 | private File sourceDirectory; 40 | 41 | @Parameter(name="outputDirectory", 42 | defaultValue="target/generated-sources/apigen") 43 | private File outputDirectory; 44 | 45 | @Parameter(name="guiceModuleName") 46 | private String guiceModuleName; 47 | 48 | @Parameter(name="defaultPackageName", defaultValue = "com.graphql.generated") 49 | private String defaultPackageName; 50 | 51 | private File makeAbsolute(File in) { 52 | if ( in.isAbsolute() ) return in; 53 | return new File(project.getBasedir(), in.toString()); 54 | } 55 | 56 | private ClassLoader getCompileClassLoader() throws Exception { 57 | List urls = new ArrayList<>(); 58 | int idx = 0; 59 | String ignored = project.getBuild().getOutputDirectory(); 60 | getLog().debug("ignore="+ignored); 61 | for ( String path : project.getCompileClasspathElements() ) { 62 | if ( path.equals(ignored) ) continue; 63 | File file = makeAbsolute(new File(path)); 64 | String name = file.toString(); 65 | if ( file.isDirectory() || ! file.exists() ) { 66 | name = name + "/"; 67 | } 68 | URL url = new URL("file", null, name); 69 | getLog().debug("classpath += " + url); 70 | urls.add(url); 71 | } 72 | return new URLClassLoader(urls.toArray(new URL[urls.size()])); 73 | } 74 | 75 | @Override 76 | public void execute() { 77 | try { 78 | sourceDirectory = makeAbsolute(sourceDirectory); 79 | outputDirectory = makeAbsolute(outputDirectory); 80 | if ( ! sourceDirectory.exists() ) return; 81 | getLog().debug("Running ApiGen\n\tsourceDirectory=" + sourceDirectory + 82 | "\n\toutputDirectory=" + outputDirectory); 83 | ClassLoader cp = getCompileClassLoader(); 84 | ApiGen apiGen = new ApiGen.Builder() 85 | .withOutputDirectory(outputDirectory.toPath()) 86 | .withGuiceModuleName(guiceModuleName) 87 | .withDefaultPackageName(defaultPackageName) 88 | .build(); 89 | PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(cp); 90 | for ( org.springframework.core.io.Resource resource : resolver.getResources("classpath*:graphql-apigen-schema/*.graphql{,s}") ) { 91 | URL url = resource.getURL(); 92 | getLog().debug("Processing "+url); 93 | apiGen.addForReference(url); 94 | } 95 | findGraphql(sourceDirectory, apiGen::addForGeneration); 96 | apiGen.generate(); 97 | Resource schemaResource = new Resource(); 98 | schemaResource.setTargetPath("graphql-apigen-schema"); 99 | schemaResource.setFiltering(false); 100 | schemaResource.setIncludes(Arrays.asList("*.graphqls","*.graphql")); 101 | schemaResource.setDirectory(sourceDirectory.toString()); 102 | project.addResource(schemaResource); 103 | project.addCompileSourceRoot(outputDirectory.getAbsolutePath()); 104 | } catch (Exception e) { 105 | String msg = e.getMessage(); 106 | if ( null == msg ) msg = e.getClass().getName(); 107 | getLog().error(msg + " when trying to build sources from graphql.", e); 108 | } 109 | } 110 | 111 | private interface VisitPath { 112 | public void visit(Path path) throws IOException; 113 | } 114 | 115 | private void findGraphql(File rootDir, VisitPath visitPath) throws IOException { 116 | PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/*.graphql{,s}"); 117 | Files.walkFileTree(rootDir.toPath(), new SimpleFileVisitor() { 118 | @Override 119 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 120 | if ( matcher.matches(file) ) { 121 | getLog().debug("Processing "+file); 122 | visitPath.visit(file); 123 | } 124 | return FileVisitResult.CONTINUE; 125 | } 126 | }); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /apigen-deps/src/main/java/com/distelli/graphql/MethodDataFetcher.java: -------------------------------------------------------------------------------- 1 | // Nearly identical to graphql.schema.PropertyDataFetcher, but deals with arguments. 2 | package com.distelli.graphql; 3 | 4 | import java.lang.reflect.InvocationHandler; 5 | import java.lang.reflect.InvocationTargetException; 6 | import java.lang.reflect.Method; 7 | import java.lang.reflect.Proxy; 8 | import java.util.Map; 9 | 10 | import graphql.schema.DataFetcher; 11 | import graphql.schema.DataFetchingEnvironment; 12 | import graphql.schema.GraphQLFieldDefinition; 13 | import graphql.schema.GraphQLFieldsContainer; 14 | import graphql.schema.GraphQLNonNull; 15 | import graphql.schema.GraphQLOutputType; 16 | import graphql.schema.GraphQLType; 17 | 18 | import static graphql.Scalars.GraphQLBoolean; 19 | 20 | public class MethodDataFetcher implements DataFetcher { 21 | private final String propertyName; 22 | private final Class argType; 23 | private final Object impl; 24 | private String graphQLPropertyName = null; 25 | 26 | public MethodDataFetcher(String propertyName, Class argType, Object impl) { 27 | if ( null != argType ) { 28 | if ( ! argType.isInterface() ) { 29 | throw new IllegalArgumentException("argType must be interface, got argType="+argType); 30 | } 31 | } 32 | this.propertyName = propertyName; 33 | this.argType = argType; 34 | this.impl = impl; 35 | } 36 | 37 | public MethodDataFetcher(String propertyName, Class argType, Object impl, String graphQLPropertyName) { 38 | this(propertyName, argType, impl); 39 | this.graphQLPropertyName = graphQLPropertyName; 40 | } 41 | 42 | @Override 43 | public Object get(DataFetchingEnvironment env) { 44 | Object source = ( null != impl ) ? impl : env.getSource(); 45 | if (source == null) return null; 46 | if (source instanceof ResolveDataFetchingEnvironment) { 47 | source = ((ResolveDataFetchingEnvironment)source).resolve(env); 48 | } 49 | return getMethodViaGetter(source, env.getFieldType(), getFieldType(env.getParentType()), env.getArguments()); 50 | } 51 | 52 | private GraphQLFieldDefinition getFieldType(GraphQLType type) { 53 | if ( type instanceof GraphQLFieldsContainer ) { 54 | GraphQLFieldDefinition fieldType = ((GraphQLFieldsContainer)type).getFieldDefinition(propertyName); 55 | 56 | if (null == fieldType && null != this.graphQLPropertyName) { 57 | fieldType = ((GraphQLFieldsContainer)type).getFieldDefinition(graphQLPropertyName); 58 | } 59 | 60 | return fieldType; 61 | } 62 | return null; 63 | } 64 | 65 | private Object getMethodViaGetter(Object object, GraphQLOutputType outputType, GraphQLFieldDefinition fieldDef, Map args) { 66 | if ( fieldDef.getArguments().size() > 0 ^ null != argType ) { 67 | throw new IllegalStateException( 68 | "MethodDataFetcher created has argType="+argType+ 69 | " and invoked with argSize="+fieldDef.getArguments().size()+ 70 | ", argType must be null if argSize == 0; or argType must be non null and argSize > 0"); 71 | } 72 | try { 73 | if (fieldDef.getArguments().size() > 0) { 74 | return getMethodViaGetterUsingPrefix(object, args, null); 75 | } else if (isBooleanMethod(outputType)) { 76 | try { 77 | return getMethodViaGetterUsingPrefix(object, args, "is"); 78 | } catch (NoSuchMethodException ex) { 79 | return getMethodViaGetterUsingPrefix(object, args, "get"); 80 | } 81 | } else { 82 | return getMethodViaGetterUsingPrefix(object, args, "get"); 83 | } 84 | } catch (NoSuchMethodException ex) { 85 | throw new RuntimeException(ex); 86 | } 87 | } 88 | 89 | private static class MapInvocationHandler implements InvocationHandler { 90 | private Map map; 91 | public MapInvocationHandler(Map map) { 92 | this.map = map; 93 | } 94 | public Object invoke(Object proxy, Method method, Object[] methodArgs) { 95 | if ( null != methodArgs && methodArgs.length > 0 ) { 96 | throw new UnsupportedOperationException( 97 | "Expected all interface methods to have no arguments, got "+methodArgs.length+" arguments"); 98 | } 99 | String name = method.getName(); 100 | if ( ! name.startsWith("get") || name.length() < 4 ) { 101 | throw new UnsupportedOperationException( 102 | "Expected all interface methods to begin with 'get', got "+name); 103 | } 104 | String ucArgName = name.substring(3); 105 | String lcArgName = ucArgName.substring(0, 1).toLowerCase() + ucArgName.substring(1); 106 | Object result = map.get(lcArgName); 107 | if ( null == result ) { 108 | result = map.get(ucArgName); 109 | } 110 | if ( null == result ) return null; 111 | if ( Map.class.isAssignableFrom(result.getClass()) ) { 112 | Class returnType = method.getReturnType(); 113 | if ( ! Map.class.isAssignableFrom(returnType) ) { 114 | return Proxy.newProxyInstance( 115 | returnType.getClassLoader(), 116 | new Class[]{returnType}, 117 | new MapInvocationHandler((Map)result)); 118 | } 119 | } 120 | return result; 121 | } 122 | } 123 | 124 | private Object getMethodViaGetterUsingPrefix(Object object, Map args, String prefix) 125 | throws NoSuchMethodException 126 | { 127 | String methodName; 128 | if ( null == prefix ) { 129 | methodName = propertyName; 130 | } else { 131 | methodName = prefix + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); 132 | } 133 | try { 134 | Method method; 135 | if ( null == argType ) { 136 | method = object.getClass().getMethod(methodName); 137 | return method.invoke(object); 138 | } 139 | method = object.getClass().getMethod(methodName, argType); 140 | Object argsProxy = 141 | Proxy.newProxyInstance(argType.getClassLoader(), 142 | new Class[]{argType}, 143 | new MapInvocationHandler(args)); 144 | return method.invoke(object, argsProxy); 145 | } catch (IllegalAccessException|InvocationTargetException ex) { 146 | throw new RuntimeException(ex); 147 | } 148 | } 149 | 150 | private boolean isBooleanMethod(GraphQLOutputType outputType) { 151 | if (outputType == GraphQLBoolean) return true; 152 | if (outputType instanceof GraphQLNonNull) { 153 | return ((GraphQLNonNull) outputType).getWrappedType() == GraphQLBoolean; 154 | } 155 | return false; 156 | } 157 | 158 | @Override 159 | public String toString() { 160 | return "MethodDataFetcher{"+ 161 | "propertyName="+propertyName+ 162 | ", argType="+argType+ 163 | "}"; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 3.0 5 | 6 | 7 | com.distelli.graphql 8 | graphql-apigen-pom 9 | 5.0.1-SNAPSHOT 10 | Generate Java interfaces given a GraphQL Schema 11 | pom 12 | 13 | GraphQL schema first development facilitated by generating java 14 | interfaces from GraphQL schema, which can easily be wired together. 15 | 16 | https://github.com/distelli/graphql-apigen 17 | 18 | 19 | distelli 20 | Distelli 21 | https://www.distelli.com 22 | 23 | developer 24 | 25 | 26 | 27 | 28 | apigen 29 | apigen-deps 30 | 31 | 32 | scm:git:git@github.com:Distelli/graphql-apigen.git 33 | https://github.com/Distelli/graphql-apigen.git 34 | 35 | 36 | 37 | ossrh 38 | https://oss.sonatype.org/content/repositories/snapshots 39 | 40 | 41 | ossrh 42 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 43 | 44 | 45 | 46 | 3.2.5 47 | 4.1.0 48 | 12.0 49 | UTF-8 50 | 51 | 52 | 53 | 54 | 55 | org.springframework.build 56 | aws-maven 57 | 5.0.0.RELEASE 58 | 59 | 60 | 61 | 62 | 63 | io.takari.maven.plugins 64 | takari-lifecycle-plugin 65 | 1.12.2 66 | true 67 | 68 | 69 | testProperties 70 | process-test-resources 71 | 72 | testProperties 73 | 74 | 75 | 76 | 77 | 78 | org.apache.maven.plugins 79 | maven-surefire-plugin 80 | 2.19.1 81 | 82 | false 83 | 84 | 85 | src/test/resources/logging.properties 86 | 87 | 88 | 89 | brief 90 | false 91 | true 92 | 93 | 94 | 95 | disable-tests-during-deploy 96 | deploy 97 | 98 | true 99 | 100 | 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-compiler-plugin 106 | 3.2 107 | 108 | 1.8 109 | 1.8 110 | 111 | 112 | 113 | org.jacoco 114 | jacoco-maven-plugin 115 | 0.7.7.201606060606 116 | 117 | ${basedir}/target/coverage-reports/jacoco-unit.exec 118 | ${basedir}/target/coverage-reports/jacoco-unit.exec 119 | 120 | 121 | 122 | jacoco-initialize 123 | 124 | prepare-agent 125 | 126 | 127 | 128 | jacoco-site 129 | package 130 | 131 | report 132 | 133 | 134 | 135 | 136 | 137 | org.apache.maven.plugins 138 | maven-source-plugin 139 | 2.2.1 140 | 141 | 142 | attach-sources 143 | 144 | jar-no-fork 145 | 146 | 147 | 148 | 149 | 150 | org.apache.maven.plugins 151 | maven-javadoc-plugin 152 | 2.9.1 153 | 154 | 155 | attach-javadocs 156 | 157 | jar 158 | 159 | 160 | 161 | 162 | 163 | org.apache.maven.plugins 164 | maven-plugin-plugin 165 | 3.5 166 | 167 | apigen 168 | 169 | 170 | 171 | default-descriptor 172 | process-classes 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | org.sonatype.plugins 181 | nexus-staging-maven-plugin 182 | 183 | 184 | org.apache.maven.plugins 185 | maven-gpg-plugin 186 | 187 | 188 | 189 | 190 | 191 | Apache License, Version 2.0 192 | https://www.apache.org/licenses/LICENSE-2.0.txt 193 | repo 194 | A business-friendly OSS license 195 | 196 | 197 | 198 | 199 | 200 | release 201 | 202 | 203 | 204 | 205 | org.apache.maven.plugins 206 | maven-gpg-plugin 207 | 1.6 208 | 209 | 210 | sign-artifacts 211 | verify 212 | 213 | sign 214 | 215 | 216 | 217 | 218 | 219 | org.sonatype.plugins 220 | nexus-staging-maven-plugin 221 | 1.6.6 222 | true 223 | 224 | ossrh 225 | https://oss.sonatype.org/ 226 | 980264ec5d0479 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | -------------------------------------------------------------------------------- /apigen/src/test/projects/posts/src/test/java/com/disteli/posts/PostsTest.java: -------------------------------------------------------------------------------- 1 | package com.distelli.posts; 2 | 3 | import org.junit.Test; 4 | import graphql.execution.batched.BatchedExecutionStrategy; 5 | import java.util.*; 6 | import graphql.schema.*; 7 | import graphql.ExecutionResult; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import graphql.GraphQL; 10 | import com.google.inject.Guice; 11 | import com.google.inject.Key; 12 | import com.google.inject.AbstractModule; 13 | import com.google.inject.Injector; 14 | import com.google.inject.TypeLiteral; 15 | import com.fasterxml.jackson.databind.SerializationFeature; 16 | import com.google.inject.multibindings.MapBinder; 17 | import javax.inject.Singleton; 18 | import java.util.concurrent.atomic.AtomicInteger; 19 | import graphql.schema.DataFetchingEnvironment; 20 | import static org.junit.Assert.*; 21 | 22 | public class PostsTest { 23 | public static class QueryPostsImpl implements QueryPosts { 24 | private Map posts; 25 | public QueryPostsImpl(Map posts) { 26 | this.posts = posts; 27 | } 28 | @Override 29 | public List getPosts() { 30 | return new ArrayList<>(posts.values()); 31 | } 32 | } 33 | public static class MutatePostsImpl implements MutatePosts { 34 | private AtomicInteger nextPostId = new AtomicInteger(5); 35 | private Map posts; 36 | private DataFetchingEnvironment env; 37 | public MutatePostsImpl(Map posts) { 38 | this.posts = posts; 39 | } 40 | 41 | @Override 42 | public MutatePostsImpl resolve(DataFetchingEnvironment env) { 43 | this.env = env; 44 | return this; 45 | } 46 | 47 | @Override 48 | public Post createPost(MutatePosts.CreatePostArgs args) { 49 | if ( ! "authorized-user".equals(env.getContext()) ) { 50 | throw new java.security.AccessControlException("context MUST be authorized-user"); 51 | } 52 | InputPost req = args.getPost(); 53 | Post.Builder postBuilder = new Post.Builder() 54 | .withTitle(req.getTitle()) 55 | .withAuthor(new Author.Unresolved(req.getAuthorId())); 56 | Post post; 57 | synchronized ( posts ) { 58 | Integer id = nextPostId.incrementAndGet(); 59 | post = postBuilder.withId(id).build(); 60 | posts.put(id, post); 61 | } 62 | return post; 63 | } 64 | 65 | @Override 66 | public Post upvotePost(MutatePosts.UpvotePostArgs args) { 67 | synchronized ( posts ) { 68 | Post post = posts.get(args.getPostId()); 69 | // Should throw NoSuchEntityException! 70 | if ( null == post ) throw new RuntimeException("NotFound"); 71 | 72 | Post upvoted = new Post.Builder(post) 73 | .withVotes(post.getVotes()+1) 74 | .build(); 75 | posts.put(args.getPostId(), upvoted); 76 | return upvoted; 77 | } 78 | } 79 | } 80 | public static class AuthorResolver implements Author.Resolver { 81 | private Map authors; 82 | public AuthorResolver(Map authors) { 83 | this.authors = authors; 84 | } 85 | @Override 86 | public List resolve(List unresolvedList) { 87 | List result = new ArrayList<>(); 88 | for ( Author unresolved : unresolvedList ) { 89 | // In a real app we would check if it is instanceof Author.Unresolved 90 | result.add(authors.get(unresolved.getId())); 91 | } 92 | return result; 93 | } 94 | } 95 | public static class PostResolver implements Post.Resolver { 96 | private Map posts; 97 | public PostResolver(Map posts) { 98 | this.posts = posts; 99 | } 100 | @Override 101 | public List resolve(List unresolvedList) { 102 | List result = new ArrayList<>(); 103 | for ( Post unresolved : unresolvedList ) { 104 | if ( null == unresolved ) { 105 | result.add(null); 106 | } else { 107 | result.add(posts.get(unresolved.getId())); 108 | } 109 | } 110 | return result; 111 | } 112 | } 113 | public Injector setup() throws Exception { 114 | // Setup datastores: 115 | Map authors = new LinkedHashMap<>(); 116 | authors.put(1, 117 | new Author.Builder() 118 | .withId(1) 119 | .withFirstName("Brian") 120 | .withLastName("Maher") 121 | .withPosts(Arrays.asList( 122 | new Post.Unresolved(1))) 123 | .build()); 124 | authors.put(2, 125 | new Author.Builder() 126 | .withId(2) 127 | .withFirstName("Rahul") 128 | .withLastName("Singh") 129 | .withPosts(Arrays.asList(new Post[] { 130 | new Post.Unresolved(4), 131 | new Post.Unresolved(3), 132 | })) 133 | .build()); 134 | Map posts = new LinkedHashMap<>(); 135 | posts.put(1, 136 | new Post.Builder() 137 | .withId(1) 138 | .withTitle("GraphQL Rocks") 139 | .withAuthor(new Author.Unresolved(1)) 140 | .build()); 141 | posts.put(3, 142 | new Post.Builder() 143 | .withId(3) 144 | .withTitle("Announcing Callisto") 145 | .withAuthor(new Author.Unresolved(2)) 146 | .build()); 147 | posts.put(4, 148 | new Post.Builder() 149 | .withId(4) 150 | .withTitle("Distelli Contributing to Open Source") 151 | .withAuthor(new Author.Unresolved(2)) 152 | .build()); 153 | 154 | Injector injector = Guice.createInjector( 155 | new PostsModule(), 156 | new AbstractModule() { 157 | @Override 158 | protected void configure() { 159 | bind(Author.Resolver.class) 160 | .toInstance(new AuthorResolver(authors)); 161 | bind(Post.Resolver.class) 162 | .toInstance(new PostResolver(posts)); 163 | bind(MutatePosts.class) 164 | .toInstance(new MutatePostsImpl(posts)); 165 | bind(QueryPosts.class) 166 | .toInstance(new QueryPostsImpl(posts)); 167 | } 168 | }); 169 | return injector; 170 | } 171 | 172 | @Test 173 | public void testQuery() throws Exception { 174 | Injector injector = setup(); 175 | 176 | // TODO: Support "schema" type so this is generated too :) 177 | Map types = 178 | injector.getInstance(Key.get(new TypeLiteral>(){})); 179 | GraphQLSchema schema = GraphQLSchema.newSchema() 180 | .query((GraphQLObjectType)types.get("QueryPosts")) 181 | .mutation((GraphQLObjectType)types.get("MutatePosts")) 182 | .build(new HashSet<>(types.values())); 183 | 184 | GraphQL graphQL = new GraphQL(schema, new BatchedExecutionStrategy()); 185 | ObjectMapper om = new ObjectMapper(); 186 | om.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS); 187 | 188 | // Using GraphQL Mutation: 189 | ExecutionResult result = graphQL.execute( 190 | "mutation{createPost(post:{title:\"NEW\" authorId:1}){title}}", 191 | "authorized-user"); 192 | checkExecutionResult(result); 193 | assertEquals("{\"createPost\":{\"title\":\"NEW\"}}", om.writeValueAsString(result.getData())); 194 | 195 | // ...or the API: 196 | MutatePosts mutatePosts = injector.getInstance(MutatePosts.class); 197 | mutatePosts.createPost(new MutatePosts.CreatePostArgs() { 198 | public InputPost getPost() { 199 | return new InputPost.Builder() 200 | .withTitle("API") 201 | .withAuthorId(2) 202 | .build(); 203 | } 204 | }); 205 | 206 | // Using GraphQL Query: 207 | result = graphQL.execute("{posts{title author{firstName lastName}}}"); 208 | checkExecutionResult(result); 209 | 210 | String value = om.writeValueAsString(result.getData()); 211 | assertEquals("{\"posts\":[{\"author\":{\"firstName\":\"Brian\",\"lastName\":\"Maher\"},\"title\":\"GraphQL Rocks\"},{\"author\":{\"firstName\":\"Rahul\",\"lastName\":\"Singh\"},\"title\":\"Announcing Callisto\"},{\"author\":{\"firstName\":\"Rahul\",\"lastName\":\"Singh\"},\"title\":\"Distelli Contributing to Open Source\"},{\"author\":{\"firstName\":\"Brian\",\"lastName\":\"Maher\"},\"title\":\"NEW\"},{\"author\":{\"firstName\":\"Rahul\",\"lastName\":\"Singh\"},\"title\":\"API\"}]}", value); 212 | 213 | // ...or the API: 214 | QueryPosts queryPosts = injector.getInstance(QueryPosts.class); 215 | List posts = queryPosts.getPosts(); 216 | // ...since we are not using GraphQL, the authors will not be resolved: 217 | assertEquals(posts.get(0).getAuthor().getClass(), Author.Unresolved.class); 218 | assertArrayEquals( 219 | new String[]{"GraphQL Rocks", "Announcing Callisto", "Distelli Contributing to Open Source", "NEW", "API"}, 220 | posts.stream().map((post) -> post.getTitle()).toArray(size -> new String[size])); 221 | assertArrayEquals( 222 | new Integer[]{1,2,2,1,2}, 223 | posts.stream().map((post) -> post.getAuthor().getId()).toArray(size -> new Integer[size])); 224 | } 225 | 226 | private void checkExecutionResult(ExecutionResult result) throws Exception { 227 | if ( null == result.getErrors() || result.getErrors().size() <= 0 ) return; 228 | ObjectMapper om = new ObjectMapper(); 229 | om.enable(SerializationFeature.INDENT_OUTPUT); 230 | String errors = om.writeValueAsString(result.getErrors()); 231 | fail(errors); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /apigen/src/main/java/com/distelli/graphql/apigen/ApiGen.java: -------------------------------------------------------------------------------- 1 | package com.distelli.graphql.apigen; 2 | 3 | import graphql.language.Definition; 4 | import graphql.language.Document; 5 | import graphql.language.SchemaDefinition; 6 | import graphql.language.TypeDefinition; 7 | import graphql.parser.Parser; 8 | import java.io.IOException; 9 | import java.net.URL; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | import java.util.HashMap; 14 | import java.util.LinkedHashMap; 15 | import java.util.Map; 16 | import java.util.Scanner; 17 | import java.util.Set; 18 | import java.util.TreeSet; 19 | import org.stringtemplate.v4.STGroup; 20 | import org.stringtemplate.v4.STGroupFile; 21 | import java.util.List; 22 | import java.util.ArrayList; 23 | import static java.nio.charset.StandardCharsets.UTF_8; 24 | 25 | public class ApiGen { 26 | private Parser parser = new Parser(); 27 | private Path outputDirectory; 28 | private STGroup stGroup; 29 | private String guiceModuleName; 30 | private String defaultPackageName; 31 | private Map generatedTypes = new LinkedHashMap<>(); 32 | private Map referenceTypes = new HashMap<>(); 33 | private List schemaDefinitions = new ArrayList<>(); 34 | 35 | public static class Builder { 36 | private Path outputDirectory; 37 | private STGroup stGroup; 38 | private String guiceModuleName; 39 | private String defaultPackageName; 40 | 41 | /** 42 | * (required) 43 | * 44 | * @param outputDirectory is the location of where the .java files are written. 45 | * 46 | * @return this 47 | */ 48 | public Builder withOutputDirectory(Path outputDirectory) { 49 | this.outputDirectory = outputDirectory; 50 | return this; 51 | } 52 | 53 | /** 54 | * @param stGroup is used for specifying a custom template group. 55 | * See graphql-apigen.stg for an example of what templates must be 56 | * specified. 57 | * 58 | * @return this 59 | */ 60 | public Builder withSTGroup(STGroup stGroup) { 61 | this.stGroup = stGroup; 62 | return this; 63 | } 64 | 65 | public Builder withGuiceModuleName(String guiceModuleName) { 66 | this.guiceModuleName = guiceModuleName; 67 | return this; 68 | } 69 | 70 | public Builder withDefaultPackageName(String defaultPackageName) { 71 | this.defaultPackageName = defaultPackageName; 72 | return this; 73 | } 74 | 75 | /** 76 | * Create a new instances of ApiGen with the built parameters. 77 | * 78 | * @return the new ApiGen instance. 79 | * 80 | * @throws IOException if an io error occurs. 81 | */ 82 | public ApiGen build() throws IOException { 83 | return new ApiGen(this); 84 | } 85 | } 86 | 87 | private ApiGen(Builder builder) throws IOException { 88 | if ( null == builder.outputDirectory ) { 89 | throw new NullPointerException("The ApiGen outputDirectory must be specified"); 90 | } 91 | guiceModuleName = builder.guiceModuleName; 92 | defaultPackageName = builder.defaultPackageName; 93 | outputDirectory = builder.outputDirectory; 94 | stGroup = ( null == builder.stGroup ) 95 | ? getDefaultSTGroup() 96 | : builder.stGroup; 97 | } 98 | 99 | /** 100 | * Add a graphql schema document used for reference, but no code generation. 101 | * 102 | * @param path is the path to a graphql file to add for reference purposes. 103 | * 104 | * @throws IOException if an io error occurs. 105 | */ 106 | public void addForReference(URL path) throws IOException { 107 | add(referenceTypes, path); 108 | } 109 | 110 | public void addForReference(Path path) throws IOException { 111 | addForReference(path.toFile().toURI().toURL()); 112 | } 113 | 114 | /** 115 | * Add a graphql schema document. 116 | * 117 | * @param path the location of the graphql document. 118 | * 119 | * @throws IOException if an io error occurs. 120 | */ 121 | public void addForGeneration(URL path) throws IOException { 122 | add(generatedTypes, path); 123 | } 124 | 125 | public void addForGeneration(Path path) throws IOException { 126 | addForGeneration(path.toFile().toURI().toURL()); 127 | } 128 | 129 | private void add(Map types, URL path) throws IOException { 130 | String content = slurp(path); 131 | try { 132 | Document doc = parser.parseDocument(content); 133 | for ( Definition definition : doc.getDefinitions() ) { 134 | if ( definition instanceof SchemaDefinition ) { 135 | if ( generatedTypes == types ) { 136 | schemaDefinitions.add(new TypeEntry(definition, path, defaultPackageName)); 137 | } 138 | continue; 139 | } else if ( ! (definition instanceof TypeDefinition) ) { 140 | // TODO: What about @definition types? 141 | throw new RuntimeException( 142 | "GraphQL schema documents must only contain schema type definitions, got "+ 143 | definition.getClass().getSimpleName() + " [" + 144 | definition.getSourceLocation().getLine() + "," + 145 | definition.getSourceLocation().getColumn() + "]"); 146 | } 147 | TypeEntry newEntry = new TypeEntry(definition, path, defaultPackageName); 148 | TypeEntry oldEntry = referenceTypes.get(newEntry.getName()); 149 | 150 | if ( null != oldEntry ) { 151 | // TODO: Support the extend type? 152 | throw new RuntimeException( 153 | "Duplicate type definition for '" + newEntry.getName() + "'" + 154 | " defined both in " + oldEntry.getSourceLocation() + " and " + 155 | newEntry.getSourceLocation()); 156 | } 157 | 158 | types.put(newEntry.getName(), newEntry); 159 | if ( types != referenceTypes ) { 160 | // All types should be added to reference types... 161 | referenceTypes.put(newEntry.getName(), newEntry); 162 | } 163 | } 164 | } catch ( Exception ex ) { 165 | throw new RuntimeException(ex.getMessage() + " when parsing '"+path+"'", ex); 166 | } 167 | } 168 | 169 | /** 170 | * Generate the graphql APIs (and DataFetcher adaptors). 171 | * 172 | * @throws IOException if an io error occurs. 173 | */ 174 | public void generate() throws IOException { 175 | Set generatorNames = new TreeSet(); 176 | for ( String name : stGroup.getTemplateNames() ) { 177 | if ( ! name.endsWith("FileName") ) continue; 178 | String generatorName = name.substring(0, name.length() - "FileName".length()); 179 | if ( ! stGroup.isDefined(generatorName + "Generator") ) continue; 180 | generatorNames.add(generatorName); 181 | } 182 | 183 | List allEntries = new ArrayList(generatedTypes.values()); 184 | allEntries.addAll(schemaDefinitions); 185 | StringBuilder moduleBuilder = new StringBuilder(); 186 | for ( TypeEntry entry : allEntries ) { 187 | try { 188 | STModel model = new STModel.Builder() 189 | .withTypeEntry(entry) 190 | .withReferenceTypes(referenceTypes) 191 | .build(); 192 | model.validate(); 193 | 194 | Path directory = getDirectory(entry.getPackageName()); 195 | for ( String generatorName : generatorNames ) { 196 | String fileName = stGroup.getInstanceOf(generatorName+"FileName") 197 | .add("model", model) 198 | .render(); 199 | if ( "".equals(fileName) || null == fileName ) continue; 200 | String content = stGroup.getInstanceOf(generatorName+"Generator") 201 | .add("model", model) 202 | .render(); 203 | if ( stGroup.isDefined(generatorName + "GuiceModule") ) { 204 | moduleBuilder.append(stGroup.getInstanceOf(generatorName+"GuiceModule") 205 | .add("model", model) 206 | .render()); 207 | } 208 | writeFile(Paths.get(directory.toString(), fileName), 209 | content); 210 | } 211 | } catch ( Exception ex ) { 212 | throw new RuntimeException(ex.getMessage() + " when generating code from '" + 213 | entry.getSource() + "'", ex); 214 | } 215 | } 216 | if ( moduleBuilder.length() > 0 && guiceModuleName != null && stGroup.isDefined("guiceModule") ) { 217 | PackageClassName packageClassName = getPackageClassName(guiceModuleName); 218 | String content = stGroup.getInstanceOf("guiceModule") 219 | .add("packageName", packageClassName.packageName) 220 | .add("className", packageClassName.className) 221 | .add("configure", moduleBuilder.toString()) 222 | .render(); 223 | writeFile(Paths.get(getDirectory(packageClassName.packageName).toString(), 224 | packageClassName.className+".java"), 225 | content); 226 | } 227 | } 228 | 229 | private static class PackageClassName { 230 | public String packageName; 231 | public String className; 232 | public PackageClassName(String packageName, String className) { 233 | this.packageName = packageName; 234 | this.className = className; 235 | } 236 | } 237 | 238 | private PackageClassName getPackageClassName(String fullName) { 239 | int dot = fullName.lastIndexOf("."); 240 | if ( dot < 0 ) return new PackageClassName(null, fullName); 241 | return new PackageClassName(fullName.substring(0, dot), 242 | fullName.substring(dot+1)); 243 | } 244 | 245 | public Path getDirectory(String packageName) { 246 | String[] dirs = packageName.split("\\."); 247 | return Paths.get(outputDirectory.toString(), dirs); 248 | } 249 | 250 | private String slurp(URL path) throws IOException { 251 | Scanner scan = new Scanner(path.openStream()).useDelimiter("\\A"); 252 | return scan.hasNext() ? scan.next() : ""; 253 | } 254 | 255 | private void writeFile(Path path, String content) throws IOException { 256 | path.getParent().toFile().mkdirs(); 257 | Files.write(path, content.getBytes(UTF_8)); 258 | } 259 | 260 | private STGroup getDefaultSTGroup() throws IOException { 261 | return new STGroupFile("graphql-apigen.stg"); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-apigen 2 | 3 | Generate Java APIs with GraphQL Schemas in order to facilitate "schema first" development. 4 | 5 | ### Posts Example 6 | 7 | Create a file to define your schema. In this example we are creating the `schema/posts.graphql` file: 8 | 9 | ```graphql 10 | type Author @java(package:"com.distelli.posts") { 11 | id: Int! # the ! means that every author object _must_ have an id 12 | firstName: String 13 | lastName: String 14 | posts: [Post] # the list of Posts by this author 15 | } 16 | 17 | type Post @java(package:"com.distelli.posts") { 18 | id: Int! 19 | title: String 20 | author: Author 21 | votes: Int 22 | } 23 | 24 | # the schema allows the following query: 25 | type QueryPosts @java(package:"com.distelli.posts") { 26 | posts: [Post] 27 | } 28 | 29 | input InputPost @java(package:"com.distelli.posts") { 30 | title: String 31 | authorId: Int! 32 | } 33 | 34 | # this schema allows the following mutation: 35 | type MutatePosts @java(package:"com.distelli.posts") { 36 | createPost(post:InputPost): Post 37 | upvotePost( 38 | postId: Int! 39 | ): Post 40 | } 41 | ``` 42 | 43 | Notice that we annotate the types with a java package name. The above schema 44 | will generate the following java **interfaces** in `target/generated-sources/apigen` 45 | (in the `com.distelli.posts` package): 46 | 47 | * `Author` and `Author.Resolver` 48 | * `Post` and `Post.Resolver` 49 | * `QueryPosts` 50 | * `InputPost` 51 | * `MutatePosts` 52 | 53 | The `*.Resolver` interfaces are only generated if their is a field named "id". This 54 | interface may be implemented to resolve a `*.Unresolved` (only the id field defined) 55 | into a fully resolved implementation (all fields defined). All interface methods 56 | have "default" implementations that return null. 57 | 58 | Each of these interfaces also have a default inner class named `*.Builder` and 59 | `*.Impl`. The `*.Builder` will have a no-argument constructor and a constructor 60 | that takes the parent interface as an argument. The `*.Builder` will also have a 61 | method `with()` for each no-arg field which returns the 62 | builder and a `build()` method that creates a `*.Impl`. 63 | 64 | Any field that takes arguments will cause a `*.Args` interface to be 65 | generated with methods for each input field. 66 | 67 | Any field that does NOT take arguments will generate method names prefixed with 68 | "get". 69 | 70 | Finally, the above schema also generates a Guice module `PostsModule` which adds to 71 | a `Map` multibinder (the name "PostsModule" comes from the 72 | filename which defines the schema). See below for information about using Spring for 73 | Dependency Injection. 74 | 75 | Putting this all together, we can implement the `QueryPosts` implementation as such: 76 | 77 | ```java 78 | public class QueryPostsImpl implements QueryPosts { 79 | private Map posts; 80 | public QueryPostsImpl(Map posts) { 81 | this.posts = posts; 82 | } 83 | @Override 84 | public List getPosts() { 85 | return new ArrayList<>(posts.values()); 86 | } 87 | } 88 | ``` 89 | 90 | ...and the `MutatePosts` implementation as such: 91 | 92 | ```java 93 | public class MutatePostsImpl implements MutatePosts { 94 | private AtomicInteger nextPostId = new AtomicInteger(1); 95 | private Map posts; 96 | public MutatePostsImpl(Map posts) { 97 | this.posts = posts; 98 | } 99 | @Override 100 | public Post createPost(MutatePosts.CreatePostArgs args) { 101 | InputPost req = args.getPost(); 102 | Post.Builder postBuilder = new Post.Builder() 103 | .withTitle(req.getTitle()) 104 | .withAuthor(new Author.Unresolved(req.getAuthorId())); 105 | Post post; 106 | synchronized ( posts ) { 107 | Integer id = nextPostId.incrementAndGet(); 108 | post = postBuilder.withId(id).build(); 109 | posts.put(id, post); 110 | } 111 | return post; 112 | } 113 | 114 | @Override 115 | public Post upvotePost(MutatePosts.UpvotePostArgs args) { 116 | synchronized ( posts ) { 117 | Post post = posts.get(args.getPostId()); 118 | if ( null == post ) { 119 | throw new NoSuchEntityException("PostId="+args.getPostId()); 120 | } 121 | Post upvoted = new Post.Builder(post) 122 | .withVotes(post.getVotes()+1) 123 | .build(); 124 | posts.put(args.getPostId(), upvoted); 125 | return upvoted; 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | ...and the `Author.Resolver` interface as such: 132 | 133 | ```java 134 | public class AuthorResolver implements Author.Resolver { 135 | private Map authors; 136 | public AuthorResolver(Map authors) { 137 | this.authors = authors; 138 | } 139 | @Override 140 | public List resolve(List unresolvedList) { 141 | List result = new ArrayList<>(); 142 | for ( Author unresolved : unresolvedList ) { 143 | // In a real app we would check if it is instanceof Author.Unresolved 144 | result.add(authors.get(unresolved.getId())); 145 | } 146 | return result; 147 | } 148 | } 149 | ``` 150 | 151 | ...and the `Post.Resolver` interface as such: 152 | 153 | ```java 154 | public class PostResolver implements Post.Resolver { 155 | private Map posts; 156 | public PostResolver(Map posts) { 157 | this.posts = posts; 158 | } 159 | @Override 160 | public List resolve(List unresolvedList) { 161 | List result = new ArrayList<>(); 162 | for ( Post unresolved : unresolvedList ) { 163 | if ( null == unresolved ) { 164 | result.add(null); 165 | } else { 166 | result.add(posts.get(unresolved.getId())); 167 | } 168 | } 169 | return result; 170 | } 171 | } 172 | ``` 173 | 174 | ...and you can use Guice to wire it all together as such (see below on 175 | using this from Spring): 176 | 177 | ```java 178 | public class MainModule implements AbstractModule { 179 | @Override 180 | protected void configure() { 181 | // Create the "data" used by the implementations: 182 | Map posts = new LinkedHashMap<>(); 183 | Map authors = new LinkedHashMap<>(); 184 | // Install the generated module: 185 | install(new PostsModule()); 186 | // Declare our implementations: 187 | bind(Author.Resolver.class) 188 | .toInstance(new AuthorResolver(authors)); 189 | bind(Post.Resolver.class) 190 | .toInstance(new PostResolver(posts)); 191 | bind(MutatePosts.class) 192 | .toInstance(new MutatePostsImpl(posts)); 193 | bind(QueryPosts.class) 194 | .toInstance(new QueryPostsImpl(posts)); 195 | } 196 | } 197 | ``` 198 | 199 | ...and to use it: 200 | 201 | ```java 202 | public class GraphQLServlet extends HttpServlet { 203 | private static ObjectMapper OM = new ObjectMapper(); 204 | private GraphQL graphQL; 205 | @Inject 206 | protected void GraphQLServlet(Map types) { 207 | GraphQLSchema schema = GraphQLSchema.newSchema() 208 | .query((GraphQLObjectType)types.get("QueryPosts")) 209 | .mutation((GraphQLObjectType)types.get("MutatePosts")) 210 | .build(new HashSet<>(types.values())); 211 | graphQL = new GraphQL(schema, new BatchedExecutionStrategy()); 212 | } 213 | protected void service(HttpServletRequest req, HttpServletResponse resp) { 214 | ExecutionResult result = graphQL.execute(req.getParameter("query")); 215 | OM.writeValue(resp.getOutputStream(), result); 216 | } 217 | } 218 | ``` 219 | 220 | This example is also a unit test which can be found 221 | [here](apigen/src/test/projects/posts/src/test/java/com/disteli/posts/PostsTest.java) 222 | 223 | ### Using Spring instead of Guice 224 | 225 | If you want to use Spring to wire the components together instead of Guice, you need to 226 | instruct Spring to include the generated code in a package-scan. Spring will find the `@Named` 227 | annotated components and will inject any dependencies (the type resolvers you implement, etc) 228 | 229 | For example, if your code was generated into the package `com.distelli.posts`, the spring 230 | configuration would look like this: 231 | 232 | ```java 233 | @ComponentScan("com.distelli.posts") 234 | @Configuration 235 | public class MyAppConfig { 236 | ... 237 | } 238 | ``` 239 | 240 | To generate a mapping similar to the guice code above, you can add this to your spring 241 | configuration: 242 | 243 | ```java 244 | @Bean 245 | public Map graphqlTypeMap(List> typeList) { 246 | return typeList.stream().map(Provider::get).collect(Collectors.toMap(GraphQLType::getName, Function.identity())); 247 | } 248 | ``` 249 | 250 | This will take any GraphQLTypes and generate a map of their string name to their implementation. 251 | 252 | ### Getting started 253 | 254 | #### How to use the latest release with Maven 255 | 256 | Generate the code with the following maven: 257 | 258 | ```xml 259 | 260 | ... 261 | 262 | 4.0.0 263 | 264 | 265 | 266 | 267 | ... 268 | 269 | com.distelli.graphql 270 | graphql-apigen 271 | ${apigen.version} 272 | 273 | 274 | com.example.my.MyGuiceModule 275 | 281 | com.example.my 282 | 284 | schema/folder 285 | 287 | output/folder 288 | 289 | 290 | 291 | java-apigen 292 | 293 | apigen 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | ... 303 | 304 | 305 | com.distelli.graphql 306 | graphql-apigen-deps 307 | ${apigen.version} 308 | 309 | 310 | 311 | 312 | com.google.inject 313 | guice 314 | 4.0 315 | 316 | 317 | 318 | com.google.inject.extensions 319 | guice-multibindings 320 | 4.0 321 | 322 | 323 | 324 | 325 | 326 | ``` 327 | 328 | Be sure to replace the values above with the correct values (and remove unnecessary configuration properties if the 329 | defaults are satisfactory). 330 | 331 | ### Customizing the Output 332 | 333 | You can customize the generated Java source by copying the [graphql-apigen.stg](apigen/src/main/resources/graphql-apigen.stg) 334 | file to the base directory of your project and making any necessary changes. The plugin will automatically use it 335 | instead of the one distributed with the library. The template uses the [StringTemplate](https://github.com/antlr/stringtemplate4/blob/master/doc/index.md) 336 | template language. The model used for the template is defined in [STModel.java](apigen/src/main/java/com/distelli/graphql/apigen/STModel.java). 337 | -------------------------------------------------------------------------------- /apigen/src/main/resources/graphql-apigen.stg: -------------------------------------------------------------------------------- 1 | // See https://github.com/antlr/stringtemplate4/blob/master/doc/groups.md 2 | group apigen; 3 | 4 | ////////////////////////////////////////////////////////////////////// 5 | // Define the objectType builder: 6 | objectTypeFileName(model) ::= ".java" 7 | objectTypeGenerator(model) ::= << 8 | package ; 9 | 10 | ;}> 13 | 14 | import java.util.List; 15 | 16 | import com.distelli.graphql.ResolveDataFetchingEnvironment; 17 | import graphql.schema.DataFetchingEnvironment; 18 | 19 | public interface extends ResolveDataFetchingEnvironment\<\>, }> { 20 | public static class Builder { 21 | 23 | 24 | private _; 25 | }> 26 | public Builder() {} 27 | public Builder( src) { 28 | 30 | 31 | _ = src.get(); 32 | }> 33 | } 34 | 35 | 37 | 38 | public Builder with( _) { 39 | this._ = _; 40 | return this; 41 | \} 42 | }> 43 | public build() { 44 | return new Impl(this); 45 | } 46 | } 47 | public static class Impl implements { 48 | 50 | 51 | private _; 52 | }> 53 | protected Impl(Builder builder) { 54 | 56 | 57 | this._ = builder._; 58 | }> 59 | } 60 | 62 | 63 | @Override 64 | public get() { 65 | return _; 66 | \} 67 | }> 68 | @Override 69 | public String toString() { 70 | return "{" 71 | 73 | 74 | + ", =" + _ 75 | }> 76 | 77 | + "}"; 78 | } 79 | // TODO: equals(Object) & hashCode() 80 | } 81 | 82 | public default resolve(DataFetchingEnvironment env) { 83 | return this; 84 | } 85 | 86 | 87 | public static class Unresolved implements { 88 | private _id; 89 | public Unresolved( id) { 90 | this._id = id; 91 | } 92 | @Override 93 | public getId() { 94 | return _id; 95 | } 96 | @Override 97 | public String toString() { 98 | return ".Unresolved{" 99 | + "id=" + _id 100 | + "}"; 101 | } 102 | } 103 | public static interface Resolver extends com.distelli.graphql.Resolver\<\> { 104 | public List\<\> resolve(List\<\> list); 105 | } 106 | 107 | 110 | // TODO: extend any implemented interfaces... 111 | interface Args { 112 | get() { return null; \}}> 115 | \} 116 | public default (Args args) { return null; \} 117 | 118 | public default get() { return null; \} 119 | }> 120 | } 121 | 122 | >> 123 | 124 | ////////////////////////////////////////////////////////////////////// 125 | // Define the object TypeProvider 126 | objectTypeProviderFileName(model) ::= "TypeProvider.java" 127 | objectTypeProviderGenerator(model) ::= << 128 | package ; 129 | 130 | ;}> 133 | import com.distelli.graphql.MethodDataFetcher; 134 | import com.distelli.graphql.ResolverDataFetcher; 135 | import graphql.Scalars; 136 | import graphql.schema.*; 137 | import java.util.Arrays; 138 | import javax.inject.Inject; 139 | import javax.inject.Provider; 140 | import javax.inject.Named; 141 | import java.util.Optional; 142 | 143 | @Named 144 | public class TypeProvider implements Provider\ { 145 | 147 | @Inject 148 | private Optional\<\> ; 149 | }> 150 | @Inject 151 | private Optional\<\> _impl; 152 | @Inject 153 | protected TypeProvider() {} 154 | @Override 155 | public GraphQLObjectType get() { 156 | return GraphQLObjectType.newObject() 157 | .name("") 158 | ) 162 | .name("") 163 | 164 | .argument(Arrays.asList( 165 | ") 169 | .type() 170 | 171 | .defaultValue() 172 | 173 | .build()}; separator=",\n">)) 174 | 175 | 176 | .dataFetcher(new ResolverDataFetcher( 177 | new MethodDataFetcher( 178 | "", 179 | .Args.classnull, 180 | _impl.orElse(null)), 181 | .orElse(null), 182 | )) 183 | 184 | .dataFetcher(new MethodDataFetcher( 185 | "", 186 | .Args.classnull, 187 | _impl.orElse(null))) 188 | 189 | .build())}> 190 | .build(); 191 | } 192 | } 193 | 194 | >> 195 | objectTypeProviderGuiceModule(model) ::= << 196 | types.addBinding("") 197 | .toProvider(.TypeProvider.class); 198 | OptionalBinder.newOptionalBinder(binder(), ..class); 199 | 200 | OptionalBinder.newOptionalBinder(binder(), ..Resolver.class); 201 | 202 | 203 | >> 204 | 205 | ////////////////////////////////////////////////////////////////////// 206 | // Define the inputObjectType builder: 207 | inputObjectTypeFileName(model) ::= ".java" 208 | inputObjectTypeGenerator(model) ::= << 209 | package ; 210 | 211 | ;}> 214 | 215 | public interface { 216 | public static class Builder { 217 | _;}> 220 | public Builder() {} 221 | public Builder( src) { 222 | = src.get();}> 225 | } 226 | 227 | ( _) { 230 | this._ = _; 231 | return this; 232 | \}}> 233 | public build() { 234 | return new Impl(this); 235 | } 236 | } 237 | public static class Impl implements { 238 | _;}> 241 | protected Impl(Builder builder) { 242 | = builder._;}> 245 | } 246 | get() { 250 | return _; 251 | \}}> 252 | @Override 253 | public String toString() { 254 | return "{" 255 | , =" + _}> 258 | 259 | + "}"; 260 | } 261 | // TODO: equals(Object) & hashCode() 262 | } 263 | get() { return null; \}}> 266 | } 267 | 268 | >> 269 | ////////////////////////////////////////////////////////////////////// 270 | // Define the object TypeProvider 271 | inputObjectTypeProviderFileName(model) ::= "TypeProvider.java" 272 | inputObjectTypeProviderGenerator(model) ::= << 273 | package ; 274 | 275 | ;}> 278 | import graphql.Scalars; 279 | import graphql.schema.*; 280 | import javax.inject.Inject; 281 | import javax.inject.Provider; 282 | import javax.inject.Named; 283 | 284 | @Named 285 | public class TypeProvider implements Provider\ { 286 | @Inject 287 | protected TypeProvider() {} 288 | @Override 289 | public GraphQLInputObjectType get() { 290 | return GraphQLInputObjectType.newInputObject() 291 | .name("") 292 | ) 296 | .name("") 297 | 298 | .defaultValue() 299 | 300 | .build())}> 301 | .build(); 302 | } 303 | } 304 | 305 | >> 306 | inputObjectTypeProviderGuiceModule(model) ::= << 307 | types.addBinding("") 308 | .toProvider(.TypeProvider.class); 309 | OptionalBinder.newOptionalBinder(binder(), ..class); 310 | 311 | >> 312 | 313 | ////////////////////////////////////////////////////////////////////// 314 | // Define the GuiceModule: 315 | guiceModule(packageName, className, configure) ::= << 316 | package ; 317 | import com.google.inject.AbstractModule; 318 | import com.google.inject.multibindings.MapBinder; 319 | import com.google.inject.multibindings.OptionalBinder; 320 | import graphql.schema.GraphQLType; 321 | 322 | public class extends AbstractModule { 323 | protected void configure() { 324 | MapBinder\ types = 325 | MapBinder.newMapBinder(binder(), String.class, GraphQLType.class); 326 | 327 | } 328 | } 329 | 330 | >> 331 | 332 | ////////////////////////////////////////////////////////////////////// 333 | // Define the interface builder: 334 | interfaceFileName(model) ::= ".java" 335 | interfaceGenerator(model) ::= << 336 | package ; 337 | 338 | ;}> 341 | 342 | public interface { 343 | Args { 345 | get() { return null; \}}> 347 | \} 348 | public default (Args args) { 349 | return null; 350 | \}}> 351 | 354 | interface Args { 355 | get();}> 358 | \} 359 | public (Args args); 360 | 361 | public get(); 362 | }> 363 | } 364 | 365 | >> 366 | 367 | ////////////////////////////////////////////////////////////////////// 368 | // Define the enum builder: 369 | enumFileName(model) ::= ".java" 370 | enumGenerator(model) ::= << 371 | package ; 372 | 373 | ;}> 376 | 377 | public enum { 378 | ,}> 381 | } 382 | 383 | >> 384 | 385 | enumTypeProviderFileName(model) ::= "TypeProvider.java" 386 | enumTypeProviderGenerator(model) ::= << 387 | package ; 388 | 389 | ;}> 392 | import graphql.schema.*; 393 | import javax.inject.Inject; 394 | import javax.inject.Provider; 395 | import javax.inject.Named; 396 | 397 | 398 | @Named 399 | public class TypeProvider implements Provider\ { 400 | @Inject 401 | protected TypeProvider() {} 402 | @Override 403 | public GraphQLEnumType get() { 404 | return GraphQLEnumType.newEnum() 405 | .name("") 406 | ", ., "")}> 409 | .build(); 410 | } 411 | } 412 | 413 | >> 414 | enumTypeProviderGuiceModule(model) ::= << 415 | types.addBinding("") 416 | .toProvider(.TypeProvider.class); 417 | >> 418 | -------------------------------------------------------------------------------- /apigen/src/main/java/com/distelli/graphql/apigen/STModel.java: -------------------------------------------------------------------------------- 1 | package com.distelli.graphql.apigen; 2 | 3 | import graphql.language.Definition; 4 | import graphql.language.EnumTypeDefinition; 5 | import graphql.language.EnumValueDefinition; 6 | import graphql.language.FieldDefinition; 7 | import graphql.language.InputObjectTypeDefinition; 8 | import graphql.language.InputValueDefinition; 9 | import graphql.language.InterfaceTypeDefinition; 10 | import graphql.language.ListType; 11 | import graphql.language.NonNullType; 12 | import graphql.language.ObjectTypeDefinition; 13 | import graphql.language.OperationTypeDefinition; 14 | import graphql.language.ScalarTypeDefinition; 15 | import graphql.language.SchemaDefinition; 16 | import graphql.language.Type; 17 | import graphql.language.TypeName; 18 | import graphql.language.UnionTypeDefinition; 19 | import graphql.language.Value; 20 | import java.util.ArrayList; 21 | import java.util.Collection; 22 | import java.util.Collections; 23 | import java.util.HashMap; 24 | import java.util.LinkedHashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.Set; 28 | import java.util.TreeSet; 29 | 30 | public class STModel { 31 | private static Map BUILTINS = new HashMap(){{ 32 | put("Int", null); 33 | put("Long", null); 34 | put("Float", null); 35 | put("String", null); 36 | put("Boolean", null); 37 | put("ID", null); 38 | put("BigInteger", "java.math.BigInteger"); 39 | put("BigDecimal", "java.math.BigDecimal"); 40 | put("Byte", null); 41 | put("Short", null); 42 | put("Char", null); 43 | }}; 44 | private static Map RENAME = new HashMap(){{ 45 | put("Int", "Integer"); 46 | put("ID", "String"); 47 | put("Char", "Character"); 48 | put("Float", "Double"); 49 | }}; 50 | public static class Builder { 51 | private TypeEntry typeEntry; 52 | private Map referenceTypes; 53 | public Builder withTypeEntry(TypeEntry typeEntry) { 54 | this.typeEntry = typeEntry; 55 | return this; 56 | } 57 | public Builder withReferenceTypes(Map referenceTypes) { 58 | this.referenceTypes = referenceTypes; 59 | return this; 60 | } 61 | public STModel build() { 62 | return new STModel(this); 63 | } 64 | } 65 | 66 | public static class DataResolver { 67 | public String fieldName; 68 | public String fieldType; 69 | public int listDepth; 70 | } 71 | 72 | public static class Interface { 73 | public String type; 74 | } 75 | 76 | public static class Arg { 77 | public String name; 78 | public String type; 79 | public String graphQLType; 80 | public String defaultValue; 81 | public Arg(String name, String type) { 82 | this.name = name; 83 | this.type = type; 84 | } 85 | public String getUcname() { 86 | return ucFirst(name); 87 | } 88 | } 89 | // Field of Interface, Object, InputObject, UnionType (no names), Enum (no types) 90 | public static class Field { 91 | public String name; 92 | public String type; 93 | public DataResolver dataResolver; 94 | public String graphQLType; 95 | public List args; 96 | public String defaultValue; 97 | public Field(String name, String type) { 98 | this.name = name; 99 | this.type = type; 100 | } 101 | public String getUcname() { 102 | return ucFirst(name); 103 | } 104 | } 105 | private TypeEntry typeEntry; 106 | private Map referenceTypes; 107 | private List fields; 108 | public List interfaces; 109 | private List imports; 110 | private Field idField; 111 | private boolean gotIdField = false; 112 | private STModel(Builder builder) { 113 | this.typeEntry = builder.typeEntry; 114 | this.referenceTypes = builder.referenceTypes; 115 | } 116 | 117 | public void validate() { 118 | // TODO: Validate that any Object "implements" actually implements 119 | // the interface so we can error before compile time... 120 | 121 | // these throw if there are any inconsistencies... 122 | getFields(); 123 | getImports(); 124 | getInterfaces(); 125 | } 126 | 127 | public boolean isObjectType() { 128 | return typeEntry.getDefinition() instanceof ObjectTypeDefinition; 129 | } 130 | 131 | public boolean isInterfaceType() { 132 | return typeEntry.getDefinition() instanceof InterfaceTypeDefinition; 133 | } 134 | 135 | public boolean isEnumType() { 136 | return typeEntry.getDefinition() instanceof EnumTypeDefinition; 137 | } 138 | 139 | public boolean isScalarType() { 140 | return typeEntry.getDefinition() instanceof ScalarTypeDefinition; 141 | } 142 | 143 | public boolean isUnionType() { 144 | return typeEntry.getDefinition() instanceof UnionTypeDefinition; 145 | } 146 | 147 | public boolean isInputObjectType() { 148 | return typeEntry.getDefinition() instanceof InputObjectTypeDefinition; 149 | } 150 | 151 | public boolean isSchemaType() { 152 | return typeEntry.getDefinition() instanceof SchemaDefinition; 153 | } 154 | 155 | public String getPackageName() { 156 | return typeEntry.getPackageName(); 157 | } 158 | 159 | public String getName() { 160 | return typeEntry.getName(); 161 | } 162 | 163 | public String getUcname() { 164 | return ucFirst(getName()); 165 | } 166 | 167 | private static String ucFirst(String name) { 168 | if ( null == name || name.length() < 1 ) return name; 169 | return name.substring(0, 1).toUpperCase() + name.substring(1); 170 | } 171 | 172 | private static String lcFirst(String name) { 173 | if ( null == name || name.length() < 1 ) return name; 174 | return name.substring(0, 1).toLowerCase() + name.substring(1); 175 | } 176 | 177 | public synchronized Field getIdField() { 178 | if ( ! gotIdField ) { 179 | for ( Field field : getFields() ) { 180 | if ( "id".equals(field.name) ) { 181 | idField = field; 182 | break; 183 | } 184 | } 185 | gotIdField = true; 186 | } 187 | return idField; 188 | } 189 | 190 | public List getInterfaces() { 191 | 192 | interfaces = new ArrayList<>(); 193 | 194 | if (!isObjectType()) { 195 | return interfaces; 196 | } 197 | 198 | ObjectTypeDefinition objectTypeDefinition = (ObjectTypeDefinition) typeEntry.getDefinition(); 199 | 200 | List interfaceTypes = objectTypeDefinition.getImplements(); 201 | 202 | for (Type anInterfaceType : interfaceTypes) { 203 | Interface anInterface = new Interface(); 204 | anInterface.type = toJavaTypeName(anInterfaceType); 205 | interfaces.add(anInterface); 206 | } 207 | 208 | return interfaces; 209 | } 210 | 211 | public List getDataResolvers() { 212 | Map resolvers = new LinkedHashMap<>(); 213 | for ( Field field : getFields() ) { 214 | DataResolver resolver = field.dataResolver; 215 | if ( null == resolver ) continue; 216 | resolvers.put(resolver.fieldType, resolver); 217 | } 218 | return new ArrayList<>(resolvers.values()); 219 | } 220 | 221 | public synchronized List getImports() { 222 | if ( null == imports ) { 223 | Definition def = typeEntry.getDefinition(); 224 | Set names = new TreeSet(); 225 | if ( isObjectType() ) { 226 | addImports(names, (ObjectTypeDefinition)def); 227 | } else if ( isInterfaceType() ) { 228 | addImports(names, (InterfaceTypeDefinition)def); 229 | } else if ( isInputObjectType() ) { 230 | addImports(names, (InputObjectTypeDefinition)def); 231 | } else if ( isUnionType() ) { 232 | addImports(names, (UnionTypeDefinition)def); 233 | } else if ( isEnumType() ) { 234 | addImports(names, (EnumTypeDefinition)def); 235 | } else if ( isSchemaType() ) { 236 | addImports(names, (SchemaDefinition)def); 237 | } 238 | imports = new ArrayList<>(names); 239 | } 240 | return imports; 241 | } 242 | 243 | public synchronized List getFields() { 244 | if ( null == fields ) { 245 | Definition def = typeEntry.getDefinition(); 246 | if ( isObjectType() ) { 247 | fields = getFields((ObjectTypeDefinition)def); 248 | } else if ( isInterfaceType() ) { 249 | fields = getFields((InterfaceTypeDefinition)def); 250 | } else if ( isInputObjectType() ) { 251 | fields = getFields((InputObjectTypeDefinition)def); 252 | } else if ( isUnionType() ) { 253 | fields = getFields((UnionTypeDefinition)def); 254 | } else if ( isEnumType() ) { 255 | fields = getFields((EnumTypeDefinition)def); 256 | } else if ( isSchemaType() ) { 257 | fields = getFields((SchemaDefinition)def); 258 | } else { 259 | fields = Collections.emptyList(); 260 | } 261 | } 262 | return fields; 263 | } 264 | 265 | private List getFields(ObjectTypeDefinition def) { 266 | List fields = new ArrayList(); 267 | for ( FieldDefinition fieldDef : def.getFieldDefinitions() ) { 268 | Field field = new Field(fieldDef.getName(), toJavaTypeName(fieldDef.getType())); 269 | field.graphQLType = toGraphQLType(fieldDef.getType()); 270 | field.dataResolver = toDataResolver(fieldDef.getType()); 271 | field.args = toArgs(fieldDef.getInputValueDefinitions()); 272 | fields.add(field); 273 | } 274 | return fields; 275 | } 276 | 277 | private List getFields(InterfaceTypeDefinition def) { 278 | List fields = new ArrayList(); 279 | for ( FieldDefinition fieldDef : def.getFieldDefinitions() ) { 280 | Field field = new Field(fieldDef.getName(), toJavaTypeName(fieldDef.getType())); 281 | field.args = toArgs(fieldDef.getInputValueDefinitions()); 282 | fields.add(field); 283 | } 284 | return fields; 285 | } 286 | 287 | private List getFields(InputObjectTypeDefinition def) { 288 | List fields = new ArrayList(); 289 | for ( InputValueDefinition fieldDef : def.getInputValueDefinitions() ) { 290 | Field field = new Field(fieldDef.getName(), toJavaTypeName(fieldDef.getType())); 291 | field.graphQLType = toGraphQLType(fieldDef.getType()); 292 | field.defaultValue = toJavaValue(fieldDef.getDefaultValue()); 293 | fields.add(field); 294 | } 295 | return fields; 296 | } 297 | 298 | private List getFields(UnionTypeDefinition def) { 299 | List fields = new ArrayList(); 300 | for ( Type type : def.getMemberTypes() ) { 301 | fields.add(new Field(null, toJavaTypeName(type))); 302 | } 303 | return fields; 304 | } 305 | 306 | private List getFields(EnumTypeDefinition def) { 307 | List fields = new ArrayList(); 308 | for ( EnumValueDefinition fieldDef : def.getEnumValueDefinitions() ) { 309 | fields.add(new Field(fieldDef.getName(), null)); 310 | } 311 | return fields; 312 | } 313 | 314 | private List getFields(SchemaDefinition def) { 315 | List fields = new ArrayList(); 316 | for ( OperationTypeDefinition fieldDef : def.getOperationTypeDefinitions() ) { 317 | fields.add(new Field(fieldDef.getName(), toJavaTypeName(fieldDef.getTypeName()))); 318 | } 319 | return fields; 320 | } 321 | 322 | private List toArgs(List defs) { 323 | List result = new ArrayList<>(); 324 | for ( InputValueDefinition def : defs ) { 325 | Arg arg = new Arg(def.getName(), toJavaTypeName(def.getType())); 326 | arg.graphQLType = toGraphQLType(def.getType()); 327 | arg.defaultValue = toJavaValue(def.getDefaultValue()); 328 | result.add(arg); 329 | } 330 | return result; 331 | } 332 | 333 | private String toJavaValue(Value value) { 334 | // TODO: Implement me! 335 | return null; 336 | } 337 | 338 | private DataResolver toDataResolver(Type type) { 339 | if ( type instanceof ListType ) { 340 | DataResolver resolver = toDataResolver(((ListType)type).getType()); 341 | if ( null == resolver ) return null; 342 | resolver.listDepth++; 343 | return resolver; 344 | } else if ( type instanceof NonNullType ) { 345 | return toDataResolver(((NonNullType)type).getType()); 346 | } else if ( type instanceof TypeName ) { 347 | String typeName = ((TypeName)type).getName(); 348 | if ( BUILTINS.containsKey(typeName) ) return null; 349 | TypeEntry typeEntry = referenceTypes.get(typeName); 350 | if ( !typeEntry.hasIdField() ) return null; 351 | DataResolver resolver = new DataResolver(); 352 | resolver.fieldType = typeName + ".Resolver"; 353 | resolver.fieldName = "_" + lcFirst(typeName) + "Resolver"; 354 | return resolver; 355 | } else { 356 | throw new UnsupportedOperationException("Unknown Type="+type.getClass().getName()); 357 | } 358 | } 359 | 360 | private String toGraphQLType(Type type) { 361 | if ( type instanceof ListType ) { 362 | return "new GraphQLList(" + toGraphQLType(((ListType)type).getType()) + ")"; 363 | } else if ( type instanceof NonNullType ) { 364 | return toGraphQLType(((NonNullType)type).getType()); 365 | } else if ( type instanceof TypeName ) { 366 | String name = ((TypeName)type).getName(); 367 | if ( BUILTINS.containsKey(name) ) { 368 | return "Scalars.GraphQL" + name; 369 | } 370 | return "new GraphQLTypeReference(\""+name+"\")"; 371 | } else { 372 | throw new UnsupportedOperationException("Unknown Type="+type.getClass().getName()); 373 | } 374 | } 375 | 376 | private String toJavaTypeName(Type type) { 377 | if ( type instanceof ListType ) { 378 | return "List<" + toJavaTypeName(((ListType)type).getType()) + ">"; 379 | } else if ( type instanceof NonNullType ) { 380 | return toJavaTypeName(((NonNullType)type).getType()); 381 | } else if ( type instanceof TypeName ) { 382 | String name = ((TypeName)type).getName(); 383 | String rename = RENAME.get(name); 384 | // TODO: scalar type directive to get implementation class... 385 | if ( null != rename ) return rename; 386 | return name; 387 | } else { 388 | throw new UnsupportedOperationException("Unknown Type="+type.getClass().getName()); 389 | } 390 | } 391 | 392 | private void addImports(Collection imports, ObjectTypeDefinition def) { 393 | for ( FieldDefinition fieldDef : def.getFieldDefinitions() ) { 394 | addImports(imports, fieldDef.getType()); 395 | } 396 | } 397 | 398 | private void addImports(Collection imports, InterfaceTypeDefinition def) { 399 | for ( FieldDefinition fieldDef : def.getFieldDefinitions() ) { 400 | addImports(imports, fieldDef.getType()); 401 | } 402 | } 403 | 404 | private void addImports(Collection imports, InputObjectTypeDefinition def) { 405 | for ( InputValueDefinition fieldDef : def.getInputValueDefinitions() ) { 406 | addImports(imports, fieldDef.getType()); 407 | } 408 | } 409 | 410 | private void addImports(Collection imports, UnionTypeDefinition def) { 411 | for ( Type type : def.getMemberTypes() ) { 412 | addImports(imports, type); 413 | } 414 | } 415 | 416 | private void addImports(Collection imports, EnumTypeDefinition def) { 417 | // No imports should be necessary... 418 | } 419 | 420 | private void addImports(Collection imports, SchemaDefinition def) { 421 | for ( OperationTypeDefinition fieldDef : def.getOperationTypeDefinitions() ) { 422 | addImports(imports, fieldDef.getTypeName()); 423 | } 424 | } 425 | 426 | private void addImports(Collection imports, Type type) { 427 | if ( type instanceof ListType ) { 428 | imports.add("java.util.List"); 429 | addImports(imports, ((ListType)type).getType()); 430 | } else if ( type instanceof NonNullType ) { 431 | addImports(imports, ((NonNullType)type).getType()); 432 | } else if ( type instanceof TypeName ) { 433 | String name = ((TypeName)type).getName(); 434 | if ( BUILTINS.containsKey(name) ) { 435 | String importName = BUILTINS.get(name); 436 | if ( null == importName ) return; 437 | imports.add(importName); 438 | } else { 439 | TypeEntry refEntry = referenceTypes.get(name); 440 | // TODO: scalar name may be different... should read annotations for scalars. 441 | if ( null == refEntry ) { 442 | throw new RuntimeException("Unknown type '"+name+"' was not defined in the schema"); 443 | } else { 444 | imports.add(refEntry.getPackageName() + "." + name); 445 | } 446 | } 447 | } else { 448 | throw new RuntimeException("Unknown Type="+type.getClass().getName()); 449 | } 450 | } 451 | } 452 | --------------------------------------------------------------------------------