diff --git a/README.md b/README.md index 6b286dec..8b5d6ed8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,426 @@ -# graphql-dyanmodb-manager +# graphql-builder +Builds a graphql schema from a model using reflection. +It reads parameter and method names of the java classes to build the schema. +It requires java11 and `-parameters` compile argument. This allows method argument names to be read removing the need for an annotations per argument. +This aproach means your method / argument names are limmited to valid java names. +This library is also designed with fine grained security requirements in mind. An example using this library can be found [here](https://github.com/ashley-taylor/graphql-aws-lamba-example) + + +- [graphql-builder](#graphql-builder) + - [Getting Started](#getting-started) + - [Creating an Entity](#creating-an-entity) + - [type entity](#type-entity) + - [Input entity](#input-entity) + - [Optional vs Required](#optional-vs-required) + - [Context](#context) + - [DataFetchingEnvironment](#datafetchingenvironment) + - [Query](#query) + - [Mutation](#mutation) + - [Subscriptions](#subscriptions) + - [Inheritance](#inheritance) + - [Ignore method](#ignore-method) + - [Enum](#enum) + - [Package Authorizer](#package-authorizer) + - [Entity type restrictions](#entity-type-restrictions) + - [Directives](#directives) + - [Scalar](#scalar) + + + +## Getting Started +To build the object you pass the package name to the schema builder class +```java +GraphQL build = SchemaBuilder.build("com.example.graph.schema.app").build(); +``` + +## Creating an Entity + +### type entity +```java +@Entity +public class User { + private String id; + private String name; + + @Id + public String getId() { + return id; + } + + public String getName() { + return name; + } +} +``` + +This defines a GraphQL output type that matches this schema + +```graphql +type User { + id: ID! + name: String! +} +``` +### Input entity + +To create an input entity specify input on the `@Entity` annotaion you can also specify `both` the input entity in this case will sufixed with `Input` + +```java +@Entity(SchemaOption.INPUT) +public class UserInput { + private String id; + private String name; + + + public void setId(@Id String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +This defines a graphql input entity that matches +```graphql +input User { + id: ID! + name: String! +} +``` + +## Optional vs Required +by default using this library all fields are required. If you want something to be optional wrap it with `Optional`. It is done this way since `Optional` is part of the JDK can has good 3rd party integration. + ```java +@Entity +public class User { + private String id; + private Optional name; + + @Id + public String getId() { + return id; + } + + public Optional getName() { + return name; + } +} +``` + +This defines a GraphQL output type that matches this schema + +```graphql +type User { + id: ID! + name: String +} +``` + + + +## Context +any method may include the context as a parameter. The context class must include the `@Context` annotation so it knows not to treat it as an argument. + +Defining context +```java +@Context +public class ApiContext { + private Database database; + public Database getDatabase() { + return database; + } +} +``` + +Calling +```java +public CompletableFuture
getAddress(ApiContext context) { + return context.getDatabase().getLink(this, Address.class); +} +``` + +## DataFetchingEnvironment +To have access to the `DataFetchingEnvironment` object just add it as an argument and it will be passed in + +## Query +To perform a query you add the `@Query` annotation to a static method. It does not need to be on the matching type static method with the package will be scanned. + +```java +@Query +public static CompletableFuture> users(ApiContext context, @Id String organisationId) { + return context.getDatabase().query(User.class); +} +``` +This will create the following schema +```graphql +extend type Query { + users(organisationId: ID!): [User!]! +} +``` +Again if you want anything to be optional use that java `Optional` class + +## Mutation +Mutatations are similar queries `@Mutation` must be applied to a static method. + +```java +@Mutation +public static CompletableFuture putUser(ApiContext context, @Id String organisationId, @Id Optional userId, String name) { + //insert logic +} +``` + +This will create the following schema +```graphql +extend type Mutation { + putUser(organisationId: ID!, userId: ID, name: String): User! +} +``` + +## Subscriptions +very similar to query add `@Subscription` and method must return a reactive `Publisher` + +```java +@Subscription +public static Publisher usersUpdated(ApiContext context, @Id String organisationId) { + //subscription logic +} +``` + +This will create the following schema +```graphql +extend type Subscription { + usersUpdated(organisationId: ID!): User! +} +``` + +## Inheritance +To create an inheritance type you can use `interface` or `abstract class` you need to add the `@Entity` annotation to the parent as well. Without that annotation inherited methods will be directly added to the type + +```java +@Entity +public abstract class Animal { + String name; + + public String getName() { + return name; + } +} + +@Entity +public class Cat extends Animal { + String meow; + + public String getMeow() { + return meow; + } +} +``` +This will create the following schema +```graphql +interface Animal { + name: String! +} + +type Cat implements Animal { + name: String! + meow: String! +} +``` + +## Ignore method +If there is a getter that you don't want exposed in the graphql schema add `@GraphQLIgnore` to the method + +```java +@Entity +public class User { + String id; + String dbId; + + @Id + public String getId() { + return id; + } + + @GraphQLIgnore + public String getDbId() { + return dbId; + } +} +``` +This will create the following schema +```graphql +type User { + id: ID! +} +``` + +## Enum +to create a GraphQL enum add the `@Entity` annotation to a java enum + +```java +public enum Animal { + CAT, + DOG +} +``` +This will create the following schema +```graphql +enum Animal { + CAT + DOG +} +``` + + +## Package Authorizer +The base package requires an Authorizer. This is a call that will determine if an endpoint is accessable. This will also be used by child packages unless they have also defined an Authorizer. + +This is designed for things like organisation access + +This class needs to implement a method called allow, that could look like something like the following. +```java +public class UserAuthorizer implements Authorizer { + public CompletableFuture allow(DataFetchingEnvironment env) { + ApiContext context = env.getContext(); + context.setOrganisationId(env.getArgument("organisationId")); + if(context.getUser() == null) { + return Promise.done(false); + } + return context.getUser().getMembership(context, context.getOrganisationId()).thenApply(membership -> { + if(membership == null) { + return false; + } + context.setOrganisationMembership(membership); + return true; + }); +} +``` + +## Entity type restrictions +If you have a permissions matrix that needs implemented this makes this easy. +It will validate all entries before returning them from the query. +Any that do not pass will be removed from the array or replaced with null. +This can lead to an error if the type is not optional. + +Using this approach it allows you to write your data access layer without worrying about permissions. +Return all matching entities from the method then have them automatically filter from everywhere in the application. + +To implement this you need to add an annotation to the class and implement the restriction factory +```java +@Entity +@Restrict(AnimalRestriction.class) +public class Animal { + ... +} + + +public class AnimalRestriction implements RestrictTypeFactory { + + @Override + public CompletableFuture> create(DataFetchingEnvironment env) { + ... + } +} + +public class AssetRestrict implements RestrictType { + + @Override + public CompletableFuture allow(Animal animal) { + ... + } +} +``` + +## Directives +These are similar to GraphQL directives but just implemented on the java model +You define a custom annotation and add the `@Directive` to it. +The directive annotation must contain an array of DirectiveLocations which will be used in the GraphQL definition. +Any function defined in the annotation will be placed on the schema definition as an argument. + +```java +@Retention(RUNTIME) +@Directive( { Introspection.DirectiveLocation.FIELD_DEFINITION } ) +public @interface CustomDirective { + String input(); +} +``` +This directive can now be placed where set: +```java +@Query +@CustomDirective(input = "Custom Directive Contents") +public static String sayHello() { + return "Hello world"; +} +``` +Which will then end up on the schema like so +```graphql +directive @CustomDirective(input: String!) on FIELD_DEFINITION + +type Query { + sayHello: String! @CustomDirective(input: "Custom Directive Contents") +} +``` + +## DataFetcherWrapper +Similar to the setup of a Directive the DataFetcherWrapper is created as an +annotation. This annotation is then passed into the DirectiveCaller allowing +you to add options to the annotation if need be + +```java +@Retention(RUNTIME) +@DataFetcherWrapper(AdminOnly.AdminOnlyDirective.class) +public @interface AdminOnly { + ... +} + +public class AdminOnlyDirective implements DirectiveCaller { + + @Override + public Object process(AdminOnly annotation, DataFetchingEnvironment env, DataFetcher fetcher) throws Exception { + ... + } +} +``` + +The annotation can then be used on any method + +```java +@Query +@AdminOnly +public static CompletableFuture> users(ApiContext context, @Id String organisationId) { + return context.getDatabase().query(User.class); +} +``` + +## Scalar + +To add a scalar you add the `@Scalar` Annotation this requires defining `Coercing` class + +```java + +@Scalar(Animal.CoercingImpl.class) +public class Animal { + public static class CoercingImpl implements Coercing { + + @Override + public Object serialize(Object dataFetcherResult) throws CoercingSerializeException { + return dataFetcherResult; + } + + @Override + public Animal parseValue(Object input) throws CoercingParseValueException { + return null; + } + + @Override + public Animal parseLiteral(Object input) throws CoercingParseLiteralException { + return null; + } + + } +} +``` diff --git a/graphql-builder/pom.xml b/graphql-builder/pom.xml new file mode 100644 index 00000000..79cbf343 --- /dev/null +++ b/graphql-builder/pom.xml @@ -0,0 +1,273 @@ + + + 4.0.0 + graphql-builder + Builds a graphql schema from a model using reflection + https://github.com/ashley-taylor/graphql-builder + + + com.fleetpin + graphql-database-manager + 3.0.4-SNAPSHOT + + + graphql-builder + + + 5.11.2 + UTF-8 + 2.18.0 + 1.17.0 + 1.2.1 + 22.3 + + + + + sonatype + central snapshot + https://oss.sonatype.org/content/repositories/snapshots + + + sonatype + central release + https://oss.sonatype.org/service/local/staging/deploy/maven2 + + + + + https://github.com/ashley-taylor/graphql-builder + scm:git:https://github.com/ashley-taylor/graphql-builder.git + scm:git:https://github.com/ashley-taylor/graphql-builder.git + HEAD + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Ashley Taylor + ashley.taylor@fleetpin.co.nz + Fleetpin + http://www.fleetpin.co.nz + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 21 + 21 + -parameters + + + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.10.1 + + + attach-javadocs + + jar + + + + + + com.hubspot.maven.plugins + prettier-maven-plugin + 0.22 + + 1.4.0 + 160 + 4 + true + true + true + + + + validate + + + + + org.pitest + pitest-maven + ${pitest.version} + + + org.pitest + pitest-junit5-plugin + ${pitest-junit5-plugin.version} + + + + 4 + false + false + + STRONGER + + + + + com.mycila + license-maven-plugin + 4.6 + + + +
src/license/license.txt
+
+
+
+
+ +
+
+ + + + com.graphql-java + graphql-java + ${graphql.version} + + + com.graphql-java + graphql-java-extended-scalars + 21.0 + test + + + org.reflections + reflections + 0.10.2 + + + jakarta.annotation + jakarta.annotation-api + 3.0.0 + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + test + + + com.fasterxml.jackson.module + jackson-module-parameter-names + ${jackson.version} + test + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${jackson.version} + test + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + test + + + io.reactivex.rxjava3 + rxjava + 3.1.9 + test + + + + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + + + org.skyscreamer + jsonassert + 1.5.3 + test + + + + + + + + sonatype + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.7 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.7.0 + true + + sonatype + https://oss.sonatype.org/ + true + + + + + + +
diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/Authorizer.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/Authorizer.java new file mode 100644 index 00000000..f6480f4f --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/Authorizer.java @@ -0,0 +1,14 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +public interface Authorizer {} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/AuthorizerSchema.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/AuthorizerSchema.java new file mode 100644 index 00000000..2eff1236 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/AuthorizerSchema.java @@ -0,0 +1,267 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static com.fleetpin.graphql.builder.EntityUtil.isContext; + +import graphql.GraphQLContext; +import graphql.GraphqlErrorException; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class AuthorizerSchema { + + private final DataFetcherRunner dataFetcherRunner; + private final Set basePackages; + private final Map targets; + + private AuthorizerSchema(DataFetcherRunner dataFetcherRunner, Set basePackages, Map targets) { + this.dataFetcherRunner = dataFetcherRunner; + this.basePackages = basePackages; + this.targets = targets; + } + + public static AuthorizerSchema build(DataFetcherRunner dataFetcherRunner, Set basePackage, Set> authorizers) + throws ReflectiveOperationException { + Map targets = new HashMap<>(); + + for (var type : authorizers) { + Authorizer auth = type.getDeclaredConstructor().newInstance(); + targets.put(type.getPackageName(), auth); + } + return new AuthorizerSchema(dataFetcherRunner, basePackage, targets); + } + + public Authorizer getAuthorizer(Class type) { + String name = type.getPackageName(); + Authorizer auth = null; + while (true) { + auth = targets.get(name); + if (auth == null) { + if (basePackages.contains(name)) { + return null; + } + if (name.indexOf('.') == -1) { + throw new RuntimeException("Referencing class outside base package " + type); + } + name = name.substring(0, name.lastIndexOf('.')); + } else { + return auth; + } + } + } + + public DataFetcher wrap(DataFetcher fetcher, Method method) { + Authorizer wrapper = getAuthorizer(method.getDeclaringClass()); + if (wrapper == null) { + return fetcher; + } + Set parameterNames = new HashSet<>(); + + for (var parameter : method.getParameters()) { + parameterNames.add(parameter.getName()); + } + + int longest = 0; + + Method[] targets = wrapper.getClass().getMethods(); + + for (Method target : targets) { + boolean valid = false; + valid |= target.getReturnType() == Boolean.TYPE; + + if (target.getReturnType().isAssignableFrom(CompletableFuture.class)) { + Type genericType = ((ParameterizedType) target.getGenericReturnType()).getActualTypeArguments()[0]; + if (Boolean.class.isAssignableFrom((Class) genericType)) { + valid = true; + } + } + if (!valid) { + continue; + } + if (target.getDeclaringClass().equals(Object.class)) { + continue; + } + int matched = 0; + for (var parameter : target.getParameters()) { + if (parameterNames.contains(parameter.getName())) { + matched++; + } + } + if (matched > longest) { + longest = matched; + } + } + + List toRun = new ArrayList<>(); + for (Method target : targets) { + boolean valid = false; + valid |= target.getReturnType() == Boolean.TYPE; + + if (target.getReturnType().isAssignableFrom(CompletableFuture.class)) { + Type genericType = ((ParameterizedType) target.getGenericReturnType()).getActualTypeArguments()[0]; + if (Boolean.class.isAssignableFrom((Class) genericType)) { + valid = true; + } + } + if (!valid) { + continue; + } + if (target.getDeclaringClass().equals(Object.class)) { + continue; + } + + int matched = 0; + for (var parameter : target.getParameters()) { + if (parameterNames.contains(parameter.getName())) { + matched++; + } + } + if (matched == longest) { + toRun.add(target); + } + } + + if (toRun.isEmpty()) { + throw new RuntimeException("No authorizer found for " + method); + } + + List> authRunners = toRun + .stream() + .>map(authorizer -> { + var count = authorizer.getParameterCount(); + + List> mappers = Arrays + .asList(authorizer.getParameters()) + .stream() + .map(parameter -> buildResolver(parameter.getName(), parameter.getType(), parameter.getAnnotations())) + .collect(Collectors.toList()); + + DataFetcher authFetcher = env -> { + Object[] args = new Object[count]; + + for (int i = 0; i < args.length; i++) { + args[i] = mappers.get(i).apply(env); + } + + return authorizer.invoke(wrapper, args); + }; + return dataFetcherRunner.manage(authorizer, authFetcher); + }) + .collect(Collectors.toList()); + + return env -> { + for (var authorizer : authRunners) { + try { + Object allow = authorizer.get(env); + + if (allow instanceof Boolean) { + if ((Boolean) allow) { + return fetcher.get(env); + } else { + throw GraphqlErrorException.newErrorException().message("unauthorized").errorClassification(ErrorType.UNAUTHORIZED).build(); + } + } else { + //only other type that passes checks above + CompletableFuture allowed = (CompletableFuture) allow; + + return allowed + .handle((r, e) -> { + if (e != null) { + if (e.getCause() instanceof Exception) { + e = e.getCause(); + } + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + throw new RuntimeException(e); + } + if (r) { + try { + return fetcher.get(env); + } catch (Throwable e1) { + if (e1.getCause() instanceof Exception) { + e1 = e1.getCause(); + } + if (e1 instanceof RuntimeException) { + throw (RuntimeException) e1; + } + throw new RuntimeException(e1); + } + } else { + throw new RuntimeException("Invalid access"); + } + }) + .thenCompose(a -> { + if (a instanceof CompletableFuture) { + return (CompletableFuture) a; + } else { + return CompletableFuture.completedFuture(a); + } + }); + } + } catch (InvocationTargetException e) { + if (e.getCause() instanceof Exception) { + throw (Exception) e.getCause(); + } else { + throw e; + } + } + } + return fetcher.get(env); + }; + } + + private Function buildResolver(String name, Class type, Annotation[] annotations) { + if (isContext(type, annotations)) { + if (type.isAssignableFrom(DataFetchingEnvironment.class)) { + return env -> env; + } + if (type.isAssignableFrom(GraphQLContext.class)) { + return env -> env.getGraphQlContext(); + } + return env -> { + var localContext = env.getLocalContext(); + if (localContext != null && type.isAssignableFrom(localContext.getClass())) { + return localContext; + } + + var context = env.getContext(); + if (context != null && type.isAssignableFrom(context.getClass())) { + return context; + } + + context = env.getGraphQlContext().get(name); + if (context != null && type.isAssignableFrom(context.getClass())) { + return context; + } + throw new RuntimeException("Context object " + name + " not found"); + }; + } + return env -> env.getArgument(name); + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DataFetcherRunner.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DataFetcherRunner.java new file mode 100644 index 00000000..1b134792 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DataFetcherRunner.java @@ -0,0 +1,8 @@ +package com.fleetpin.graphql.builder; + +import graphql.schema.DataFetcher; +import java.lang.reflect.Method; + +public interface DataFetcherRunner { + public DataFetcher manage(Method method, DataFetcher fetcher); +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DirectiveCaller.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DirectiveCaller.java new file mode 100644 index 00000000..73b3bd9d --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DirectiveCaller.java @@ -0,0 +1,20 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.lang.annotation.Annotation; + +public interface DirectiveCaller extends DirectiveOperation { + public Object process(T annotation, DataFetchingEnvironment env, DataFetcher fetcher) throws Exception; +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DirectiveOperation.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DirectiveOperation.java new file mode 100644 index 00000000..e278c2c7 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DirectiveOperation.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import java.lang.annotation.Annotation; + +/** + * Implementations are either + * DirectiveOperator is used to wrap a method call and modify it. Can be used for things like restrictions + * SchemaDirective is used to add directive information to the graphql schema + * + * + */ +public interface DirectiveOperation {} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DirectiveProcessor.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DirectiveProcessor.java new file mode 100644 index 00000000..18d429d8 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DirectiveProcessor.java @@ -0,0 +1,89 @@ +package com.fleetpin.graphql.builder; + +import com.fleetpin.graphql.builder.annotations.Directive; +import graphql.introspection.Introspection; +import graphql.schema.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; + +public class DirectiveProcessor { + + private final GraphQLDirective directive; + private final Map> builders; + + public DirectiveProcessor(GraphQLDirective directive, Map> builders) { + this.directive = directive; + this.builders = builders; + } + + public static DirectiveProcessor build(EntityProcessor entityProcessor, Class directive) { + var builder = GraphQLDirective.newDirective().name(directive.getSimpleName()); + var validLocations = directive.getAnnotation(Directive.class).value(); + // loop through and add valid locations + for (Introspection.DirectiveLocation location : validLocations) { + builder.validLocation(location); + } + + // Check for repeatable tag in annotation and add it + builder.repeatable(directive.getAnnotation(Directive.class).repeatable()); + + // Go through each argument and add name/type to directive + var methods = directive.getDeclaredMethods(); + Map> builders = new HashMap<>(); + for (Method method : methods) { + if (method.getParameterCount() != 0) { + continue; + } + var name = method.getName(); + + GraphQLArgument.Builder argument = GraphQLArgument.newArgument(); + argument.name(name); + + // Get the type of the argument from the return type of the method + TypeMeta innerMeta = new TypeMeta(null, method.getReturnType(), method.getGenericReturnType()); + var argumentType = entityProcessor.getEntity(innerMeta).getInputType(innerMeta, method.getAnnotations()); + argument.type(argumentType); + + // Add the argument to the directive builder to be used for declaration + builder.argument(argument); + + // Add a builder to the builders list (in order to populate applied directives) + builders.put( + name, + object -> { + try { + return GraphQLAppliedDirectiveArgument.newArgument().name(name).type(argumentType).valueProgrammatic(method.invoke(object)).build(); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + ); + } + return new DirectiveProcessor(builder.build(), builders); + } + + public void apply(Annotation annotation, Consumer builder) throws InvocationTargetException, IllegalAccessException { + var methods = annotation.annotationType().getDeclaredMethods(); + + // Create a new AppliedDirective which we will populate with the set values + var arguments = GraphQLAppliedDirective.newDirective(); + arguments.name(directive.getName()); + + // To get the value we loop through each method and get the method name and value + for (Method m : methods) { + // Using the builder created earlier populate the values of each method. + arguments.argument(builders.get(m.getName()).apply(annotation)); + } + + // Add the argument to the Builder + builder.accept(arguments.build()); + } + + public GraphQLDirective getDirective() { + return this.directive; + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DirectivesSchema.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DirectivesSchema.java new file mode 100644 index 00000000..ce6e59e7 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/DirectivesSchema.java @@ -0,0 +1,218 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import com.fleetpin.graphql.builder.annotations.DataFetcherWrapper; +import com.fleetpin.graphql.builder.annotations.Directive; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLAppliedDirective; +import graphql.schema.GraphQLDirective; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.reactivestreams.Publisher; + +class DirectivesSchema { + + private final Collection> global; + private final Map, DirectiveCaller> targets; + private final Collection> directives; + private Map, DirectiveProcessor> directiveProcessors; + + private DirectivesSchema( + Collection> global, + Map, DirectiveCaller> targets, + Collection> directives + ) { + this.global = global; + this.targets = targets; + this.directives = directives; + } + + //TODO:mess of exceptions + public static DirectivesSchema build(List> globalDirectives, Set> directiveTypes) throws ReflectiveOperationException { + Map, DirectiveCaller> targets = new HashMap<>(); + + Collection> allDirectives = new ArrayList<>(); + for (Class directiveType : directiveTypes) { + if (directiveType.isAnnotationPresent(DataFetcherWrapper.class)) { + Class> caller = directiveType.getAnnotation(DataFetcherWrapper.class).value(); + if (DirectiveCaller.class.isAssignableFrom(caller)) { + // TODO error for no zero args constructor + var callerInstance = (DirectiveCaller) caller.getConstructor().newInstance(); + targets.put((Class) directiveType, callerInstance); + } + continue; + } + if (!directiveType.isAnnotationPresent(Directive.class)) { + continue; + } + if (!directiveType.isAnnotation()) { + throw new RuntimeException("@Directive Annotation must only be placed on annotations"); + } + allDirectives.add((Class) directiveType); + } + + return new DirectivesSchema(globalDirectives, targets, allDirectives); + } + + private DirectiveCaller get(Annotation annotation) { + return targets.get(annotation.annotationType()); + } + + private DataFetcher wrap(DirectiveCaller directive, T annotation, DataFetcher fetcher) { + return env -> { + return directive.process(annotation, env, fetcher); + }; + } + + public Stream getSchemaDirective() { + return directiveProcessors.values().stream().map(DirectiveProcessor::getDirective); + } + + private DataFetcher wrap(RestrictTypeFactory directive, DataFetcher fetcher) { + //TODO: hate having this cache here would love to scope against the env object but nothing to hook into dataload caused global leak + Map> cache = Collections.synchronizedMap(new WeakHashMap<>()); + + return env -> + cache + .computeIfAbsent(env, key -> directive.create(key).thenApply(t -> t)) + .thenCompose(restrict -> { + try { + Object response = fetcher.get(env); + if (response instanceof CompletionStage) { + return ((CompletionStage) response).thenCompose(r -> applyRestrict(restrict, r)); + } + return applyRestrict(restrict, response); + } catch (Exception e) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + throw new RuntimeException(e); + } + }); + } + + public boolean target(Method method, TypeMeta meta) { + for (var global : this.global) { + //TODO: extract class + if (global.extractType().isAssignableFrom(meta.getType())) { + return true; + } + } + for (Annotation annotation : method.getAnnotations()) { + if (get(annotation) != null) { + return true; + } + } + return false; + } + + public DataFetcher wrap(Method method, TypeMeta meta, DataFetcher fetcher) { + for (var g : global) { + if (g.extractType().isAssignableFrom(meta.getType())) { + fetcher = wrap(g, fetcher); + } + } + for (Annotation annotation : method.getAnnotations()) { + DirectiveCaller directive = (DirectiveCaller) get(annotation); + if (directive != null) { + fetcher = wrap(directive, annotation, fetcher); + } + } + return fetcher; + } + + private CompletableFuture applyRestrict(RestrictType restrict, Object response) { + if (response instanceof List) { + return restrict.filter((List) response); + } else if (response instanceof Publisher) { + return CompletableFuture.completedFuture(new FilteredPublisher((Publisher) response, restrict)); + } else if (response instanceof Optional) { + var optional = (Optional) response; + if (optional.isEmpty()) { + return CompletableFuture.completedFuture(response); + } + var target = optional.get(); + if (target instanceof List) { + return restrict.filter((List) target); + } else { + return restrict + .allow(target) + .thenApply(allow -> { + if (allow == Boolean.TRUE) { + return response; + } else { + return Optional.empty(); + } + }); + } + } else { + return restrict + .allow(response) + .thenApply(allow -> { + if (allow == Boolean.TRUE) { + return response; + } else { + return null; + } + }); + } + } + + private static CompletableFuture> all(List> toReturn) { + return CompletableFuture + .allOf(toReturn.toArray(CompletableFuture[]::new)) + .thenApply(__ -> + toReturn + .stream() + .map(m -> { + try { + return m.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()) + ); + } + + public void addSchemaDirective(AnnotatedElement element, Class location, Consumer builder) { + for (Annotation annotation : element.getAnnotations()) { + var processor = this.directiveProcessors.get(annotation.annotationType()); + if (processor != null) { + try { + processor.apply(annotation, builder); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException("Could not process applied directive: " + location.getName()); + } + } + } + } + + public void processDirectives(EntityProcessor ep) { // Replacement of processSDL + Map, DirectiveProcessor> directiveProcessors = new HashMap<>(); + + this.directives.forEach(dir -> directiveProcessors.put(dir, DirectiveProcessor.build(ep, dir))); + this.directiveProcessors = directiveProcessors; + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/EntityHolder.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/EntityHolder.java new file mode 100644 index 00000000..d585319f --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/EntityHolder.java @@ -0,0 +1,291 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import com.fleetpin.graphql.builder.TypeMeta.Flag; +import com.fleetpin.graphql.builder.annotations.Id; +import com.fleetpin.graphql.builder.mapper.InputTypeBuilder; +import graphql.Scalars; +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLNamedInputType; +import graphql.schema.GraphQLNamedOutputType; +import graphql.schema.GraphQLNamedType; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLTypeReference; +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; + +public abstract class EntityHolder { + + private GraphQLNamedOutputType type; + private GraphQLNamedInputType inputType; + private InputTypeBuilder resolver; + + final GraphQLOutputType getType(TypeMeta meta, Annotation[] annotations) { + if (type == null) { + type = new GraphQLTypeReference(EntityUtil.getName(meta)); + type = buildType(); + } + GraphQLOutputType toReturn = getTypeInner(annotations); + return toType(meta, toReturn); + } + + private GraphQLOutputType toType(TypeMeta meta, GraphQLOutputType toReturn) { + boolean required = true; + for (var flag : meta.getFlags()) { + if (flag == Flag.OPTIONAL) { + required = false; + } + if (flag == Flag.ARRAY) { + if (required) { + toReturn = GraphQLNonNull.nonNull(toReturn); + } + toReturn = GraphQLList.list(toReturn); + required = true; + } + } + if (required) { + toReturn = GraphQLNonNull.nonNull(toReturn); + } + return toReturn; + } + + public final GraphQLNamedOutputType getInnerType(TypeMeta meta) { + if (type == null) { + type = new GraphQLTypeReference(EntityUtil.getName(meta)); + type = buildType(); + } + return type; + } + + private GraphQLNamedOutputType getTypeInner(Annotation[] annotations) { + if (annotations == null) { + return type; + } + for (Annotation an : annotations) { + if (an.annotationType().equals(Id.class)) { + return Scalars.GraphQLID; + } + } + return type; + } + + protected abstract GraphQLNamedOutputType buildType(); + + public final GraphQLInputType getInputType(TypeMeta meta, Annotation[] annotations) { + if (inputType == null) { + inputType = new GraphQLTypeReference(buildInputName()); + inputType = buildInput(); + } + GraphQLInputType toReturn = getInputTypeInner(annotations); + + boolean required = true; + for (var flag : meta.getFlags()) { + if (flag == Flag.OPTIONAL) { + required = false; + } + if (flag == Flag.ARRAY) { + if (required) { + toReturn = GraphQLNonNull.nonNull(toReturn); + } + toReturn = GraphQLList.list(toReturn); + required = true; + } + } + if (required) { + toReturn = GraphQLNonNull.nonNull(toReturn); + } + return toReturn; + } + + private GraphQLInputType getInputTypeInner(Annotation[] annotations) { + for (Annotation an : annotations) { + if (an.annotationType().equals(Id.class)) { + return Scalars.GraphQLID; + } + } + return inputType; + } + + protected abstract GraphQLNamedInputType buildInput(); + + protected abstract String buildInputName(); + + public Stream types() { + List types = new ArrayList<>(2); + if (type != null) { + types.add(type); + } + if (inputType != null && inputType != type) { + types.add(inputType); + } + return types.stream(); + } + + protected abstract InputTypeBuilder buildResolver(); + + public final InputTypeBuilder getResolver(TypeMeta meta) { + if (resolver == null) { + resolver = resolverPointer(); + resolver = buildResolver(); + } + var flags = new ArrayList<>(meta.getFlags()); + Collections.reverse(flags); + return process(meta.getTypes().iterator(), flags.iterator(), resolver); + } + + private InputTypeBuilder resolverPointer() { + return (obj, graphQLContext, locale) -> this.resolver.convert(obj, graphQLContext, locale); + } + + private static InputTypeBuilder process(Iterator> iterator, Iterator flags, InputTypeBuilder resolver) { + if (iterator.hasNext()) { + Flag flag = null; + if (flags.hasNext()) { + flag = flags.next(); + } + var type = iterator.next(); + + if (Optional.class.isAssignableFrom(type)) { + return processOptional(iterator, flags, resolver); + } + + if (flag == Flag.OPTIONAL) { + return processNull(type, iterator, flags, resolver); + } + + if (List.class.isAssignableFrom(type)) { + return processCollection(ArrayList::new, iterator, flags, resolver); + } + + if (Set.class.isAssignableFrom(type)) { + return processCollection(size -> new LinkedHashSet<>((int) (size / 0.75 + 1)), iterator, flags, resolver); + } + + if (type.isArray()) { + return processArray(type, iterator, flags, resolver); + } + + if (iterator.hasNext()) { + throw new RuntimeException("Unsupported type " + type); + } + + if (type.isEnum()) { + return processEnum((Class) type); + } + return resolver; + } + throw new RuntimeException("No type"); + } + + private static InputTypeBuilder processEnum(Class type) { + var constants = type.getEnumConstants(); + var map = new HashMap(); + for (var c : constants) { + map.put(c.name(), c); + } + + return (obj, context, locale) -> { + if (type.isInstance(obj)) { + return obj; + } + return map.get(obj); + }; + } + + private static InputTypeBuilder processOptional(Iterator> iterator, Iterator flags, InputTypeBuilder resolver) { + var mapper = process(iterator, flags, resolver); + return (obj, context, locale) -> { + if (obj instanceof Optional) { + if (((Optional) obj).isEmpty()) { + return obj; + } else { + obj = ((Optional) obj).get(); + } + } + if (obj == null) { + return Optional.empty(); + } + return Optional.of(mapper.convert(obj, context, locale)); + }; + } + + private static InputTypeBuilder processNull(Class type, Iterator> iterator, Iterator flags, InputTypeBuilder resolver) { + var classes = new ArrayList>(); + classes.add(type); + iterator.forEachRemaining(classes::add); + + var mapper = process(classes.iterator(), flags, resolver); + return (obj, context, locale) -> { + if (obj == null) { + return null; + } + return mapper.convert(obj, context, locale); + }; + } + + private static InputTypeBuilder processArray(Class type, Iterator> iterator, Iterator flags, InputTypeBuilder resolver) { + var component = type.getComponentType(); + if (component.isPrimitive()) { + throw new RuntimeException("Do not support primitive array"); + } + + var mapper = process(iterator, flags, resolver); + return (obj, context, locale) -> { + if (obj instanceof Collection) { + var collection = (Collection) obj; + Object[] toReturn = (Object[]) Array.newInstance(component, collection.size()); + int i = 0; + for (var c : collection) { + toReturn[i++] = mapper.convert(c, context, locale); + } + return toReturn; + } else { + throw new RuntimeException("Expected a Collection got " + obj.getClass()); + } + }; + } + + private static InputTypeBuilder processCollection( + Function create, + Iterator> iterator, + Iterator flags, + InputTypeBuilder resolver + ) { + var mapper = process(iterator, flags, resolver); + return (obj, context, locale) -> { + if (obj instanceof Collection) { + var collection = (Collection) obj; + var toReturn = create.apply(collection.size()); + for (var c : collection) { + toReturn.add(mapper.convert(c, context, locale)); + } + return toReturn; + } else { + throw new RuntimeException("Expected a Collection got " + obj.getClass()); + } + }; + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/EntityProcessor.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/EntityProcessor.java new file mode 100644 index 00000000..0deb63cb --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/EntityProcessor.java @@ -0,0 +1,182 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import com.fleetpin.graphql.builder.annotations.Scalar; +import com.fleetpin.graphql.builder.annotations.Union; +import com.fleetpin.graphql.builder.mapper.InputTypeBuilder; +import graphql.Scalars; +import graphql.schema.GraphQLAppliedDirective; +import graphql.schema.GraphQLCodeRegistry; +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLType; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class EntityProcessor { + + private final DirectivesSchema directives; + + private final Map entities; + private final MethodProcessor methodProcessor; + + EntityProcessor(DataFetcherRunner dataFetcherRunner, List scalars, DirectivesSchema diretives) { + this.methodProcessor = new MethodProcessor(dataFetcherRunner, this, diretives); + this.entities = new HashMap<>(); + addDefaults(); + addScalars(scalars); + + this.directives = diretives; + } + + private void addDefaults() { + put(Boolean.class, new ScalarEntity(Scalars.GraphQLBoolean)); + put(Boolean.TYPE, new ScalarEntity(Scalars.GraphQLBoolean)); + + put(Double.class, new ScalarEntity(Scalars.GraphQLFloat)); + put(Double.TYPE, new ScalarEntity(Scalars.GraphQLFloat)); + + put(Integer.class, new ScalarEntity(Scalars.GraphQLInt)); + put(Integer.TYPE, new ScalarEntity(Scalars.GraphQLInt)); + + put(String.class, new ScalarEntity(Scalars.GraphQLString)); + } + + private void addScalars(List scalars) { + for (var scalar : scalars) { + var coercing = scalar.getCoercing(); + var type = coercing.getClass(); + for (var method : type.getMethods()) { + if (method.isSynthetic()) { + continue; + } + if ("parseValue".equals(method.getName())) { + var returnType = method.getReturnType(); + if (returnType.equals(Long.class)) { + put(Long.TYPE, new ScalarEntity(scalar)); + } else if (returnType.equals(Byte.class)) { + put(Byte.TYPE, new ScalarEntity(scalar)); + } else if (returnType.equals(Character.class)) { + put(Character.TYPE, new ScalarEntity(scalar)); + } else if (returnType.equals(Float.class)) { + put(Float.TYPE, new ScalarEntity(scalar)); + } else if (returnType.equals(Short.class)) { + put(Short.TYPE, new ScalarEntity(scalar)); + } + put(returnType, new ScalarEntity(scalar)); + break; + } + } + } + } + + private void put(Class type, ScalarEntity entity) { + var name = EntityUtil.getName(new TypeMeta(null, type, type)); + entities.put(name, entity); + } + + Set getAdditionalTypes() { + return entities.values().stream().flatMap(s -> s.types()).collect(Collectors.toSet()); + } + + public EntityHolder getEntity(Class type) { + return getEntity(new TypeMeta(null, type, type)); + } + + EntityHolder getEntity(TypeMeta meta) { + String name = EntityUtil.getName(meta); + return entities.computeIfAbsent( + name, + __ -> { + Class type = meta.getType(); + Type genericType = meta.getGenericType(); + if (genericType == null) { + genericType = type; + } + try { + if (type.isAnnotationPresent(Scalar.class)) { + return new ScalarEntity(directives, meta); + } + if (type.isEnum()) { + return new EnumEntity(directives, meta); + } else { + return new ObjectEntity(this, meta); + } + } catch (ReflectiveOperationException | RuntimeException e) { + throw new RuntimeException("Failed to build schema for class " + type, e); + } + } + ); + } + + public GraphQLOutputType getType(TypeMeta meta, Annotation[] annotations) { + for (var annotation : annotations) { + if (annotation instanceof Union) { + var union = (Union) annotation; + return getUnionType(meta, union); + } + } + + return getEntity(meta).getType(meta, annotations); + } + + private GraphQLOutputType getUnionType(TypeMeta meta, Union union) { + var name = UnionType.name(union); + + return entities + .computeIfAbsent( + name, + __ -> { + try { + return new UnionType(this, union); + } catch (RuntimeException e) { + throw new RuntimeException("Failed to build schema for union " + union, e); + } + } + ) + .getType(meta, null); + } + + public GraphQLInputType getInputType(TypeMeta meta, Annotation[] annotations) { + return getEntity(meta).getInputType(meta, annotations); + } + + void addSchemaDirective(AnnotatedElement element, Class location, Consumer builder) { + this.directives.addSchemaDirective(element, location, builder); + } + + public InputTypeBuilder getResolver(TypeMeta meta) { + return getEntity(meta).getResolver(meta); + } + + public InputTypeBuilder getResolver(Class type) { + var meta = new TypeMeta(null, type, type); + return getEntity(meta).getResolver(meta); + } + + GraphQLCodeRegistry.Builder getCodeRegistry() { + return this.methodProcessor.getCodeRegistry(); + } + + MethodProcessor getMethodProcessor() { + return methodProcessor; + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/EntityUtil.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/EntityUtil.java new file mode 100644 index 00000000..f63efdf1 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/EntityUtil.java @@ -0,0 +1,172 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import com.fleetpin.graphql.builder.annotations.Context; +import com.fleetpin.graphql.builder.annotations.GraphQLIgnore; +import com.fleetpin.graphql.builder.annotations.GraphQLName; +import com.fleetpin.graphql.builder.annotations.InputIgnore; +import graphql.GraphQLContext; +import graphql.schema.DataFetchingEnvironment; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Optional; + +class EntityUtil { + + static String getName(TypeMeta meta) { + var type = meta.getType(); + + var genericType = meta.getGenericType(); + + var name = buildUpName(meta, type, genericType); + if (meta.isDirect()) { + name += "_DIRECT"; + } + + return name; + } + + private static String buildUpName(TypeMeta meta, Class type, Type genericType) { + String name = type.getSimpleName(); + + for (int i = 0; i < type.getTypeParameters().length; i++) { + if (genericType instanceof ParameterizedType) { + var t = ((ParameterizedType) genericType).getActualTypeArguments()[i]; + if (t instanceof Class) { + String extra = ((Class) t).getSimpleName(); + name += "_" + extra; + } else if (t instanceof TypeVariable) { + var variable = (TypeVariable) t; + Class extra = meta.resolveToType(variable); + if (extra != null) { + name += "_" + extra.getSimpleName(); + } + } else if (t instanceof ParameterizedType pType) { + var rawType = pType.getRawType(); + if (rawType instanceof Class rawClass) { + var extra = buildUpName(meta, rawClass, pType); + name += "_" + extra; + } else { + throw new RuntimeException("Generics are more complex that logic currently can handle"); + } + } + } else { + Class extra = meta.resolveToType(type.getTypeParameters()[i]); + if (extra != null) { + name += "_" + extra.getSimpleName(); + } + } + } + return name; + } + + public static Optional getter(Method method) { + if (method.isSynthetic()) { + return Optional.empty(); + } + if (method.getDeclaringClass().equals(Object.class)) { + return Optional.empty(); + } + if (method.isAnnotationPresent(GraphQLIgnore.class)) { + return Optional.empty(); + } + // will also be on implementing class + if (Modifier.isAbstract(method.getModifiers()) || method.getDeclaringClass().isInterface()) { + return Optional.empty(); + } + if (Modifier.isStatic(method.getModifiers())) { + return Optional.empty(); + } else if (method.getName().matches("(get|is)[A-Z].*")) { + String name; + if (method.getName().startsWith("get")) { + name = method.getName().substring("get".length(), "get".length() + 1).toLowerCase() + method.getName().substring("get".length() + 1); + } else { + name = method.getName().substring("is".length(), "is".length() + 1).toLowerCase() + method.getName().substring("is".length() + 1); + } + return Optional.of(getName(name, method)); + } + return Optional.empty(); + } + + public static Optional setter(Method method) { + if (method.isSynthetic()) { + return Optional.empty(); + } + if (method.getDeclaringClass().equals(Object.class)) { + return Optional.empty(); + } + if (method.isAnnotationPresent(GraphQLIgnore.class)) { + return Optional.empty(); + } + // will also be on implementing class + if (Modifier.isAbstract(method.getModifiers()) || method.getDeclaringClass().isInterface()) { + return Optional.empty(); + } + if (Modifier.isStatic(method.getModifiers())) { + return Optional.empty(); + } else if (method.getName().matches("set[A-Z].*")) { + if (method.getParameterCount() == 1 && !method.isAnnotationPresent(InputIgnore.class)) { + String name = method.getName().substring("set".length(), "set".length() + 1).toLowerCase() + method.getName().substring("set".length() + 1); + return Optional.of(getName(name, method)); + } + } + return Optional.empty(); + } + + static String getName(String fallback, AnnotatedElement... annotated) { + for (var anno : annotated) { + if (anno.isAnnotationPresent(GraphQLName.class)) { + return anno.getAnnotation(GraphQLName.class).value(); + } + } + return fallback; + } + + static boolean isContext(Class class1, Annotation[] annotations) { + for (var annotation : annotations) { + if (annotation instanceof Context) { + return true; + } + } + return ( + class1.isAssignableFrom(GraphQLContext.class) || class1.isAssignableFrom(DataFetchingEnvironment.class) || class1.isAnnotationPresent(Context.class) + ); + } + + static T getAnnotation(Class type, Class annotation) { + var response = type.getAnnotation(annotation); + if (response != null) { + return response; + } + + if (type.getSuperclass() != null) { + response = getAnnotation(type.getSuperclass(), annotation); + if (response != null) { + return response; + } + } + + for (var parent : type.getInterfaces()) { + response = getAnnotation(parent, annotation); + if (response != null) { + return response; + } + } + return null; + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/EnumEntity.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/EnumEntity.java new file mode 100644 index 00000000..b05bc208 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/EnumEntity.java @@ -0,0 +1,78 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static graphql.schema.GraphQLEnumValueDefinition.newEnumValueDefinition; + +import com.fleetpin.graphql.builder.annotations.GraphQLDescription; +import com.fleetpin.graphql.builder.annotations.GraphQLIgnore; +import com.fleetpin.graphql.builder.mapper.InputTypeBuilder; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLNamedInputType; +import graphql.schema.GraphQLNamedOutputType; + +public class EnumEntity extends EntityHolder { + + private final GraphQLEnumType enumType; + + public EnumEntity(DirectivesSchema directives, TypeMeta meta) throws ReflectiveOperationException { + graphql.schema.GraphQLEnumType.Builder enumType = GraphQLEnumType.newEnum(); + String typeName = EntityUtil.getName(meta); + enumType.name(typeName); + + var type = meta.getType(); + + var description = type.getAnnotation(GraphQLDescription.class); + if (description != null) { + enumType.description(description.value()); + } + + Object[] enums = type.getEnumConstants(); + for (Object e : enums) { + Enum a = (Enum) e; + var field = type.getDeclaredField(e.toString()); + if (field.isAnnotationPresent(GraphQLIgnore.class)) { + continue; + } + var name = EntityUtil.getName(a.name(), field); + var valueDef = newEnumValueDefinition().name(a.name()).value(a); + var desc = field.getAnnotation(GraphQLDescription.class); + if (desc != null) { + valueDef.description(desc.value()); + } + + enumType.value(valueDef.build()); + } + directives.addSchemaDirective(type, type, enumType::withAppliedDirective); + this.enumType = enumType.build(); + } + + @Override + protected GraphQLNamedInputType buildInput() { + return enumType; + } + + @Override + protected GraphQLNamedOutputType buildType() { + return enumType; + } + + @Override + protected String buildInputName() { + return enumType.getName(); + } + + @Override + protected InputTypeBuilder buildResolver() { + return null; + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/ErrorType.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/ErrorType.java new file mode 100644 index 00000000..16e7e679 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/ErrorType.java @@ -0,0 +1,18 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import graphql.ErrorClassification; + +enum ErrorType implements ErrorClassification { + UNAUTHORIZED, +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/FilteredPublisher.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/FilteredPublisher.java new file mode 100644 index 00000000..afe8cc91 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/FilteredPublisher.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +class FilteredPublisher implements Publisher { + + private final RestrictType restrict; + private final Publisher publisher; + + public FilteredPublisher(Publisher publisher, RestrictType restrict) { + this.publisher = publisher; + this.restrict = restrict; + } + + @Override + public void subscribe(Subscriber subscriber) { + publisher.subscribe( + new Subscriber() { + private CompletableFuture current = CompletableFuture.completedFuture(null); + private Subscription s; + + @Override + public void onSubscribe(Subscription s) { + this.s = s; + subscriber.onSubscribe(s); + } + + @Override + public void onNext(T t) { + synchronized (this) { + current = current.thenCompose(__ -> restrict.allow(t)).whenComplete(process(subscriber, t)); + } + } + + private BiConsumer process(Subscriber subscriber, T t) { + return (allow, error) -> { + if (error != null) { + subscriber.onError(error); + return; + } + if (allow) { + subscriber.onNext(t); + } else { + s.request(1); + } + }; + } + + @Override + public void onError(Throwable t) { + synchronized (this) { + current = + current.whenComplete((__, error) -> { + if (error != null) { + subscriber.onError(error); + } + subscriber.onError(t); + }); + } + } + + @Override + public void onComplete() { + synchronized (this) { + current = + current.whenComplete((__, error) -> { + if (error != null) { + subscriber.onError(error); + } + subscriber.onComplete(); + }); + } + } + } + ); + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/InputBuilder.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/InputBuilder.java new file mode 100644 index 00000000..ff00057f --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/InputBuilder.java @@ -0,0 +1,289 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.GraphQLDescription; +import com.fleetpin.graphql.builder.annotations.GraphQLIgnore; +import com.fleetpin.graphql.builder.annotations.InputIgnore; +import com.fleetpin.graphql.builder.annotations.OneOf; +import com.fleetpin.graphql.builder.annotations.SchemaOption; +import com.fleetpin.graphql.builder.mapper.ConstructorFieldBuilder; +import com.fleetpin.graphql.builder.mapper.ConstructorFieldBuilder.RecordMapper; +import com.fleetpin.graphql.builder.mapper.InputTypeBuilder; +import com.fleetpin.graphql.builder.mapper.ObjectFieldBuilder; +import com.fleetpin.graphql.builder.mapper.ObjectFieldBuilder.FieldMapper; +import com.fleetpin.graphql.builder.mapper.OneOfBuilder; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInputObjectType.Builder; +import graphql.schema.GraphQLNamedInputType; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; + +public abstract class InputBuilder { + + protected final EntityProcessor entityProcessor; + protected final TypeMeta meta; + + public InputBuilder(EntityProcessor entityProcessor, TypeMeta meta) { + this.entityProcessor = entityProcessor; + this.meta = meta; + } + + protected String buildName() { + var schemaType = SchemaOption.BOTH; + Entity graphTypeAnnotation = meta.getType().getAnnotation(Entity.class); + if (graphTypeAnnotation != null) { + schemaType = graphTypeAnnotation.value(); + } + + String typeName = EntityUtil.getName(meta); + + String inputName = typeName; + if (schemaType != SchemaOption.INPUT) { + inputName += "Input"; + } + return inputName; + } + + public GraphQLNamedInputType buildInput() { + GraphQLInputObjectType.Builder graphInputType = GraphQLInputObjectType.newInputObject(); + var type = meta.getType(); + + graphInputType.name(buildName()); + + { + var description = type.getAnnotation(GraphQLDescription.class); + if (description != null) { + graphInputType.description(description.value()); + } + } + + processFields(graphInputType); + + entityProcessor.addSchemaDirective(type, type, graphInputType::withAppliedDirective); + return graphInputType.build(); + } + + abstract void processFields(Builder graphInputType); + + protected abstract InputTypeBuilder resolve(); + + public static class OneOfInputBuilder extends InputBuilder { + + public OneOfInputBuilder(EntityProcessor entityProcessor, TypeMeta meta) { + super(entityProcessor, meta); + } + + @Override + void processFields(Builder graphInputType) { + var oneOf = meta.getType().getAnnotation(OneOf.class); + for (var oneOfType : oneOf.value()) { + var name = oneOfType.name(); + GraphQLInputObjectField.Builder field = GraphQLInputObjectField.newInputObjectField(); + field.name(name); + if (!oneOfType.description().isEmpty()) { + field.description(oneOfType.description()); + } + TypeMeta innerMeta = new TypeMeta(meta, oneOfType.type(), oneOfType.type()); + innerMeta.optional(); + var type = entityProcessor.getEntity(innerMeta).getInputType(innerMeta, new Annotation[0]); + field.type(type); + graphInputType.field(field); + } + } + + @Override + protected InputTypeBuilder resolve() { + var oneOf = meta.getType().getAnnotation(OneOf.class); + return new OneOfBuilder(entityProcessor, meta.getType(), oneOf); + } + } + + public static class ObjectType extends InputBuilder { + + public ObjectType(EntityProcessor entityProcessor, TypeMeta meta) { + super(entityProcessor, meta); + } + + @Override + void processFields(Builder graphInputType) { + for (Method method : meta.getType().getMethods()) { + try { + var name = EntityUtil.setter(method); + if (name.isPresent()) { + GraphQLInputObjectField.Builder field = GraphQLInputObjectField.newInputObjectField(); + field.name(name.get()); + entityProcessor.addSchemaDirective(method, meta.getType(), field::withAppliedDirective); + TypeMeta innerMeta = new TypeMeta(meta, method.getParameterTypes()[0], method.getGenericParameterTypes()[0], method.getParameters()[0]); + var entity = entityProcessor.getEntity(innerMeta); + var inputType = entity.getInputType(innerMeta, method.getParameterAnnotations()[0]); + field.type(inputType); + + var description = method.getAnnotation(GraphQLDescription.class); + if (description != null) { + field.description(description.value()); + } + + graphInputType.field(field); + } + } catch (RuntimeException e) { + throw new RuntimeException("Failed to process method " + method, e); + } + } + } + + @Override + public InputTypeBuilder resolve() { + var fieldMappers = new ArrayList(); + + for (Method method : meta.getType().getMethods()) { + try { + var name = EntityUtil.setter(method); + if (name.isPresent()) { + TypeMeta innerMeta = new TypeMeta(meta, method.getParameterTypes()[0], method.getGenericParameterTypes()[0], method.getParameters()[0]); + fieldMappers.add(FieldMapper.build(entityProcessor, innerMeta, name.get(), method)); + } + } catch (RuntimeException e) { + throw new RuntimeException("Failed to process method " + method, e); + } + } + + return new ObjectFieldBuilder(meta.getType(), fieldMappers); + } + } + + public static class ObjectConstructorType extends InputBuilder { + + private final Constructor constructor; + + public ObjectConstructorType(EntityProcessor entityProcessor, TypeMeta meta, Constructor constructor) { + super(entityProcessor, meta); + this.constructor = constructor; + } + + @Override + void processFields(Builder graphInputType) { + for (var parameter : constructor.getParameters()) { + try { + GraphQLInputObjectField.Builder field = GraphQLInputObjectField.newInputObjectField(); + field.name(parameter.getName()); + entityProcessor.addSchemaDirective(parameter, meta.getType(), field::withAppliedDirective); + TypeMeta innerMeta = new TypeMeta(meta, parameter.getType(), parameter.getParameterizedType(), parameter); + var entity = entityProcessor.getEntity(innerMeta); + var inputType = entity.getInputType(innerMeta, parameter.getAnnotations()); + + var description = parameter.getAnnotation(GraphQLDescription.class); + if (description != null) { + field.description(description.value()); + } + field.type(inputType); + graphInputType.field(field); + } catch (RuntimeException e) { + throw new RuntimeException("Failed to process method " + parameter, e); + } + } + } + + @Override + public InputTypeBuilder resolve() { + var fieldMappers = new ArrayList(); + + for (var parameter : constructor.getParameters()) { + TypeMeta innerMeta = new TypeMeta(meta, parameter.getType(), parameter.getParameterizedType(), parameter); + var resolver = entityProcessor.getResolver(innerMeta); + fieldMappers.add(new RecordMapper(parameter.getName(), parameter.getType(), resolver)); + } + + return new ConstructorFieldBuilder(meta.getType(), fieldMappers); + } + } + + public static class Record extends InputBuilder { + + public Record(EntityProcessor entityProcessor, TypeMeta meta) { + super(entityProcessor, meta); + } + + @Override + void processFields(Builder graphInputType) { + for (var field : this.meta.getType().getDeclaredFields()) { + try { + if (field.isSynthetic()) { + continue; + } + if (field.isAnnotationPresent(GraphQLIgnore.class)) { + continue; + } + if (Modifier.isStatic(field.getModifiers())) { + continue; + } else { + // getter type + if (!field.isAnnotationPresent(InputIgnore.class)) { + String name = EntityUtil.getName(field.getName(), field); + GraphQLInputObjectField.Builder fieldBuilder = GraphQLInputObjectField.newInputObjectField(); + fieldBuilder.name(name); + entityProcessor.addSchemaDirective(field, meta.getType(), fieldBuilder::withAppliedDirective); + TypeMeta innerMeta = new TypeMeta(meta, field.getType(), field.getGenericType(), field); + var entity = entityProcessor.getEntity(innerMeta); + var inputType = entity.getInputType(innerMeta, field.getAnnotations()); + + var description = field.getAnnotation(GraphQLDescription.class); + if (description != null) { + fieldBuilder.description(description.value()); + } + + fieldBuilder.type(inputType); + graphInputType.field(fieldBuilder); + } + } + } catch (RuntimeException e) { + throw new RuntimeException("Failed to process method " + field, e); + } + } + } + + @Override + protected InputTypeBuilder resolve() { + try { + var fieldMappers = new ArrayList(); + + for (var field : this.meta.getType().getDeclaredFields()) { + if (field.isSynthetic()) { + continue; + } + if (field.isAnnotationPresent(GraphQLIgnore.class)) { + continue; + } + if (Modifier.isStatic(field.getModifiers())) { + continue; + } else { + // getter type + if (!field.isAnnotationPresent(InputIgnore.class)) { + TypeMeta innerMeta = new TypeMeta(meta, field.getType(), field.getGenericType(), field); + var resolver = entityProcessor.getResolver(innerMeta); + var name = EntityUtil.getName(field.getName(), field); + fieldMappers.add(new RecordMapper(name, field.getType(), resolver)); + } + } + } + return new ConstructorFieldBuilder(meta.getType(), fieldMappers); + } catch (RuntimeException e) { + throw new RuntimeException("Failed to process " + this.meta.getType(), e); + } + } + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/MethodProcessor.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/MethodProcessor.java new file mode 100644 index 00000000..8bef984f --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/MethodProcessor.java @@ -0,0 +1,249 @@ +package com.fleetpin.graphql.builder; + +import static com.fleetpin.graphql.builder.EntityUtil.isContext; + +import com.fleetpin.graphql.builder.annotations.Directive; +import com.fleetpin.graphql.builder.annotations.GraphQLDeprecated; +import com.fleetpin.graphql.builder.annotations.GraphQLDescription; +import com.fleetpin.graphql.builder.annotations.Mutation; +import com.fleetpin.graphql.builder.annotations.Query; +import com.fleetpin.graphql.builder.annotations.Subscription; +import graphql.GraphQLContext; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLAppliedDirective; +import graphql.schema.GraphQLAppliedDirectiveArgument; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLCodeRegistry; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLFieldDefinition.Builder; +import graphql.schema.GraphQLObjectType; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.function.Function; + +class MethodProcessor { + + private final DataFetcherRunner dataFetcherRunner; + private final EntityProcessor entityProcessor; + private final DirectivesSchema diretives; + + private final GraphQLCodeRegistry.Builder codeRegistry; + + private final GraphQLObjectType.Builder graphQuery; + private final GraphQLObjectType.Builder graphMutations; + private final GraphQLObjectType.Builder graphSubscriptions; + + public MethodProcessor(DataFetcherRunner dataFetcherRunner, EntityProcessor entityProcessor, DirectivesSchema diretives) { + this.dataFetcherRunner = dataFetcherRunner; + this.entityProcessor = entityProcessor; + this.diretives = diretives; + this.codeRegistry = GraphQLCodeRegistry.newCodeRegistry(); + + this.graphQuery = GraphQLObjectType.newObject(); + graphQuery.name("Query"); + this.graphMutations = GraphQLObjectType.newObject(); + graphMutations.name("Mutations"); + this.graphSubscriptions = GraphQLObjectType.newObject(); + graphSubscriptions.name("Subscriptions"); + } + + void process(AuthorizerSchema authorizer, Method method) throws ReflectiveOperationException { + if (!Modifier.isStatic(method.getModifiers())) { + throw new RuntimeException("End point must be a static method"); + } + FieldCoordinates coordinates; + GraphQLObjectType.Builder object; + if (method.isAnnotationPresent(Query.class)) { + coordinates = FieldCoordinates.coordinates("Query", EntityUtil.getName(method.getName(), method)); + object = graphQuery; + } else if (method.isAnnotationPresent(Mutation.class)) { + coordinates = FieldCoordinates.coordinates("Mutations", EntityUtil.getName(method.getName(), method)); + object = graphMutations; + } else if (method.isAnnotationPresent(Subscription.class)) { + coordinates = FieldCoordinates.coordinates("Subscriptions", EntityUtil.getName(method.getName(), method)); + object = graphSubscriptions; + } else { + return; + } + + object.field(process(authorizer, coordinates, null, method)); + } + + Builder process(AuthorizerSchema authorizer, FieldCoordinates coordinates, TypeMeta parentMeta, Method method) + throws InvocationTargetException, IllegalAccessException { + GraphQLFieldDefinition.Builder field = GraphQLFieldDefinition.newFieldDefinition(); + + entityProcessor.addSchemaDirective(method, method.getDeclaringClass(), field::withAppliedDirective); + + var deprecated = method.getAnnotation(GraphQLDeprecated.class); + if (deprecated != null) { + field.deprecate(deprecated.value()); + } + + var description = method.getAnnotation(GraphQLDescription.class); + if (description != null) { + field.description(description.value()); + } + + field.name(coordinates.getFieldName()); + + TypeMeta meta = new TypeMeta(parentMeta, method.getReturnType(), method.getGenericReturnType(), method); + var type = entityProcessor.getType(meta, method.getAnnotations()); + field.type(type); + for (int i = 0; i < method.getParameterCount(); i++) { + var parameter = method.getParameters()[i]; + GraphQLArgument.Builder argument = GraphQLArgument.newArgument(); + if (isContext(parameter.getType(), parameter.getAnnotations())) { + continue; + } + + TypeMeta inputMeta = new TypeMeta(null, parameter.getType(), method.getGenericParameterTypes()[i], parameter); + argument.type(entityProcessor.getInputType(inputMeta, method.getParameterAnnotations()[i])); // TODO:dirty cast + + description = parameter.getAnnotation(GraphQLDescription.class); + if (description != null) { + argument.description(description.value()); + } + + for (Annotation annotation : parameter.getAnnotations()) { + // Check to see if the annotation is a directive + if (!annotation.annotationType().isAnnotationPresent(Directive.class)) { + continue; + } + var annotationType = annotation.annotationType(); + // Get the values out of the directive annotation + var methods = annotationType.getDeclaredMethods(); + + // Get the applied directive and add it to the argument + var appliedDirective = getAppliedDirective(annotation, annotationType, methods); + argument.withAppliedDirective(appliedDirective); + } + + argument.name(EntityUtil.getName(parameter.getName(), parameter)); + // TODO: argument.defaultValue(defaultValue) + field.argument(argument); + } + + DataFetcher fetcher = buildFetcher(diretives, authorizer, method, meta); + codeRegistry.dataFetcher(coordinates, fetcher); + return field; + } + + private GraphQLAppliedDirective getAppliedDirective(Annotation annotation, Class annotationType, Method[] methods) + throws IllegalAccessException, InvocationTargetException { + var appliedDirective = new GraphQLAppliedDirective.Builder().name(annotationType.getSimpleName()); + for (var definedMethod : methods) { + var name = definedMethod.getName(); + var value = definedMethod.invoke(annotation); + if (value == null) { + continue; + } + + TypeMeta innerMeta = new TypeMeta(null, definedMethod.getReturnType(), definedMethod.getGenericReturnType()); + var argumentType = entityProcessor.getEntity(innerMeta).getInputType(innerMeta, definedMethod.getAnnotations()); + appliedDirective.argument(GraphQLAppliedDirectiveArgument.newArgument().name(name).type(argumentType).valueProgrammatic(value).build()); + } + return appliedDirective.build(); + } + + private DataFetcher buildFetcher(DirectivesSchema diretives, AuthorizerSchema authorizer, Method method, TypeMeta meta) { + DataFetcher fetcher = buildDataFetcher(meta, method); + fetcher = diretives.wrap(method, meta, fetcher); + + if (authorizer != null) { + fetcher = authorizer.wrap(fetcher, method); + } + return fetcher; + } + + private DataFetcher buildDataFetcher(TypeMeta meta, Method method) { + Function[] resolvers = new Function[method.getParameterCount()]; + + method.setAccessible(true); + + for (int i = 0; i < resolvers.length; i++) { + var parameter = method.getParameters()[i]; + Class type = parameter.getType(); + var name = EntityUtil.getName(parameter.getName(), parameter); + var generic = method.getGenericParameterTypes()[i]; + var argMeta = new TypeMeta(meta, type, generic, parameter); + resolvers[i] = buildResolver(name, argMeta, parameter.getAnnotations()); + } + + DataFetcher fetcher = env -> { + try { + Object[] args = new Object[resolvers.length]; + for (int i = 0; i < resolvers.length; i++) { + args[i] = resolvers[i].apply(env); + } + return method.invoke(env.getSource(), args); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof Exception) { + throw (Exception) e.getCause(); + } else { + throw e; + } + } catch (Exception e) { + throw e; + } + }; + + return dataFetcherRunner.manage(method, fetcher); + } + + private Function buildResolver(String name, TypeMeta argMeta, Annotation[] annotations) { + if (isContext(argMeta.getType(), annotations)) { + var type = argMeta.getType(); + if (type.isAssignableFrom(DataFetchingEnvironment.class)) { + return env -> env; + } + if (type.isAssignableFrom(GraphQLContext.class)) { + return env -> env.getGraphQlContext(); + } + return env -> { + var localContext = env.getLocalContext(); + if (localContext != null && type.isAssignableFrom(localContext.getClass())) { + return localContext; + } + + var context = env.getContext(); + if (context != null && type.isAssignableFrom(context.getClass())) { + return context; + } + + context = env.getGraphQlContext().get(name); + if (context != null && type.isAssignableFrom(context.getClass())) { + return context; + } + throw new RuntimeException("Context object " + name + " not found"); + }; + } + + var resolver = entityProcessor.getResolver(argMeta); + + return env -> { + var arg = env.getArgument(name); + return resolver.convert(arg, env.getGraphQlContext(), env.getLocale()); + }; + } + + public GraphQLCodeRegistry.Builder getCodeRegistry() { + return codeRegistry; + } + + GraphQLObjectType.Builder getGraphQuery() { + return graphQuery; + } + + GraphQLObjectType.Builder getGraphMutations() { + return graphMutations; + } + + GraphQLObjectType.Builder getGraphSubscriptions() { + return graphSubscriptions; + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/ObjectEntity.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/ObjectEntity.java new file mode 100644 index 00000000..cffab6b5 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/ObjectEntity.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import com.fleetpin.graphql.builder.annotations.GraphQLCreator; +import com.fleetpin.graphql.builder.annotations.OneOf; +import com.fleetpin.graphql.builder.mapper.InputTypeBuilder; +import graphql.schema.GraphQLNamedInputType; +import graphql.schema.GraphQLNamedOutputType; + +public class ObjectEntity extends EntityHolder { + + private InputBuilder inputBuilder; + + private TypeBuilder typeBuilder; + + public ObjectEntity(EntityProcessor entityProcessor, TypeMeta meta) { + if (meta.getType().isRecord()) { + typeBuilder = new TypeBuilder.Record(entityProcessor, meta); + } else { + typeBuilder = new TypeBuilder.ObjectType(entityProcessor, meta); + } + + if (meta.getType().isAnnotationPresent(OneOf.class)) { + inputBuilder = new InputBuilder.OneOfInputBuilder(entityProcessor, meta); + } else if (meta.getType().isRecord()) { + inputBuilder = new InputBuilder.Record(entityProcessor, meta); + } else { + var constructors = meta.getType().getDeclaredConstructors(); + if (constructors.length == 1) { + var constructor = constructors[0]; + if (constructor.getParameterCount() > 0) { + inputBuilder = new InputBuilder.ObjectConstructorType(entityProcessor, meta, constructor); + return; + } + } + for (var constructor : constructors) { + if (constructor.isAnnotationPresent(GraphQLCreator.class)) { + inputBuilder = new InputBuilder.ObjectConstructorType(entityProcessor, meta, constructor); + return; + } + } + inputBuilder = new InputBuilder.ObjectType(entityProcessor, meta); + } + } + + @Override + protected GraphQLNamedInputType buildInput() { + return inputBuilder.buildInput(); + } + + @Override + public InputTypeBuilder buildResolver() { + return inputBuilder.resolve(); + } + + @Override + protected GraphQLNamedOutputType buildType() { + try { + return typeBuilder.buildType(); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @Override + protected String buildInputName() { + return inputBuilder.buildName(); + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/RestrictType.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/RestrictType.java new file mode 100644 index 00000000..b234d5ac --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/RestrictType.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public interface RestrictType { + public CompletableFuture allow(T obj); + + public default CompletableFuture> filter(List list) { + boolean[] keep = new boolean[list.size()]; + + CompletableFuture[] all = new CompletableFuture[list.size()]; + for (int i = 0; i < list.size(); i++) { + int offset = i; + all[i] = allow(list.get(i)).thenAccept(allow -> keep[offset] = allow); + } + return CompletableFuture + .allOf(all) + .thenApply(__ -> { + List toReturn = new ArrayList<>(); + for (int i = 0; i < list.size(); i++) { + if (keep[i]) { + toReturn.add(list.get(i)); + } + } + return toReturn; + }); + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/RestrictTypeFactory.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/RestrictTypeFactory.java new file mode 100644 index 00000000..05e4be2e --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/RestrictTypeFactory.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import graphql.schema.DataFetchingEnvironment; +import java.lang.reflect.ParameterizedType; +import java.util.concurrent.CompletableFuture; + +public interface RestrictTypeFactory { + public CompletableFuture> create(DataFetchingEnvironment context); + + default Class extractType() { + for (var inter : getClass().getGenericInterfaces()) { + if (inter instanceof ParameterizedType) { + var param = (ParameterizedType) inter; + if (RestrictTypeFactory.class.equals(param.getRawType())) { + return (Class) param.getActualTypeArguments()[0]; + } + } + } + return null; + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/ScalarEntity.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/ScalarEntity.java new file mode 100644 index 00000000..d1c0422b --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/ScalarEntity.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import com.fleetpin.graphql.builder.annotations.GraphQLDescription; +import com.fleetpin.graphql.builder.annotations.Scalar; +import com.fleetpin.graphql.builder.mapper.InputTypeBuilder; +import graphql.schema.Coercing; +import graphql.schema.GraphQLNamedInputType; +import graphql.schema.GraphQLNamedOutputType; +import graphql.schema.GraphQLScalarType; + +public class ScalarEntity extends EntityHolder { + + private final GraphQLScalarType scalar; + + public ScalarEntity(GraphQLScalarType scalar) { + this.scalar = scalar; + } + + public ScalarEntity(DirectivesSchema directives, TypeMeta meta) throws ReflectiveOperationException { + GraphQLScalarType.Builder scalarType = GraphQLScalarType.newScalar(); + String typeName = EntityUtil.getName(meta); + scalarType.name(typeName); + + var type = meta.getType(); + + var description = type.getAnnotation(GraphQLDescription.class); + if (description != null) { + scalarType.description(description.value()); + } + + Class coerecing = type.getAnnotation(Scalar.class).value(); + scalarType.coercing(coerecing.getDeclaredConstructor().newInstance()); + + directives.addSchemaDirective(type, type, scalarType::withAppliedDirective); + this.scalar = scalarType.build(); + } + + @Override + protected GraphQLNamedInputType buildInput() { + return scalar; + } + + @Override + protected GraphQLNamedOutputType buildType() { + return scalar; + } + + @Override + protected String buildInputName() { + return scalar.getName(); + } + + @Override + public InputTypeBuilder buildResolver() { + return scalar.getCoercing()::parseValue; + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/SchemaBuilder.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/SchemaBuilder.java new file mode 100644 index 00000000..24bf2fcb --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/SchemaBuilder.java @@ -0,0 +1,192 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import com.fleetpin.graphql.builder.annotations.*; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchema; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.reflections.Reflections; +import org.reflections.scanners.Scanners; + +public class SchemaBuilder { + + private final DirectivesSchema directives; + private final AuthorizerSchema authorizer; + private final EntityProcessor entityProcessor; + + private SchemaBuilder(DataFetcherRunner dataFetcherRunner, List scalars, DirectivesSchema directives, AuthorizerSchema authorizer) { + this.directives = directives; + this.authorizer = authorizer; + + this.entityProcessor = new EntityProcessor(dataFetcherRunner, scalars, directives); + + directives.processDirectives(entityProcessor); + } + + private SchemaBuilder processTypes(Set> types) { + for (var type : types) { + TypeMeta meta = new TypeMeta(null, type, type); + + var annotation = type.getAnnotation(Entity.class); + if (annotation.value() != SchemaOption.INPUT) { + this.entityProcessor.getEntity(meta).getInnerType(meta); + } + } + return this; + } + + private SchemaBuilder process(HashSet endPoints) throws ReflectiveOperationException { + var methodProcessor = this.entityProcessor.getMethodProcessor(); + for (var method : endPoints) { + methodProcessor.process(authorizer, method); + } + + return this; + } + + private graphql.schema.GraphQLSchema.Builder build(Set> schemaConfiguration) { + var methods = entityProcessor.getMethodProcessor(); + + var builder = GraphQLSchema.newSchema().codeRegistry(methods.getCodeRegistry().build()).additionalTypes(entityProcessor.getAdditionalTypes()); + + var query = methods.getGraphQuery().build(); + builder.query(query); + + var mutations = methods.getGraphMutations().build(); + if (!mutations.getFields().isEmpty()) { + builder.mutation(mutations); + } + var subscriptions = methods.getGraphSubscriptions().build(); + if (!subscriptions.getFields().isEmpty()) { + builder.subscription(subscriptions); + } + + directives.getSchemaDirective().forEach(directive -> builder.additionalDirective(directive)); + + for (var schema : schemaConfiguration) { + this.directives.addSchemaDirective(schema, schema, builder::withSchemaAppliedDirective); + } + return builder; + } + + public static GraphQLSchema build(String... classPath) { + return builder(classPath).build(); + } + + public static GraphQLSchema.Builder builder(String... classpath) { + var builder = builder(); + for (var path : classpath) { + builder.classpath(path); + } + return builder.build(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private DataFetcherRunner dataFetcherRunner = (method, fetcher) -> fetcher; + private List classpaths = new ArrayList<>(); + private List scalars = new ArrayList<>(); + + private Builder() {} + + public Builder dataFetcherRunner(DataFetcherRunner dataFetcherRunner) { + this.dataFetcherRunner = dataFetcherRunner; + return this; + } + + public Builder classpath(String classpath) { + this.classpaths.add(classpath); + return this; + } + + public Builder scalar(GraphQLScalarType scalar) { + this.scalars.add(scalar); + return this; + } + + public GraphQLSchema.Builder build() { + try { + Reflections reflections = new Reflections(classpaths, Scanners.SubTypes, Scanners.MethodsAnnotated, Scanners.TypesAnnotated); + Set> authorizers = reflections.getSubTypesOf(Authorizer.class); + //want to make everything split by package + AuthorizerSchema authorizer = AuthorizerSchema.build(dataFetcherRunner, new HashSet<>(classpaths), authorizers); + + Set> schemaConfiguration = reflections.getSubTypesOf(SchemaConfiguration.class); + + Set> directivesTypes = reflections.getTypesAnnotatedWith(Directive.class); + directivesTypes.addAll(reflections.getTypesAnnotatedWith(DataFetcherWrapper.class)); + + Set> restrict = reflections.getTypesAnnotatedWith(Restrict.class); + Set> restricts = reflections.getTypesAnnotatedWith(Restricts.class); + List> globalRestricts = new ArrayList<>(); + + for (var r : restrict) { + Restrict annotation = EntityUtil.getAnnotation(r, Restrict.class); + var factoryClass = annotation.value(); + var factory = factoryClass.getConstructor().newInstance(); + if (!factory.extractType().isAssignableFrom(r)) { + throw new RuntimeException( + "Restrict annotation does match class applied to targets" + factory.extractType() + " but was on class " + r + ); + } + globalRestricts.add(factory); + } + + for (var r : restricts) { + Restricts annotations = EntityUtil.getAnnotation(r, Restricts.class); + for (Restrict annotation : annotations.value()) { + var factoryClass = annotation.value(); + var factory = factoryClass.getConstructor().newInstance(); + + if (!factory.extractType().isAssignableFrom(r)) { + throw new RuntimeException( + "Restrict annotation does match class applied to targets" + factory.extractType() + " but was on class " + r + ); + } + globalRestricts.add(factory); + } + } + + DirectivesSchema directivesSchema = DirectivesSchema.build(globalRestricts, directivesTypes); // Entry point for directives + + Set> types = reflections.getTypesAnnotatedWith(Entity.class); + + var mutations = reflections.getMethodsAnnotatedWith(Mutation.class); + var subscriptions = reflections.getMethodsAnnotatedWith(Subscription.class); + var queries = reflections.getMethodsAnnotatedWith(Query.class); + + var endPoints = new HashSet<>(mutations); + endPoints.addAll(subscriptions); + endPoints.addAll(queries); + + types.removeIf(t -> t.getDeclaredAnnotation(Entity.class) == null); + types.removeIf(t -> t.isAnonymousClass()); + + return new SchemaBuilder(dataFetcherRunner, scalars, directivesSchema, authorizer) + .processTypes(types) + .process(endPoints) + .build(schemaConfiguration); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/SchemaConfiguration.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/SchemaConfiguration.java new file mode 100644 index 00000000..4eb2412b --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/SchemaConfiguration.java @@ -0,0 +1,18 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +/** + * Used to add extra information to the schema. Currently only supports directives. That are inferred using the annotation + * + */ +public interface SchemaConfiguration {} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/TypeBuilder.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/TypeBuilder.java new file mode 100644 index 00000000..7d35b9c1 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/TypeBuilder.java @@ -0,0 +1,232 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.GraphQLDescription; +import com.fleetpin.graphql.builder.annotations.GraphQLIgnore; +import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLInterfaceType; +import graphql.schema.GraphQLNamedOutputType; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLObjectType.Builder; +import graphql.schema.GraphQLTypeReference; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +public abstract class TypeBuilder { + + protected final EntityProcessor entityProcessor; + protected final TypeMeta meta; + + public TypeBuilder(EntityProcessor entityProcessor, TypeMeta meta) { + this.entityProcessor = entityProcessor; + this.meta = meta; + } + + public GraphQLNamedOutputType buildType() throws ReflectiveOperationException { + Builder graphType = GraphQLObjectType.newObject(); + String typeName = EntityUtil.getName(meta); + graphType.name(typeName); + + GraphQLInterfaceType.Builder interfaceBuilder = GraphQLInterfaceType.newInterface(); + interfaceBuilder.name(typeName); + var type = meta.getType(); + { + var description = type.getAnnotation(GraphQLDescription.class); + if (description != null) { + graphType.description(description.value()); + interfaceBuilder.description(description.value()); + } + } + + processFields(typeName, graphType, interfaceBuilder); + + boolean unmappedGenerics = meta.hasUnmappedGeneric(); + + if (unmappedGenerics) { + var name = EntityUtil.getName(meta.notDirect()); + + graphType.withInterface(GraphQLTypeReference.typeRef(name)); + if (meta.isDirect()) { + interfaceBuilder.withInterface(GraphQLTypeReference.typeRef(name)); + } + } + Class parent = type.getSuperclass(); + while (parent != null) { + if (parent.isAnnotationPresent(Entity.class)) { + TypeMeta innerMeta = new TypeMeta(meta, parent, type.getGenericSuperclass()); + GraphQLInterfaceType interfaceName = (GraphQLInterfaceType) entityProcessor.getEntity(innerMeta).getInnerType(innerMeta); + addInterface(graphType, interfaceBuilder, interfaceName); + + if (!parent.equals(type.getGenericSuperclass())) { + innerMeta = new TypeMeta(meta, parent, parent); + interfaceName = (GraphQLInterfaceType) entityProcessor.getEntity(innerMeta).getInnerType(innerMeta); + addInterface(graphType, interfaceBuilder, interfaceName); + } + + var genericMeta = new TypeMeta(null, parent, parent); + if (!EntityUtil.getName(innerMeta).equals(EntityUtil.getName(genericMeta))) { + interfaceName = (GraphQLInterfaceType) entityProcessor.getEntity(genericMeta).getInnerType(genericMeta); + addInterface(graphType, interfaceBuilder, interfaceName); + } + } + parent = parent.getSuperclass(); + } + // generics + TypeMeta innerMeta = new TypeMeta(meta, type, type); + if (!EntityUtil.getName(innerMeta).equals(typeName)) { + var interfaceName = entityProcessor.getEntity(innerMeta).getInnerType(innerMeta); + graphType.withInterface(GraphQLTypeReference.typeRef(interfaceName.getName())); + interfaceBuilder.withInterface(GraphQLTypeReference.typeRef(interfaceName.getName())); + } + innerMeta = new TypeMeta(null, type, type); + if (!EntityUtil.getName(innerMeta).equals(typeName)) { + var interfaceName = entityProcessor.getEntity(innerMeta).getInnerType(innerMeta); + graphType.withInterface(GraphQLTypeReference.typeRef(interfaceName.getName())); + interfaceBuilder.withInterface(GraphQLTypeReference.typeRef(interfaceName.getName())); + } + + boolean interfaceable = type.isInterface() || Modifier.isAbstract(type.getModifiers()); + if (!meta.isDirect() && (interfaceable || unmappedGenerics)) { + entityProcessor.addSchemaDirective(type, type, interfaceBuilder::withAppliedDirective); + GraphQLInterfaceType built = interfaceBuilder.build(); + + entityProcessor + .getCodeRegistry() + .typeResolver( + built.getName(), + env -> { + if (type.isInstance(env.getObject())) { + var meta = new TypeMeta(null, env.getObject().getClass(), env.getObject().getClass()); + var t = entityProcessor.getEntity(meta).getInnerType(null); + if (!(t instanceof GraphQLObjectType)) { + t = entityProcessor.getEntity(meta.direct()).getInnerType(null); + } + try { + return (GraphQLObjectType) t; + } catch (ClassCastException e) { + throw e; + } + } + return null; + } + ); + + if (unmappedGenerics && !meta.isDirect()) { + var directType = meta.direct(); + entityProcessor.getEntity(directType).getInnerType(directType); + } + return built; + } + + entityProcessor.addSchemaDirective(type, type, graphType::withAppliedDirective); + var built = graphType.build(); + entityProcessor + .getCodeRegistry() + .typeResolver( + built.getName(), + env -> { + if (type.isInstance(env.getObject())) { + return built; + } + return null; + } + ); + return built; + } + + private void addInterface(Builder graphType, GraphQLInterfaceType.Builder interfaceBuilder, GraphQLInterfaceType interfaceName) { + graphType.withInterface(interfaceName); + for (var inner : interfaceName.getInterfaces()) { + graphType.withInterface(GraphQLTypeReference.typeRef(inner.getName())); + interfaceBuilder.withInterface(GraphQLTypeReference.typeRef(inner.getName())); + } + interfaceBuilder.withInterface(interfaceName); + } + + protected abstract void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder) + throws ReflectiveOperationException; + + public static class ObjectType extends TypeBuilder { + + public ObjectType(EntityProcessor entityProcessor, TypeMeta meta) { + super(entityProcessor, meta); + } + + @Override + protected void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder) + throws ReflectiveOperationException { + var type = meta.getType(); + for (Method method : type.getMethods()) { + try { + var name = EntityUtil.getter(method); + if (name.isEmpty()) { + continue; + } + var f = entityProcessor.getMethodProcessor().process(null, FieldCoordinates.coordinates(typeName, name.get()), meta, method); + graphType.field(f); + interfaceBuilder.field(f); + } catch (RuntimeException e) { + throw new RuntimeException("Failed to process method " + method, e); + } + } + } + } + + public static class Record extends TypeBuilder { + + public Record(EntityProcessor entityProcessor, TypeMeta meta) { + super(entityProcessor, meta); + } + + @Override + protected void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder) + throws ReflectiveOperationException { + var type = meta.getType(); + + for (var field : type.getDeclaredFields()) { + try { + if (field.isSynthetic()) { + continue; + } + if (field.getDeclaringClass().equals(Object.class)) { + continue; + } + if (field.isAnnotationPresent(GraphQLIgnore.class)) { + continue; + } + // will also be on implementing class + if (Modifier.isAbstract(field.getModifiers()) || field.getDeclaringClass().isInterface()) { + continue; + } + if (Modifier.isStatic(field.getModifiers())) { + continue; + } else { + var method = type.getMethod(field.getName()); + if (method.isAnnotationPresent(GraphQLIgnore.class)) { + continue; + } + + var name = EntityUtil.getName(field.getName(), field, method); + + var f = entityProcessor.getMethodProcessor().process(null, FieldCoordinates.coordinates(typeName, name), meta, method); + graphType.field(f); + interfaceBuilder.field(f); + } + } catch (RuntimeException e) { + throw new RuntimeException("Failed to process method " + field, e); + } + } + } + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/TypeMeta.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/TypeMeta.java new file mode 100644 index 00000000..37cf193f --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/TypeMeta.java @@ -0,0 +1,355 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import com.fleetpin.graphql.builder.annotations.InnerNullable; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; +import org.reactivestreams.Publisher; + +public class TypeMeta { + + enum Flag { + ASYNC, + ARRAY, + OPTIONAL, + SUBSCRIPTION, + DIRECT, + } + + private List flags; + + private Type genericType; + private Class type; + + private boolean direct; + + private final TypeMeta parent; + + private List> types; + + private AnnotatedElement element; + + TypeMeta(TypeMeta parent, Class type, Type genericType) { + this(parent, type, genericType, null); + } + + TypeMeta(TypeMeta parent, Class type, Type genericType, AnnotatedElement element) { + this.parent = parent; + this.element = element; + flags = new ArrayList<>(); + types = new ArrayList<>(); + process(type, genericType, element); + Collections.reverse(flags); + } + + private void processGeneric(TypeMeta target, TypeVariable type, AnnotatedElement element) { + var owningClass = target.genericType; + if (owningClass == null) { + owningClass = target.type; + } + if (owningClass instanceof Class) { + findType(target, type, (Class) owningClass, element); + } else if (owningClass instanceof ParameterizedType) { + var pt = (ParameterizedType) owningClass; + if (!matchType(target, type.getTypeName(), pt, true, element)) { + throw new UnsupportedOperationException("Does not handle type " + owningClass); + } + } else { + throw new UnsupportedOperationException("Does not handle type " + owningClass); + } + } + + private boolean matchType(TypeMeta target, String typeName, ParameterizedType type, boolean parent, AnnotatedElement element) { + var raw = (Class) type.getRawType(); + while (raw != null) { + for (int i = 0; i < raw.getTypeParameters().length; i++) { + var arg = type.getActualTypeArguments()[i]; + var param = raw.getTypeParameters()[i]; + if (param.getTypeName().equals(typeName)) { + if (arg instanceof TypeVariable) { + if (parent) { + processGeneric(target.parent, (TypeVariable) arg, element); + } else { + processGeneric(target, (TypeVariable) arg, element); + } + return true; + } else if (arg instanceof WildcardType) { + for (var bound : param.getBounds()) { + if (bound instanceof ParameterizedType) { + process((Class) ((ParameterizedType) bound).getRawType(), bound, element); + } else if (bound instanceof TypeVariable) { + processGeneric(target, (TypeVariable) bound, element); + } else { + process((Class) bound, null, element); + } + } + return true; + } else if (arg instanceof ParameterizedType pType) { + process((Class) pType.getRawType(), pType, element); + return true; + } else { + var klass = (Class) arg; + process(klass, klass, element); + return true; + } + } + } + raw = raw.getSuperclass(); + } + return false; + } + + private void findType(TypeMeta target, TypeVariable type, Class start, AnnotatedElement element) { + var startClass = (Class) start; + var genericDeclaration = type.getGenericDeclaration(); + if (start.equals(genericDeclaration)) { + // we don't have any implementing logic we are at this level so take the bounds + for (var bound : type.getBounds()) { + if (bound instanceof ParameterizedType) { + process((Class) ((ParameterizedType) bound).getRawType(), bound, element); + } else if (bound instanceof TypeVariable) { + processGeneric(target, (TypeVariable) bound, element); + } else { + process((Class) bound, null, element); + } + } + } + if (startClass.getSuperclass() != null && startClass.getSuperclass().equals(genericDeclaration)) { + var generic = (ParameterizedType) startClass.getGenericSuperclass(); + if (matchType(target, type.getTypeName(), generic, false, element)) { + return; + } + } + for (var inter : startClass.getGenericInterfaces()) { + if (inter instanceof ParameterizedType) { + var generic = (ParameterizedType) inter; + if (generic.getRawType().equals(genericDeclaration)) { + if (matchType(target, type.getTypeName(), generic, false, element)) { + return; + } + } + } + } + if (startClass.getSuperclass() != null) { + findType(target, type, startClass.getSuperclass(), element); + } + + for (var inter : startClass.getInterfaces()) { + findType(target, type, inter, element); + } + } + + private void process(Class type, Type genericType, AnnotatedElement element) { + if (element != null && (element.isAnnotationPresent(Nullable.class) || element.isAnnotationPresent(jakarta.annotation.Nullable.class))) { + if (!flags.contains(Flag.OPTIONAL)) { + flags.add(Flag.OPTIONAL); + } + } + + if (type.isArray()) { + flags.add(Flag.ARRAY); + types.add(type); + process(type.getComponentType(), null, element); + return; + } + + if (Collection.class.isAssignableFrom(type)) { + flags.add(Flag.ARRAY); + types.add(type); + genericType = ((ParameterizedType) genericType).getActualTypeArguments()[0]; + if (genericType instanceof ParameterizedType) { + process((Class) ((ParameterizedType) genericType).getRawType(), genericType, element); + } else if (genericType instanceof TypeVariable) { + processGeneric(parent, (TypeVariable) genericType, element); + } else { + process((Class) genericType, null, element); + } + return; + } + if (CompletableFuture.class.isAssignableFrom(type)) { + flags.add(Flag.ASYNC); + types.add(type); + genericType = ((ParameterizedType) genericType).getActualTypeArguments()[0]; + if (genericType instanceof ParameterizedType) { + process((Class) ((ParameterizedType) genericType).getRawType(), genericType, element); + } else if (genericType instanceof TypeVariable) { + processGeneric(parent, (TypeVariable) genericType, element); + } else { + process((Class) genericType, null, element); + } + return; + } + + if (Optional.class.isAssignableFrom(type)) { + flags.add(Flag.OPTIONAL); + types.add(type); + genericType = ((ParameterizedType) genericType).getActualTypeArguments()[0]; + if (genericType instanceof ParameterizedType) { + process((Class) ((ParameterizedType) genericType).getRawType(), genericType, element); + } else if (genericType instanceof TypeVariable) { + processGeneric(parent, (TypeVariable) genericType, element); + } else { + process((Class) genericType, null, element); + } + return; + } + + if (Publisher.class.isAssignableFrom(type)) { + flags.add(Flag.SUBSCRIPTION); + types.add(type); + genericType = ((ParameterizedType) genericType).getActualTypeArguments()[0]; + if (genericType instanceof ParameterizedType) { + process((Class) ((ParameterizedType) genericType).getRawType(), genericType, element); + } else if (genericType instanceof TypeVariable) { + processGeneric(parent, (TypeVariable) genericType, element); + } else { + process((Class) genericType, null, element); + } + return; + } + + if (genericType != null && genericType instanceof TypeVariable) { + processGeneric(parent, (TypeVariable) genericType, element); + return; + } + + if (element != null && (element.isAnnotationPresent(InnerNullable.class))) { + if (!flags.contains(Flag.OPTIONAL)) { + flags.add(Flag.OPTIONAL); + } + } + + this.type = type; + this.genericType = genericType; + types.add(type); + } + + public Class getType() { + return type; + } + + public Type getGenericType() { + return genericType; + } + + public List getFlags() { + return flags; + } + + public List> getTypes() { + return types; + } + + public boolean hasUnmappedGeneric() { + if (type.getTypeParameters().length == 0) { + return false; + } + if (genericType == null || !(genericType instanceof ParameterizedType)) { + return true; + } + + ParameterizedType pt = (ParameterizedType) genericType; + + for (var type : pt.getActualTypeArguments()) { + if (type instanceof TypeVariable) { + if (resolveToType((TypeVariable) type) == null) { + return true; + } + } else if (!(type instanceof Class)) { + return true; + } + } + return false; + } + + public Class resolveToType(TypeVariable variable) { + var parent = this.parent; + + var parentType = type; + var genericType = this.genericType; + + while (true) { + if (genericType instanceof ParameterizedType) { + var pt = parentType.getTypeParameters(); + for (int i = 0; i < pt.length; i++) { + var p = pt[i]; + if (p.equals(variable)) { + var generic = (ParameterizedType) genericType; // safe as has to if equal vaiable + var implementingType = generic.getActualTypeArguments()[i]; + if (implementingType instanceof Class) { + return (Class) implementingType; + } else if (implementingType instanceof TypeVariable) { + return resolveToType((TypeVariable) implementingType); + } else { + throw new RuntimeException("Generics are more complex that logic currently can handle"); + } + } + } + } + + var pt = parentType.getSuperclass().getTypeParameters(); + for (int i = 0; i < pt.length; i++) { + var p = pt[i]; + if (p.equals(variable)) { + var superClass = (ParameterizedType) parentType.getGenericSuperclass(); // safe as has to if equal vaiable + var implementingType = superClass.getActualTypeArguments()[i]; + + if (implementingType instanceof Class) { + return (Class) implementingType; + } else if (implementingType instanceof TypeVariable) { + return resolveToType((TypeVariable) implementingType); + } else { + throw new RuntimeException("Generics are more complex that logic currently can handle"); + } + } + } + + if (parent == null) { + break; + } + parentType = parent.getType(); + genericType = parent.getGenericType(); + parent = parent.parent; + } + return null; + } + + public void optional() { + flags.add(Flag.OPTIONAL); + } + + public TypeMeta direct() { + var toReturn = new TypeMeta(parent, type, genericType, element); + toReturn.direct = true; + return toReturn; + } + + public boolean isDirect() { + return direct; + } + + public TypeMeta notDirect() { + var toReturn = new TypeMeta(parent, type, genericType, element); + return toReturn; + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/UnionType.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/UnionType.java new file mode 100644 index 00000000..c25ccc66 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/UnionType.java @@ -0,0 +1,83 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import com.fleetpin.graphql.builder.annotations.Union; +import com.fleetpin.graphql.builder.mapper.InputTypeBuilder; +import graphql.schema.GraphQLNamedInputType; +import graphql.schema.GraphQLNamedOutputType; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLTypeReference; +import graphql.schema.GraphQLUnionType; + +public class UnionType extends EntityHolder { + + private final EntityProcessor entityProcessor; + private final Union union; + + public UnionType(EntityProcessor entityProcessor, Union union) { + this.entityProcessor = entityProcessor; + this.union = union; + } + + @Override + protected GraphQLNamedOutputType buildType() { + String name = buildInputName(); + var builder = GraphQLUnionType.newUnionType(); + builder.name(name); + + for (var type : union.value()) { + var possible = entityProcessor.getEntity(type).getInnerType(new TypeMeta(null, type, type)); + builder.possibleType(GraphQLTypeReference.typeRef(possible.getName())); + } + + entityProcessor + .getCodeRegistry() + .typeResolver( + name, + env -> { + for (var type : union.value()) { + if (type.isInstance(env.getObject())) { + return (GraphQLObjectType) entityProcessor.getEntity(type).getInnerType(new TypeMeta(null, type, type)); + } + } + throw new RuntimeException("Union " + name + " Does not support type " + env.getObject().getClass().getSimpleName()); + } + ); + return builder.build(); + } + + @Override + protected GraphQLNamedInputType buildInput() { + return null; + } + + @Override + protected String buildInputName() { + return name(union); + } + + static String name(Union union) { + StringBuilder name = new StringBuilder("Union"); + + for (var type : union.value()) { + name.append("_"); + name.append(EntityUtil.getName(new TypeMeta(null, type, type))); + } + return name.toString(); + } + + @Override + protected InputTypeBuilder buildResolver() { + return null; + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Context.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Context.java new file mode 100644 index 00000000..0ebe5d75 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Context.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({ ElementType.TYPE, ElementType.PARAMETER }) +public @interface Context { +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/DataFetcherWrapper.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/DataFetcherWrapper.java new file mode 100644 index 00000000..1f11f01e --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/DataFetcherWrapper.java @@ -0,0 +1,14 @@ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.fleetpin.graphql.builder.DirectiveOperation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface DataFetcherWrapper { + Class> value(); +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Directive.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Directive.java new file mode 100644 index 00000000..7dbd6895 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Directive.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.fleetpin.graphql.builder.DirectiveCaller; +import com.fleetpin.graphql.builder.DirectiveOperation; +import graphql.introspection.Introspection; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface Directive { + Introspection.DirectiveLocation[] value(); + + boolean repeatable() default false; +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Entity.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Entity.java new file mode 100644 index 00000000..32184cfb --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Entity.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(ElementType.TYPE) +public @interface Entity { + SchemaOption value() default SchemaOption.TYPE; +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLCreator.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLCreator.java new file mode 100644 index 00000000..e70af410 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLCreator.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({ ElementType.CONSTRUCTOR }) +public @interface GraphQLCreator { +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLDeprecated.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLDeprecated.java new file mode 100644 index 00000000..49666f72 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLDeprecated.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({ ElementType.METHOD }) +public @interface GraphQLDeprecated { + public String value(); +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLDescription.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLDescription.java new file mode 100644 index 00000000..ce6c6d7f --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLDescription.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER }) +public @interface GraphQLDescription { + public String value(); +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLIgnore.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLIgnore.java new file mode 100644 index 00000000..e51c4ae5 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLIgnore.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD }) +public @interface GraphQLIgnore { +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLName.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLName.java new file mode 100644 index 00000000..726f7ee9 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/GraphQLName.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +public @interface GraphQLName { + public String value(); +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Id.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Id.java new file mode 100644 index 00000000..c1fe3766 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Id.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({ ElementType.METHOD, ElementType.PARAMETER }) +public @interface Id { +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/InnerNullable.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/InnerNullable.java new file mode 100644 index 00000000..2802df1c --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/InnerNullable.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({ ElementType.METHOD, ElementType.PARAMETER }) +public @interface InnerNullable { +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/InputIgnore.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/InputIgnore.java new file mode 100644 index 00000000..23362b38 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/InputIgnore.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({ ElementType.METHOD, ElementType.PARAMETER }) +public @interface InputIgnore { +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Mutation.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Mutation.java new file mode 100644 index 00000000..411e98c7 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Mutation.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(ElementType.METHOD) +public @interface Mutation { +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/OneOf.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/OneOf.java new file mode 100644 index 00000000..b05736e2 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/OneOf.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(ElementType.TYPE) +public @interface OneOf { + Type[] value(); + + @Retention(RUNTIME) + @Target(ElementType.TYPE) + public @interface Type { + String name(); + + Class type(); + + String description() default ""; + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Query.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Query.java new file mode 100644 index 00000000..bc5a73bd --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Query.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(ElementType.METHOD) +public @interface Query { +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Restrict.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Restrict.java new file mode 100644 index 00000000..b577f50f --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Restrict.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.fleetpin.graphql.builder.RestrictTypeFactory; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(ElementType.TYPE) +@Repeatable(Restricts.class) +public @interface Restrict { + Class> value(); +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Restricts.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Restricts.java new file mode 100644 index 00000000..dff1c64e --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Restricts.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(ElementType.TYPE) +public @interface Restricts { + Restrict[] value(); +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Scalar.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Scalar.java new file mode 100644 index 00000000..39024222 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Scalar.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import graphql.schema.Coercing; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(ElementType.TYPE) +public @interface Scalar { + Class> value(); +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/SchemaOption.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/SchemaOption.java new file mode 100644 index 00000000..391851b2 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/SchemaOption.java @@ -0,0 +1,18 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +public enum SchemaOption { + INPUT, + TYPE, + BOTH, +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Subscription.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Subscription.java new file mode 100644 index 00000000..f6617054 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Subscription.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(ElementType.METHOD) +public @interface Subscription { +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Union.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Union.java new file mode 100644 index 00000000..d373c92f --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/annotations/Union.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(ElementType.METHOD) +public @interface Union { + Class[] value(); +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/exceptions/InvalidOneOfException.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/exceptions/InvalidOneOfException.java new file mode 100644 index 00000000..e5cbf0e8 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/exceptions/InvalidOneOfException.java @@ -0,0 +1,8 @@ +package com.fleetpin.graphql.builder.exceptions; + +public class InvalidOneOfException extends RuntimeException { + + public InvalidOneOfException(String message) { + super(message); + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/mapper/ConstructorFieldBuilder.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/mapper/ConstructorFieldBuilder.java new file mode 100644 index 00000000..35670637 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/mapper/ConstructorFieldBuilder.java @@ -0,0 +1,67 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.mapper; + +import graphql.GraphQLContext; +import java.util.ArrayList; +import java.util.Locale; +import java.util.Map; + +public class ConstructorFieldBuilder implements InputTypeBuilder { + + private final InputTypeBuilder map; + + public ConstructorFieldBuilder(Class type, ArrayList mappers) { + try { + var argTypes = mappers.stream().map(t -> t.type).toArray(Class[]::new); + var constructor = type.getDeclaredConstructor(argTypes); + constructor.setAccessible(true); + map = + (obj, context, locale) -> { + try { + Map map = (Map) obj; + + var args = new Object[argTypes.length]; + + for (int i = 0; i < args.length; i++) { + var mapper = mappers.get(i); + args[i] = mapper.resolver.convert(map.get(mapper.name), context, locale); + } + + return constructor.newInstance(args); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + }; + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @Override + public Object convert(Object obj, GraphQLContext graphQLContext, Locale locale) { + return map.convert(obj, graphQLContext, locale); + } + + public static class RecordMapper { + + private final String name; + private final Class type; + private final InputTypeBuilder resolver; + + public RecordMapper(String name, Class type, InputTypeBuilder resolver) { + this.name = name; + this.type = type; + this.resolver = resolver; + } + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/mapper/InputTypeBuilder.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/mapper/InputTypeBuilder.java new file mode 100644 index 00000000..0554a41e --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/mapper/InputTypeBuilder.java @@ -0,0 +1,19 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.mapper; + +import graphql.GraphQLContext; +import java.util.Locale; + +public interface InputTypeBuilder { + Object convert(Object obj, GraphQLContext graphQLContext, Locale locale); +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/mapper/ObjectFieldBuilder.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/mapper/ObjectFieldBuilder.java new file mode 100644 index 00000000..9fb20d83 --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/mapper/ObjectFieldBuilder.java @@ -0,0 +1,99 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.mapper; + +import com.fleetpin.graphql.builder.EntityProcessor; +import com.fleetpin.graphql.builder.TypeMeta; +import graphql.GraphQLContext; +import graphql.com.google.common.base.Preconditions; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Locale; +import java.util.Map; + +public class ObjectFieldBuilder implements InputTypeBuilder { + + private final InputTypeBuilder map; + + public ObjectFieldBuilder(Class type, ArrayList mappers) { + try { + var constructor = type.getConstructor(); + map = + (obj, context, locale) -> { + try { + var toReturn = constructor.newInstance(); + + Map map = (Map) obj; + + for (var mapper : mappers) { + var name = mapper.getName(); + if (map.containsKey(name)) { + mapper.map(toReturn, map.get(name), context, locale); + } + } + + return toReturn; + } catch (Throwable e) { + throwIfUnchecked(e); + throw new RuntimeException(e); + } + }; + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @Override + public Object convert(Object obj, GraphQLContext graphQLContext, Locale locale) { + return map.convert(obj, graphQLContext, locale); + } + + public static class FieldMapper { + + private final Method method; + private final String name; + private final InputTypeBuilder mapper; + + private FieldMapper(String name, Method method, InputTypeBuilder objectBuilder) { + this.name = name; + this.method = method; + this.mapper = objectBuilder; + } + + public String getName() { + return name; + } + + protected void map(Object inputType, Object argument, GraphQLContext graphQLContext, Locale locale) throws Throwable { + try { + method.invoke(inputType, mapper.convert(argument, graphQLContext, locale)); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + public static FieldMapper build(EntityProcessor entityProcessor, TypeMeta inputType, String name, Method method) { + return new FieldMapper(name, method, entityProcessor.getResolver(inputType)); + } + } + + // copied from guava + public static void throwIfUnchecked(Throwable throwable) { + Preconditions.checkNotNull(throwable); + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } else if (throwable instanceof Error) { + throw (Error) throwable; + } + } +} diff --git a/graphql-builder/src/main/java/com/fleetpin/graphql/builder/mapper/OneOfBuilder.java b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/mapper/OneOfBuilder.java new file mode 100644 index 00000000..0484dcbb --- /dev/null +++ b/graphql-builder/src/main/java/com/fleetpin/graphql/builder/mapper/OneOfBuilder.java @@ -0,0 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.mapper; + +import com.fleetpin.graphql.builder.EntityProcessor; +import com.fleetpin.graphql.builder.annotations.OneOf; +import com.fleetpin.graphql.builder.exceptions.InvalidOneOfException; +import graphql.GraphQLContext; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +public class OneOfBuilder implements InputTypeBuilder { + + private final InputTypeBuilder map; + + public OneOfBuilder(EntityProcessor entityProcessor, Class type, OneOf oneOf) { + Map builders = new HashMap<>(); + + for (var typeOf : oneOf.value()) { + if (!type.isAssignableFrom(typeOf.type())) { + throw new InvalidOneOfException("OneOf on " + type + " can not support type " + typeOf); + } + + builders.put(typeOf.name(), entityProcessor.getResolver(typeOf.type())); + } + + map = + (obj, context, locale) -> { + Map map = (Map) obj; + + if (map.size() > 1) { + var fields = String.join(", ", map.keySet()); + throw new InvalidOneOfException("OneOf must only have a single field set. Fields: " + fields); + } + + for (var entry : map.entrySet()) { + var builder = builders.get(entry.getKey()); + return builder.convert(entry.getValue(), context, locale); + } + + throw new InvalidOneOfException("OneOf must have a field set"); + }; + } + + @Override + public Object convert(Object obj, GraphQLContext graphQLContext, Locale locale) { + return map.convert(obj, graphQLContext, locale); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/AuthorizerTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/AuthorizerTest.java new file mode 100644 index 00000000..11663049 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/AuthorizerTest.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class AuthorizerTest { + + @Test + public void testQueryCatAllowed() throws ReflectiveOperationException { + var result = execute("query {getCat(name: \"socks\") {age}}"); + Map> response = result.getData(); + + var cat = response.get("getCat"); + + assertEquals(3, cat.get("age")); + + assertTrue(result.getErrors().isEmpty()); + } + + @Test + public void testQueryCatNotAllowed() throws ReflectiveOperationException { + var result = execute("query {getCat(name: \"boots\") {age}}"); + + assertNull(result.getData()); + + assertEquals(1, result.getErrors().size()); + var error = result.getErrors().get(0); + + assertEquals("Exception while fetching data (/getCat) : unauthorized", error.getMessage()); + //assertEquals("", error.getErrorType()); + } + + private ExecutionResult execute(String query) { + return execute(query, null); + } + + private ExecutionResult execute(String query, Map variables) { + GraphQL schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.authorizer")).build(); + var input = ExecutionInput.newExecutionInput(); + input.query(query); + if (variables != null) { + input.variables(variables); + } + ExecutionResult result = schema.execute(input); + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/ContextTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/ContextTest.java new file mode 100644 index 00000000..e6e56e65 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/ContextTest.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fleetpin.graphql.builder.annotations.Context; +import com.fleetpin.graphql.builder.annotations.Query; +import com.fleetpin.graphql.builder.context.GraphContext; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.introspection.IntrospectionWithDirectivesSupport; +import java.util.Map; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; + +public class ContextTest { + + @Test + public void testEntireContext() throws ReflectiveOperationException { + Map response = execute("query {entireContext} ", __ -> {}).getData(); + assertTrue(response.get("entireContext")); + } + + @Test + public void testEnv() throws ReflectiveOperationException { + Map response = execute("query {env} ", __ -> {}).getData(); + assertTrue(response.get("env")); + } + + @Test + public void testDeprecated() throws ReflectiveOperationException { + @SuppressWarnings("deprecation") + Map response = execute("query {deprecatedContext} ", b -> b.context(new GraphContext("context"))).getData(); + assertTrue(response.get("deprecatedContext")); + } + + @Test + public void testNamed() throws ReflectiveOperationException { + Map response = execute("query {namedContext} ", b -> b.graphQLContext(c -> c.of("named", new GraphContext("context")))).getData(); + assertTrue(response.get("namedContext")); + } + + @Test + public void testNamedParameter() throws ReflectiveOperationException { + Map response = execute("query {namedParemeterContext} ", b -> b.graphQLContext(c -> c.of("context", "test"))).getData(); + assertTrue(response.get("namedParemeterContext")); + } + + @Test + public void testMissing() throws ReflectiveOperationException { + var response = execute("query {missingContext} ", b -> b.graphQLContext(c -> c.of("context", "test"))); + assertEquals(1, response.getErrors().size()); + var error = response.getErrors().get(0); + assertTrue(error.getMessage().contains("Context object notPresent not found")); + } + + private ExecutionResult execute(String query, Consumer modify) { + GraphQL schema = GraphQL + .newGraphQL(new IntrospectionWithDirectivesSupport().apply(SchemaBuilder.build("com.fleetpin.graphql.builder.context"))) + .build(); + var input = ExecutionInput.newExecutionInput(); + input.query(query); + modify.accept(input); + ExecutionResult result = schema.execute(input); + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/DirectiveTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/DirectiveTest.java new file mode 100644 index 00000000..a374f6a2 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/DirectiveTest.java @@ -0,0 +1,109 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.*; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.introspection.IntrospectionWithDirectivesSupport; +import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLSchema; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +public class DirectiveTest { + + @Test + public void testDirectiveAppliedToQuery() throws ReflectiveOperationException { + GraphQL schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.type.directive")).build(); + var cat = schema.getGraphQLSchema().getFieldDefinition(FieldCoordinates.coordinates(schema.getGraphQLSchema().getQueryType(), "getCat")); + var capture = cat.getAppliedDirective("Capture"); + var argument = capture.getArgument("color"); + var color = argument.getValue(); + assertEquals("meow", color); + } + + @Test + public void testNoArgumentDirective() throws ReflectiveOperationException { + GraphQL schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.type.directive")).build(); + var cat = schema.getGraphQLSchema().getFieldDefinition(FieldCoordinates.coordinates(schema.getGraphQLSchema().getQueryType(), "getUpper")); + var uppercase = cat.getAppliedDirective("Uppercase"); + assertNotNull(uppercase); + assertTrue(uppercase.getArguments().isEmpty()); + } + + @Test + public void testPresentOnSchema() throws ReflectiveOperationException { + GraphQL schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.type.directive")).build(); + var capture = schema.getGraphQLSchema().getSchemaAppliedDirective("Capture"); + var argument = capture.getArgument("color"); + var color = argument.getValue(); + assertEquals("top", color); + } + + @Test + public void testDirectivePass() throws ReflectiveOperationException { + Map response = execute("query allowed($name: String!){allowed(name: $name)} ", Map.of("name", "tabby")).getData(); + assertEquals("tabby", response.get("allowed")); + } + + @Test + public void testDirectiveFail() throws ReflectiveOperationException { + var response = execute("query allowed($name: String!){allowed(name: $name)} ", Map.of("name", "calico")); + + assertNull(response.getData()); + + assertTrue(response.getErrors().get(0).getMessage().contains("forbidden")); + } + + @Test + public void testDirectiveArgument() { + GraphQL schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.type.directive")).build(); + var cat = schema.getGraphQLSchema().getFieldDefinition(FieldCoordinates.coordinates(schema.getGraphQLSchema().getQueryType(), "getNickname")); + var argument = cat.getArgument("nickName"); + var directive = argument.getAppliedDirective("Input"); + assertNotNull(directive); + var value = directive.getArgument("value").getValue(); + assertEquals("TT", value); + } + + @Test + public void testDirectiveArgumentDefinition() { + Map response = execute("query IntrospectionQuery { __schema { directives { name locations args { name } } } }", null).getData(); + List> dir = (List>) ((Map) response.get("__schema")).get("directives"); + LinkedHashMap input = dir.stream().filter(map -> map.get("name").equals("Input")).collect(Collectors.toList()).get(0); + + assertEquals(8, dir.size()); + assertEquals("ARGUMENT_DEFINITION", ((List) input.get("locations")).get(0)); + assertEquals(1, ((List) input.get("args")).size()); + //getNickname(nickName: String! @Input(value : "TT")): String! + //directive @Input(value: String!) on ARGUMENT_DEFINITION + } + + private ExecutionResult execute(String query, Map variables) { + GraphQLSchema preSchema = SchemaBuilder.builder().classpath("com.fleetpin.graphql.builder.type.directive").build().build(); + GraphQL schema = GraphQL.newGraphQL(new IntrospectionWithDirectivesSupport().apply(preSchema)).build(); + + var input = ExecutionInput.newExecutionInput(); + input.query(query); + if (variables != null) { + input.variables(variables); + } + ExecutionResult result = schema.execute(input); + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/MetaTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/MetaTest.java new file mode 100644 index 00000000..5e939ff4 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/MetaTest.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import graphql.schema.GraphQLObjectType; +import org.junit.jupiter.api.Test; + +public class MetaTest { + + @Test + public void testDeprecated() throws ReflectiveOperationException { + var schema = SchemaBuilder.build("com.fleetpin.graphql.builder.type"); + + var query = schema.getQueryType().getField("deprecatedTest"); + assertTrue(query.isDeprecated()); + assertEquals("old", query.getDeprecationReason()); + + GraphQLObjectType type = (GraphQLObjectType) schema.getType("DeprecatedObject"); + var field = type.getField("naame"); + assertTrue(field.isDeprecated()); + assertEquals("spelling", field.getDeprecationReason()); + } + + @Test + public void testDescription() throws ReflectiveOperationException { + var schema = SchemaBuilder.build("com.fleetpin.graphql.builder.type"); + + var query = schema.getQueryType().getField("descriptionTest"); + assertEquals("returns something", query.getDescription()); + + GraphQLObjectType type = (GraphQLObjectType) schema.getType("DescriptionObject"); + assertEquals("test description comes through", type.getDescription()); + var field = type.getField("name"); + assertEquals("first and last", field.getDescription()); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/ParameterParsingTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/ParameterParsingTest.java new file mode 100644 index 00000000..a5be89fb --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/ParameterParsingTest.java @@ -0,0 +1,236 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import graphql.ExecutionResult; +import graphql.GraphQL; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class ParameterParsingTest { + + //TODO:add failure cases + @Test + public void testRequiredString() throws ReflectiveOperationException { + Map> response = execute("query {requiredString(type: \"There\")} ").getData(); + assertEquals("There", response.get("requiredString")); + } + + @Test + public void testOptionalStringPresent() throws ReflectiveOperationException { + Map> response = execute("query {optionalString(type: \"There\")} ").getData(); + assertEquals("There", response.get("optionalString")); + } + + @Test + public void testOptionalStringNull() throws ReflectiveOperationException { + Map> response = execute("query {optionalString(type: null)} ").getData(); + assertEquals(null, response.get("optionalString")); + } + + @Test + public void testOptionalStringMissing() throws ReflectiveOperationException { + Map> response = execute("query {optionalString} ").getData(); + assertEquals(null, response.get("optionalString")); + } + + //TODO:id checks don't confirm actual an id + @Test + public void testRequiredId() throws ReflectiveOperationException { + Map> response = execute("query {testRequiredId(type: \"There\")} ").getData(); + assertEquals("There", response.get("testRequiredId")); + } + + @Test + public void testOptionalIdPresent() throws ReflectiveOperationException { + Map> response = execute("query {optionalId(type: \"There\")} ").getData(); + assertEquals("There", response.get("optionalId")); + } + + @Test + public void testOptionalIdNull() throws ReflectiveOperationException { + Map response = execute("query {optionalId(type: null)} ").getData(); + assertEquals(null, response.get("optionalId")); + assertTrue(response.containsKey("optionalId")); + } + + @Test + public void testOptionalIdNullAlways() throws ReflectiveOperationException { + Map response = execute("query {optionalIdNull{nullOptional}} ").getData(); + assertEquals(null, response.get("optionalId")); + } + + @Test + public void testRequiredListStringEmpty() throws ReflectiveOperationException { + Map>> response = execute("query {requiredListString(type: [])} ").getData(); + assertEquals(Collections.emptyList(), response.get("requiredListString")); + } + + @Test + public void testRequiredListString() throws ReflectiveOperationException { + Map>> response = execute("query {requiredListString(type: [\"free\"])} ").getData(); + assertEquals(Arrays.asList("free"), response.get("requiredListString")); + } + + @Test + public void testRequiredArrayString() throws ReflectiveOperationException { + Map>> response = execute("query {requiredArrayString(type: [\"free\"])} ").getData(); + assertEquals(Arrays.asList("free"), response.get("requiredArrayString")); + } + + @Test + public void testOptionalListStringEmpty() throws ReflectiveOperationException { + Map>> response = execute("query {optionalListString(type: [])} ").getData(); + assertEquals(Collections.emptyList(), response.get("optionalListString")); + } + + @Test + public void testOptionalListString() throws ReflectiveOperationException { + Map>> response = execute("query {optionalListString(type: [\"free\"])} ").getData(); + assertEquals(Arrays.asList("free"), response.get("optionalListString")); + } + + @Test + public void testOptionalListStringNull() throws ReflectiveOperationException { + Map>> response = execute("query {optionalListString} ").getData(); + assertEquals(null, response.get("optionalListString")); + } + + @Test + public void testRequiredListOptionalString() throws ReflectiveOperationException { + Map>> response = execute("query {requiredListOptionalString(type: [null, \"free\"])} ").getData(); + assertEquals(Arrays.asList(null, "free"), response.get("requiredListOptionalString")); + } + + @Test + public void testOptionalListOptionalString() throws ReflectiveOperationException { + Map>> response = execute("query {optionalListOptionalString(type: [null, \"free\"])} ").getData(); + assertEquals(Arrays.asList(null, "free"), response.get("optionalListOptionalString")); + } + + @Test + public void testOptionalListOptionalStringNull() throws ReflectiveOperationException { + Map>> response = execute("query {optionalListOptionalString} ").getData(); + assertEquals(null, response.get("optionalListOptionalString")); + } + + @Test + public void testRequiredListIdEmpty() throws ReflectiveOperationException { + Map>> response = execute("query {requiredListId(type: [])} ").getData(); + assertEquals(Collections.emptyList(), response.get("requiredListId")); + } + + @Test + public void testRequiredListId() throws ReflectiveOperationException { + Map>> response = execute("query {requiredListId(type: [\"free\"])} ").getData(); + assertEquals(Arrays.asList("free"), response.get("requiredListId")); + } + + @Test + public void testOptionalListIdEmpty() throws ReflectiveOperationException { + Map>> response = execute("query {optionalListId(type: [])} ").getData(); + assertEquals(Collections.emptyList(), response.get("optionalListId")); + } + + @Test + public void testOptionalListId() throws ReflectiveOperationException { + Map>> response = execute("query {optionalListId(type: [\"free\"])} ").getData(); + assertEquals(Arrays.asList("free"), response.get("optionalListId")); + } + + @Test + public void testOptionalListIdNull() throws ReflectiveOperationException { + Map>> response = execute("query {optionalListId} ").getData(); + assertEquals(null, response.get("optionalListId")); + } + + @Test + public void testRequiredListOptionalId() throws ReflectiveOperationException { + Map>> response = execute("query {requiredListOptionalId(type: [null, \"free\"])} ").getData(); + assertEquals(Arrays.asList(null, "free"), response.get("requiredListOptionalId")); + } + + @Test + public void testOptionalListOptionalId() throws ReflectiveOperationException { + Map>> response = execute("query {optionalListOptionalId(type: [null, \"free\"])} ").getData(); + assertEquals(Arrays.asList(null, "free"), response.get("optionalListOptionalId")); + } + + @Test + public void testOptionalListOptionalIdNull() throws ReflectiveOperationException { + Map>> response = execute("query {optionalListOptionalId} ").getData(); + assertEquals(null, response.get("optionalListOptionalId")); + } + + @Test + public void testMultipleArguments() throws ReflectiveOperationException { + Map>> response = execute("query {multipleArguments(first: \"free\", second: \"bird\")} ").getData(); + assertEquals("free:bird", response.get("multipleArguments")); + } + + @Test + public void testMultipleArgumentsOptional() throws ReflectiveOperationException { + Map>> response = execute("query {multipleArgumentsOptional(first: \"free\", second: \"bird\")} ").getData(); + assertEquals("free:bird", response.get("multipleArgumentsOptional")); + } + + @Test + public void testMultipleArgumentsOptionalPartial1() throws ReflectiveOperationException { + Map>> response = execute("query {multipleArgumentsOptional(second: \"bird\")} ").getData(); + assertEquals(":bird", response.get("multipleArgumentsOptional")); + } + + @Test + public void testMultipleArgumentsOptionalPartial2() throws ReflectiveOperationException { + Map>> response = execute("query {multipleArgumentsOptional(second: null)} ").getData(); + assertEquals(":", response.get("multipleArgumentsOptional")); + } + + @Test + public void testMultipleArgumentsOptionalPartial3() throws ReflectiveOperationException { + Map>> response = execute("query {multipleArgumentsOptional} ").getData(); + assertEquals(":", response.get("multipleArgumentsOptional")); + } + + @Test + public void testMultipleArgumentsMix1() throws ReflectiveOperationException { + Map>> response = execute("query {multipleArgumentsMix(first: \"free\")} ").getData(); + assertEquals("free:", response.get("multipleArgumentsMix")); + } + + @Test + public void testMultipleArgumentsMix2() throws ReflectiveOperationException { + Map>> response = execute("query {multipleArgumentsMix(first: \"free\", second: null)} ").getData(); + assertEquals("free:", response.get("multipleArgumentsMix")); + } + + @Test + public void testMultipleArgumentsMix3() throws ReflectiveOperationException { + Map>> response = execute("query {multipleArgumentsMix(first: \"free\", second: \"bird\")} ").getData(); + assertEquals("free:bird", response.get("multipleArgumentsMix")); + } + + private ExecutionResult execute(String query) throws ReflectiveOperationException { + var schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.parameter")).build(); + ExecutionResult result = schema.execute(query); + if (!result.getErrors().isEmpty()) { + throw new RuntimeException(result.getErrors().toString()); //TODO:cleanup + } + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/ParameterTypeParsingTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/ParameterTypeParsingTest.java new file mode 100644 index 00000000..27e53b91 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/ParameterTypeParsingTest.java @@ -0,0 +1,185 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class ParameterTypeParsingTest { + + public static final ObjectMapper MAPPER = new ObjectMapper() + .registerModule(new ParameterNamesModule()) + .registerModule(new Jdk8Module()) + .registerModule(new JavaTimeModule()) + .setVisibility(PropertyAccessor.FIELD, Visibility.ANY); + + // TODO:add failure cases + @Test + public void testRequiredType() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map> response = execute("query test($type: InputTestInput!){requiredType(type: $type){value}} ", "{\"value\": \"There\"}") + .getData(); + assertEquals("There", response.get("requiredType").get("value")); + } + + @Test + public void testEnum() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map> response = execute("query enumTest($type: AnimalType!){enumTest(type: $type)} ", "\"CAT\"").getData(); + assertEquals("CAT", response.get("enumTest")); + } + + @Test + public void testDescription() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map> response = execute( + "{" + + " __type(name: \"AnimalType\") {" + + " name" + + " kind" + + " description" + + " enumValues {" + + " name" + + " description" + + " }" + + " }" + + "} ", + null + ) + .getData(); + + var type = response.get("__type"); + + assertEquals("enum desc", type.get("description")); + + Map dog = new HashMap<>(); + dog.put("name", "DOG"); + dog.put("description", null); + + assertEquals(List.of(Map.of("name", "CAT", "description", "A cat"), dog), type.get("enumValues")); + } + + @Test + public void testOptionalTypePresent() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map> response = execute("query test($type: InputTestInput){optionalType(type: $type){value}} ", "{\"value\": \"There\"}") + .getData(); + assertEquals("There", response.get("optionalType").get("value")); + } + + @Test + public void testOptionalTypeNull() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map> response = execute("query test($type: InputTestInput){optionalType(type: $type){value}} ", null).getData(); + assertEquals(null, response.get("optionalType")); + } + + @Test + public void testOptionalTypeMissing() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map> response = execute("query test{optionalType{value}} ", null).getData(); + assertEquals(null, response.get("optionalType")); + } + + @Test + public void testRequiredListTypeEmpty() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map>> response = execute("query {requiredListType(type: []){value}} ", null).getData(); + assertEquals(Collections.emptyList(), response.get("requiredListType")); + } + + @Test + public void testRequiredListType() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map>> response = execute( + "query test($type: [InputTestInput!]!){requiredListType(type: $type){value}} ", + "[{\"value\": \"There\"}]" + ) + .getData(); + assertEquals("There", response.get("requiredListType").get(0).get("value")); + } + + @Test + public void testOptionalListTypeEmpty() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map>> response = execute("query {optionalListType(type: []){value}} ", null).getData(); + assertEquals(Collections.emptyList(), response.get("optionalListType")); + } + + @Test + public void testOptionalListType() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map>> response = execute( + "query test($type: [InputTestInput!]){optionalListType(type: $type){value}} ", + "[{\"value\": \"There\"}]" + ) + .getData(); + assertEquals("There", response.get("optionalListType").get(0).get("value")); + } + + @Test + public void testOptionalListTypeNull() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map>> response = execute("query {optionalListType{value}} ", null).getData(); + assertEquals(null, response.get("optionalListType")); + } + + @Test + public void testRequiredListOptionalType() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map>> response = execute( + "query test($type: [InputTestInput]!){requiredListOptionalType(type: $type){value}} ", + "[null, {\"value\": \"There\"}]" + ) + .getData(); + assertEquals("There", response.get("requiredListOptionalType").get(1).get("value")); + assertEquals(null, response.get("requiredListOptionalType").get(0)); + } + + @Test + public void testOptionalListOptionalType() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map>> response = execute( + "query test($type: [InputTestInput]){optionalListOptionalType(type: $type){value}} ", + "[null, {\"value\": \"There\"}]" + ) + .getData(); + assertEquals("There", response.get("optionalListOptionalType").get(1).get("value")); + assertEquals(null, response.get("optionalListOptionalType").get(0)); + } + + @Test + public void testOptionalListOptionalTypeNull() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map>> response = execute("query {optionalListOptionalType{value}} ", null).getData(); + assertEquals(null, response.get("optionalListOptionalType")); + } + + private ExecutionResult execute(String query, String type) throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Object obj = null; + if (type != null) { + obj = MAPPER.readValue(type, Object.class); + } + Map variables = new HashMap<>(); + variables.put("type", obj); + + var schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.parameter")).build(); + var input = ExecutionInput.newExecutionInput().query(query).variables(variables).build(); + ExecutionResult result = schema.execute(input); + if (!result.getErrors().isEmpty()) { + throw new RuntimeException(result.getErrors().toString()); // TODO:cleanup + } + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/PublishRestrictions.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/PublishRestrictions.java new file mode 100644 index 00000000..9d137f9d --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/PublishRestrictions.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import graphql.GraphQL; +import io.reactivex.rxjava3.core.Flowable; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +public class PublishRestrictions { + + @Test + public void testOptionalArray() throws ReflectiveOperationException { + var schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.publishRestrictions")).build(); + var res = schema.execute("subscription {test {value}} "); + Publisher response = res.getData(); + assertEquals(0, Flowable.fromPublisher(response).count().blockingGet()); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/RecordTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/RecordTest.java new file mode 100644 index 00000000..f077c4d3 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/RecordTest.java @@ -0,0 +1,174 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.introspection.IntrospectionWithDirectivesSupport; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +//does not test all of records as needs newer version of java. But Classes that look like records +public class RecordTest { + + @Test + public void testEntireContext() { + var type = Map.of("name", "foo", "age", 4); + Map> response = execute( + "query passthrough($type: InputTypeInput!){passthrough(type: $type) {name age weight}} ", + Map.of("type", type) + ) + .getData(); + var passthrough = response.get("passthrough"); + var expected = new HashMap<>(type); + expected.put("weight", null); + assertEquals(expected, passthrough); + } + + @Test + public void testDescription() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map> response = execute( + "{" + + " __type(name: \"InputTypeInput\") {" + + " name" + + " kind" + + " description" + + " inputFields {" + + " name" + + " description" + + " }" + + " }" + + "} ", + null + ) + .getData(); + + var type = response.get("__type"); + System.out.println(type); + assertEquals("record Type", type.get("description")); + + Map age = new HashMap<>(); + age.put("name", "age"); + age.put("description", null); + + Map weight = new HashMap<>(); + weight.put("name", "weight"); + weight.put("description", null); + + assertEquals(List.of(Map.of("name", "name", "description", "the name"), age, weight), type.get("inputFields")); + } + + @Test + public void testNullable() { + var response = execute("query nullableTest($type: Boolean){nullableTest(type: $type)} ", null); + var expected = new HashMap(); + expected.put("nullableTest", null); + assertEquals(expected, response.getData()); + assertTrue(response.getErrors().isEmpty()); + } + + @Test + public void testSetNullable() { + Map response = execute("query nullableTest($type: Boolean){nullableTest(type: $type)}", Map.of("type", true)).getData(); + var passthrough = response.get("nullableTest"); + assertEquals(true, passthrough); + } + + @Test + public void testNullableArray() { + List array = new ArrayList<>(); + array.add(true); + var response = execute("query nullableArrayTest($type: [Boolean!]){nullableArrayTest(type: $type)}", Map.of("type", array)); + var expected = new HashMap>(); + expected.put("nullableArrayTest", array); + assertTrue(response.getErrors().isEmpty()); + assertEquals(expected, response.getData()); + } + + @Test + public void testNullable2Array() { + var response = execute("query nullableArrayTest($type: [Boolean!]){nullableArrayTest(type: $type)}", Map.of()); + var expected = new HashMap>(); + expected.put("nullableArrayTest", null); + assertTrue(response.getErrors().isEmpty()); + assertEquals(expected, response.getData()); + } + + @Test + public void testNullableArrayFails() { + List array = new ArrayList<>(); + array.add(true); + var response = execute("query nullableArrayTest($type: [Boolean]){nullableArrayTest(type: $type)}", Map.of("type", array)); + assertFalse(response.getErrors().isEmpty()); + } + + @Test + public void testNullableInnerArray() { + List array = new ArrayList<>(); + array.add(null); + array.add(true); + var response = execute("query nullableInnerArrayTest($type: [Boolean]!){nullableInnerArrayTest(type: $type)}", Map.of("type", array)); + var expected = new HashMap>(); + expected.put("nullableInnerArrayTest", array); + assertTrue(response.getErrors().isEmpty()); + assertEquals(expected, response.getData()); + } + + @Test + public void testNullableInnerArrayFails() { + List array = new ArrayList<>(); + array.add(true); + var response = execute("query nullableInnerArrayTest($type: [Boolean]){nullableInnerArrayTest(type: $type)}", Map.of("type", array)); + assertFalse(response.getErrors().isEmpty()); + } + + @Test + public void testInnerNullableArray() { + List array = new ArrayList<>(); + array.add(null); + array.add(true); + var response = execute("query innerNullableArrayTest($type: [Boolean]!){innerNullableArrayTest(type: $type)}", Map.of("type", array)); + var expected = new HashMap>(); + expected.put("innerNullableArrayTest", array); + assertTrue(response.getErrors().isEmpty()); + assertEquals(expected, response.getData()); + } + + @Test + public void testInnerNullableArrayFails() { + List array = new ArrayList<>(); + array.add(true); + var response = execute("query innerNullableArrayTest($type: [Boolean]){innerNullableArrayTest(type: $type)}", Map.of("type", array)); + assertFalse(response.getErrors().isEmpty()); + } + + private ExecutionResult execute(String query, Map variables) { + GraphQL schema = GraphQL.newGraphQL(new IntrospectionWithDirectivesSupport().apply(SchemaBuilder.build("com.fleetpin.graphql.builder.record"))).build(); + var input = ExecutionInput.newExecutionInput(); + input.query(query); + if (variables != null) { + input.variables(variables); + } + ExecutionResult result = schema.execute(input); + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/RenameTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/RenameTest.java new file mode 100644 index 00000000..5133f278 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/RenameTest.java @@ -0,0 +1,83 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.introspection.IntrospectionWithDirectivesSupport; +import java.util.Map; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +public class RenameTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + public void testClassRename() throws JsonProcessingException, JSONException { + var type = Map.of("nameSet", "foo"); + var response = execute(""" + query passthroughClass($type: ClassTypeInput!) { + passthroughClass(type: $type) { + nameGet + } + } + """, Map.of("type", type)) + .toSpecification(); + JSONAssert.assertEquals(""" + { + "data": { + "passthroughClass": { + "nameGet": "foo" + } + } + } + """, MAPPER.writeValueAsString(response), false); + } + + @Test + public void testRecordRename() throws JsonProcessingException, JSONException { + var type = Map.of("name", "foo"); + var response = execute(""" + query passthroughRecord($type: RecordTypeInput!) { + passthroughRecord(type: $type) { + name + } + } + """, Map.of("type", type)) + .toSpecification(); + JSONAssert.assertEquals(""" + { + "data": { + "passthroughRecord": { + "name": "foo" + } + } + } + """, MAPPER.writeValueAsString(response), false); + } + + private ExecutionResult execute(String query, Map variables) { + GraphQL schema = GraphQL.newGraphQL(new IntrospectionWithDirectivesSupport().apply(SchemaBuilder.build("com.fleetpin.graphql.builder.rename"))).build(); + var input = ExecutionInput.newExecutionInput(); + input.query(query); + if (variables != null) { + input.variables(variables); + } + ExecutionResult result = schema.execute(input); + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/ScalarTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/ScalarTest.java new file mode 100644 index 00000000..76bd04eb --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/ScalarTest.java @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.fleetpin.graphql.builder.scalar.Fur; +import com.fleetpin.graphql.builder.scalar.Shape; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.introspection.IntrospectionWithDirectivesSupport; +import graphql.scalars.ExtendedScalars; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ScalarTest { + + @Test + public void testCatFur() throws ReflectiveOperationException { + var scalar = getField("Fur", "SCALAR"); + assertEquals("soft", scalar.get("description")); + } + + @Test + public void testCatShape() throws ReflectiveOperationException { + var scalar = getField("Shape", "SCALAR"); + assertEquals(null, scalar.get("description")); + List> directive = (List>) scalar.get("appliedDirectives"); + assertEquals("Capture", directive.get(0).get("name")); + } + + public Map getField(String typeName, String kind) throws ReflectiveOperationException { + Map> response = execute( + "{" + + " __type(name: \"" + + typeName + + "\") {" + + " name" + + " description" + + " kind" + + " appliedDirectives {\n" + + " name\n" + + " args {\n" + + " name\n" + + " value\n" + + " }\n" + + " }" + + " }" + + "} " + ) + .getData(); + var type = response.get("__type"); + Assertions.assertEquals(typeName, type.get("name")); + Assertions.assertEquals(kind, type.get("kind")); + return type; + } + + @Test + public void testQueryCatFur() throws ReflectiveOperationException { + Map> response = execute( + "query fur($fur: Fur!, $age: Long!){getCat(fur: $fur, age: $age){ fur age}} ", + Map.of("fur", "long", "age", 2) + ) + .getData(); + var cat = response.get("getCat"); + + var fur = cat.get("fur"); + + assertEquals("long", fur.getInput()); + assertEquals(2L, cat.get("age")); + } + + @Test + public void testQueryShape() throws ReflectiveOperationException { + Map response = execute("query shape($shape: Shape!){getShape(shape: $shape)} ", Map.of("shape", "round")).getData(); + var shape = response.get("getShape"); + + assertEquals("round", shape.getInput()); + } + + private ExecutionResult execute(String query) { + return execute(query, null); + } + + private ExecutionResult execute(String query, Map variables) { + GraphQL schema = GraphQL + .newGraphQL( + new IntrospectionWithDirectivesSupport() + .apply(SchemaBuilder.builder().classpath("com.fleetpin.graphql.builder.scalar").scalar(ExtendedScalars.GraphQLLong).build().build()) + ) + .build(); + var input = ExecutionInput.newExecutionInput(); + input.query(query); + if (variables != null) { + input.variables(variables); + } + ExecutionResult result = schema.execute(input); + if (!result.getErrors().isEmpty()) { + throw new RuntimeException(result.getErrors().toString()); //TODO:cleanup + } + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeGenericInputParsingTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeGenericInputParsingTest.java new file mode 100644 index 00000000..b8b0ba80 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeGenericInputParsingTest.java @@ -0,0 +1,205 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import graphql.ExecutionResult; +import graphql.GraphQL; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TypeGenericInputParsingTest { + + @Test + public void testAnimalName() throws ReflectiveOperationException { + var name = getField("Animal", "INTERFACE", "name"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testCatName() throws ReflectiveOperationException { + var name = getField("Cat", "OBJECT", "name"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testCatInputName() throws ReflectiveOperationException { + var name = getInputField("CatInput", "INPUT_OBJECT", "name"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testCatInputFur() throws ReflectiveOperationException { + var name = getInputField("CatInput", "INPUT_OBJECT", "fur"); + var nonNull = confirmNonNull(name); + confirmBoolean(nonNull); + } + + @Test + public void testAnimalInputName() throws ReflectiveOperationException { + var name = getInputField("CatAnimalInput", "INPUT_OBJECT", "id"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testAnimalInputGenericName() throws ReflectiveOperationException { + var name = getInputField("AnimalInput_Cat", "INPUT_OBJECT", "id"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testAnimalInputCat() throws ReflectiveOperationException { + var name = getInputField("CatAnimalInput", "INPUT_OBJECT", "animal"); + var nonNull = confirmNonNull(name); + confirmInputObject(nonNull, "CatInput"); + } + + @Test + public void testAnimalInputGenericCat() throws ReflectiveOperationException { + var name = getInputField("AnimalInput_Cat", "INPUT_OBJECT", "animal"); + var nonNull = confirmNonNull(name); + confirmInputObject(nonNull, "CatInput"); + } + + private void confirmString(Map type) { + Assertions.assertEquals("SCALAR", type.get("kind")); + Assertions.assertEquals("String", type.get("name")); + } + + private void confirmInputObject(Map type, String name) { + Assertions.assertEquals("INPUT_OBJECT", type.get("kind")); + Assertions.assertEquals(name, type.get("name")); + } + + private Map confirmNonNull(Map type) { + Assertions.assertEquals("NON_NULL", type.get("kind")); + var toReturn = (Map) type.get("ofType"); + Assertions.assertNotNull(toReturn); + return toReturn; + } + + private void confirmBoolean(Map type) { + Assertions.assertEquals("SCALAR", type.get("kind")); + Assertions.assertEquals("Boolean", type.get("name")); + } + + public Map getField(String typeName, String kind, String name) throws ReflectiveOperationException { + Map> response = execute( + "{" + + " __type(name: \"" + + typeName + + "\") {" + + " name" + + " kind" + + " fields {" + + " name" + + " type {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " }" + + " }" + + " }" + + " }" + + " }" + + " }" + + "} " + ) + .getData(); + var type = response.get("__type"); + Assertions.assertEquals(typeName, type.get("name")); + Assertions.assertEquals(kind, type.get("kind")); + List> fields = (List>) type.get("fields"); + var field = fields.stream().filter(map -> map.get("name").equals(name)).findAny().get(); + Assertions.assertEquals(name, field.get("name")); + return (Map) field.get("type"); + } + + public Map getInputField(String typeName, String kind, String name) throws ReflectiveOperationException { + Map> response = execute( + "{" + + " __type(name: \"" + + typeName + + "\") {" + + " name" + + " kind" + + " inputFields {" + + " name" + + " type {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " }" + + " }" + + " }" + + " }" + + " }" + + " }" + + "} " + ) + .getData(); + var type = response.get("__type"); + Assertions.assertEquals(typeName, type.get("name")); + Assertions.assertEquals(kind, type.get("kind")); + List> fields = (List>) type.get("inputFields"); + var field = fields.stream().filter(map -> map.get("name").equals(name)).findAny().get(); + Assertions.assertEquals(name, field.get("name")); + return (Map) field.get("type"); + } + + @Test + public void testQueryCatFur() throws ReflectiveOperationException { + Map response = execute("mutation {addCat(input: {id: \"1\", animal: {name: \"felix\", fur: false}})} ").getData(); + var cat = response.get("addCat"); + assertEquals("felix", cat); + } + + @Test + public void testQueryCatFurGeneric() throws ReflectiveOperationException { + Map response = execute("mutation {addCatGenerics(input: {id: \"1\", animal: {name: \"felix\", fur: true}})} ").getData(); + var cat = response.get("addCatGenerics"); + assertEquals(true, cat); + } + + private ExecutionResult execute(String query) { + GraphQL schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.inputgenerics")).build(); + ExecutionResult result = schema.execute(query); + if (!result.getErrors().isEmpty()) { + throw new RuntimeException(result.getErrors().toString()); + } + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeGenericInputRecords.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeGenericInputRecords.java new file mode 100644 index 00000000..babab3b7 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeGenericInputRecords.java @@ -0,0 +1,81 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import graphql.ExceptionWhileDataFetching; +import graphql.ExecutionResult; +import graphql.GraphQL; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class TypeGenericInputRecords { + + @Test + public void textQuery() throws ReflectiveOperationException { + Map response = execute(""" + query {doChange(input: { + name: { + wrap: ["felix"] + }, + age: { + wrap: [234] + }, + description: { + wrap: "cat" + } + })} + """) + .getData(); + var change = response.get("doChange"); + assertEquals("felix[234]cat", change); + } + + @Test + public void textCorrectNullableQuery() throws ReflectiveOperationException { + Map response = execute(""" + query {doChange(input: { + name: { + wrap: ["felix"] + }, + age: { + }, + description: { + wrap: "cat" + } + })} + """).getData(); + var change = response.get("doChange"); + assertEquals("felixnullcat", change); + } + + @Test + public void textQueryNull() throws ReflectiveOperationException { + Map response = execute(""" + query {doChange(input: {})} + """).getData(); + var change = response.get("doChange"); + assertEquals("empty", change); + } + + private ExecutionResult execute(String query) { + GraphQL schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.inputgenericsRecords")).build(); + ExecutionResult result = schema.execute(query); + if (!result.getErrors().isEmpty()) { + ExceptionWhileDataFetching d = (ExceptionWhileDataFetching) result.getErrors().get(0); + d.getException().printStackTrace(); + throw new RuntimeException(result.getErrors().toString()); + } + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeGenericParsingTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeGenericParsingTest.java new file mode 100644 index 00000000..4d588ee1 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeGenericParsingTest.java @@ -0,0 +1,318 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import graphql.ExecutionResult; +import graphql.GraphQL; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TypeGenericParsingTest { + + @Test + public void testAnimalName() throws ReflectiveOperationException { + var name = getField("Animal", "INTERFACE", "name"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testAnimalFur() throws ReflectiveOperationException { + var name = getField("Animal", "INTERFACE", "fur"); + var nonNull = confirmNonNull(name); + confirmInterface(nonNull, "Fur"); + } + + @Test + public void testAnimalFurs() throws ReflectiveOperationException { + var name = getField("Animal", "INTERFACE", "furs"); + var type = confirmNonNull(name); + type = confirmArray(type); + type = confirmNonNull(type); + confirmInterface(type, "Fur"); + } + + @Test + public void testCatName() throws ReflectiveOperationException { + var name = getField("Cat", "OBJECT", "name"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testCatFur() throws ReflectiveOperationException { + var name = getField("Cat", "OBJECT", "fur"); + var nonNull = confirmNonNull(name); + confirmObject(nonNull, "CatFur"); + } + + @Test + public void testCatFurs() throws ReflectiveOperationException { + var name = getField("Cat", "OBJECT", "furs"); + var type = confirmNonNull(name); + type = confirmArray(type); + type = confirmNonNull(type); + confirmObject(type, "CatFur"); + } + + @Test + public void testDogName() throws ReflectiveOperationException { + var name = getField("Dog", "OBJECT", "name"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testDogFur() throws ReflectiveOperationException { + var name = getField("Dog", "OBJECT", "fur"); + var nonNull = confirmNonNull(name); + confirmObject(nonNull, "DogFur"); + } + + @Test + public void testDogFurs() throws ReflectiveOperationException { + var name = getField("Dog", "OBJECT", "furs"); + var type = confirmNonNull(name); + type = confirmArray(type); + type = confirmNonNull(type); + confirmObject(type, "DogFur"); + } + + @Test + public void testFurName() throws ReflectiveOperationException { + var name = getField("Fur", "INTERFACE", "length"); + var nonNull = confirmNonNull(name); + confirmNumber(nonNull); + } + + @Test + public void testCatFurCalico() throws ReflectiveOperationException { + var name = getField("CatFur", "OBJECT", "calico"); + var nonNull = confirmNonNull(name); + confirmBoolean(nonNull); + } + + @Test + public void testCatFurLong() throws ReflectiveOperationException { + var name = getField("CatFur", "OBJECT", "long"); + var nonNull = confirmNonNull(name); + confirmBoolean(nonNull); + } + + @Test + public void testDogFurShaggy() throws ReflectiveOperationException { + var name = getField("DogFur", "OBJECT", "shaggy"); + var nonNull = confirmNonNull(name); + confirmBoolean(nonNull); + } + + @Test + public void testCatFamilyName() throws ReflectiveOperationException { + var name = getField("CatFamily", "INTERFACE", "name"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testCatFamilyFur() throws ReflectiveOperationException { + var name = getField("CatFamily", "INTERFACE", "fur"); + var nonNull = confirmNonNull(name); + confirmInterface(nonNull, "CatFamilyFur"); + } + + @Test + public void testCatFamilyFurs() throws ReflectiveOperationException { + var name = getField("CatFamily", "INTERFACE", "furs"); + var type = confirmNonNull(name); + type = confirmArray(type); + type = confirmNonNull(type); + confirmInterface(type, "CatFamilyFur"); + } + + @Test + public void testCatFamilyFurCalico() throws ReflectiveOperationException { + var name = getField("CatFamilyFur", "INTERFACE", "length"); + var nonNull = confirmNonNull(name); + confirmNumber(nonNull); + } + + @Test + public void testCatFamilyFurLong() throws ReflectiveOperationException { + var name = getField("CatFamilyFur", "INTERFACE", "long"); + var nonNull = confirmNonNull(name); + confirmBoolean(nonNull); + } + + private void confirmString(Map type) { + Assertions.assertEquals("SCALAR", type.get("kind")); + Assertions.assertEquals("String", type.get("name")); + } + + private void confirmInterface(Map type, String name) { + Assertions.assertEquals("INTERFACE", type.get("kind")); + Assertions.assertEquals(name, type.get("name")); + } + + private void confirmObject(Map type, String name) { + Assertions.assertEquals("OBJECT", type.get("kind")); + Assertions.assertEquals(name, type.get("name")); + } + + private void confirmBoolean(Map type) { + Assertions.assertEquals("SCALAR", type.get("kind")); + Assertions.assertEquals("Boolean", type.get("name")); + } + + private void confirmNumber(Map type) { + Assertions.assertEquals("SCALAR", type.get("kind")); + Assertions.assertEquals("Int", type.get("name")); + } + + private Map confirmNonNull(Map type) { + Assertions.assertEquals("NON_NULL", type.get("kind")); + var toReturn = (Map) type.get("ofType"); + Assertions.assertNotNull(toReturn); + return toReturn; + } + + private Map confirmArray(Map type) { + Assertions.assertEquals("LIST", type.get("kind")); + var toReturn = (Map) type.get("ofType"); + Assertions.assertNotNull(toReturn); + return toReturn; + } + + public Map getField(String typeName, String kind, String name) throws ReflectiveOperationException { + Map> response = execute( + "{" + + " __type(name: \"" + + typeName + + "\") {" + + " name" + + " kind" + + " fields {" + + " name" + + " type {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " }" + + " }" + + " }" + + " }" + + " }" + + " }" + + "} " + ) + .getData(); + var type = response.get("__type"); + Assertions.assertEquals(typeName, type.get("name")); + Assertions.assertEquals(kind, type.get("kind")); + + List> fields = (List>) type.get("fields"); + var field = fields.stream().filter(map -> map.get("name").equals(name)).findAny().get(); + Assertions.assertEquals(name, field.get("name")); + return (Map) field.get("type"); + } + + @Test + public void testQueryCatFur() throws ReflectiveOperationException { + Map>> response = execute( + "query {animals{" + + "name " + + "... on Cat { " + + " fur{ " + + " calico " + + " length" + + " catFur: long" + + " }" + + "} " + + "... on Dog {" + + " fur {" + + " shaggy" + + " dogFur: long" + + " length" + + " } " + + "} " + + "}} " + ) + .getData(); + + var animals = response.get("animals"); + + var cat = animals.get(0); + var dog = animals.get(1); + + var catFur = (Map) cat.get("fur"); + var dogFur = (Map) dog.get("fur"); + + assertEquals("name", cat.get("name")); + assertEquals(4, catFur.get("length")); + assertEquals(true, catFur.get("calico")); + assertEquals(true, catFur.get("catFur")); + + assertEquals(4, dogFur.get("length")); + assertEquals(true, dogFur.get("shaggy")); + assertEquals("very", dogFur.get("dogFur")); + } + + @Test + public void testMutationCatFur() throws ReflectiveOperationException { + Map>> response = execute( + "mutation {makeCat{" + + "item { " + + " ... on Cat { " + + " name " + + " fur{ " + + " calico " + + " length" + + " long" + + " }" + + " } " + + "}" + + "}} " + ) + .getData(); + + var makeCat = response.get("makeCat"); + + var cat = makeCat.get("item"); + + var catFur = (Map) cat.get("fur"); + + assertEquals("name", cat.get("name")); + assertEquals(4, catFur.get("length")); + assertEquals(true, catFur.get("calico")); + assertEquals(true, catFur.get("long")); + } + + private ExecutionResult execute(String query) { + GraphQL schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.generics")).build(); + ExecutionResult result = schema.execute(query); + if (!result.getErrors().isEmpty()) { + throw new RuntimeException(result.getErrors().toString()); + } + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeInheritanceParsingTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeInheritanceParsingTest.java new file mode 100644 index 00000000..36808ac6 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeInheritanceParsingTest.java @@ -0,0 +1,498 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fleetpin.graphql.builder.exceptions.InvalidOneOfException; +import graphql.ExceptionWhileDataFetching; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.validation.ValidationError; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TypeInheritanceParsingTest { + + @Test + public void findTypes() { + Map>>> response = execute("{__schema {types {name}}} ").getData(); + var types = response.get("__schema").get("types"); + var count = types.stream().filter(map -> map.get("name").equals("SimpleType")).count(); + Assertions.assertEquals(1, count); + } + + @Test + public void testAnimalName() { + var name = getField("Animal", "INTERFACE", "name"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testAnimalInputName() { + var name = getField("AnimalInput", "INPUT_OBJECT", "cat"); + confirmInputObject(name, "CatInput"); + } + + @Test + public void testCatName() { + var name = getField("Cat", "OBJECT", "name"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testCatAge() { + var name = getField("Cat", "OBJECT", "age"); + var nonNull = confirmNonNull(name); + confirmNumber(nonNull); + } + + @Test + public void testCatFur() { + var name = getField("Cat", "OBJECT", "fur"); + confirmBoolean(name); + } + + @Test + public void testCatCalico() { + var name = getField("Cat", "OBJECT", "calico"); + var nonNull = confirmNonNull(name); + confirmBoolean(nonNull); + } + + @Test + public void testDogName() { + var name = getField("Dog", "OBJECT", "name"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testDogFur() { + var name = getField("Dog", "OBJECT", "fur"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testDogAge() { + var name = getField("Dog", "OBJECT", "age"); + var nonNull = confirmNonNull(name); + confirmNumber(nonNull); + } + + @Test + public void testCatDescription() { + var type = getField("Cat", "OBJECT", null); + Assertions.assertEquals("cat type", type.get("description")); + } + + @Test + public void testCatFurDescription() { + var type = getField("Cat", "OBJECT", null); + + List> fields = (List>) type.get("fields"); + var field = fields.stream().filter(map -> map.get("name").equals("fur")).findAny().get(); + Assertions.assertEquals("get fur", field.get("description")); + } + + @Test + public void testCatWeightArgumentDescription() { + var type = getField("Cat", "OBJECT", null); + + List> fields = (List>) type.get("fields"); + var field = fields.stream().filter(map -> map.get("name").equals("weight")).findAny().get(); + Assertions.assertEquals(null, field.get("description")); + + List> args = (List>) field.get("args"); + var round = args.stream().filter(map -> map.get("name").equals("round")).findAny().get(); + Assertions.assertEquals("whole number", round.get("description")); + } + + @Test + public void testMutationDescription() { + var type = getField("Mutations", "OBJECT", null); + + List> fields = (List>) type.get("fields"); + var field = fields.stream().filter(map -> map.get("name").equals("getCat")).findAny().get(); + Assertions.assertEquals("cat endpoint", field.get("description")); + + List> args = (List>) field.get("args"); + var round = args.stream().filter(map -> map.get("name").equals("age")).findAny().get(); + Assertions.assertEquals("sample", round.get("description")); + } + + @Test + public void testCatFurInputDescription() { + var type = getField("CatInput", "INPUT_OBJECT", null); + + List> fields = (List>) type.get("inputFields"); + var field = fields.stream().filter(map -> map.get("name").equals("fur")).findAny().get(); + Assertions.assertEquals("set fur", field.get("description")); + } + + @Test + public void testInputCatDescription() { + var type = getField("CatInput", "INPUT_OBJECT", null); + Assertions.assertEquals("cat type", type.get("description")); + } + + @Test + public void testInputOneOfDescription() { + var type = getField("AnimalInput", "INPUT_OBJECT", null); + Assertions.assertEquals("animal desc", type.get("description")); + List> fields = (List>) type.get("inputFields"); + var field = fields.stream().filter(map -> map.get("name").equals("dog")).findAny().get(); + Assertions.assertEquals("A dog", field.get("description")); + } + + private void confirmString(Map type) { + Assertions.assertEquals("SCALAR", type.get("kind")); + Assertions.assertEquals("String", type.get("name")); + } + + private void confirmInputObject(Map type, String name) { + Assertions.assertEquals("INPUT_OBJECT", type.get("kind")); + Assertions.assertEquals(name, type.get("name")); + } + + private void confirmBoolean(Map type) { + Assertions.assertEquals("SCALAR", type.get("kind")); + Assertions.assertEquals("Boolean", type.get("name")); + } + + private void confirmNumber(Map type) { + Assertions.assertEquals("SCALAR", type.get("kind")); + Assertions.assertEquals("Int", type.get("name")); + } + + private Map confirmNonNull(Map type) { + Assertions.assertEquals("NON_NULL", type.get("kind")); + var toReturn = (Map) type.get("ofType"); + Assertions.assertNotNull(toReturn); + return toReturn; + } + + private Map confirmArray(Map type) { + Assertions.assertEquals("LIST", type.get("kind")); + var toReturn = (Map) type.get("ofType"); + Assertions.assertNotNull(toReturn); + return toReturn; + } + + public Map getField(String typeName, String kind, String name) { + Map> response = execute( + "{" + + " __type(name: \"" + + typeName + + "\") {" + + " name" + + " kind" + + " description" + + " fields {" + + " name" + + " description" + + " args {" + + " name" + + " description" + + " }" + + " type {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " }" + + " }" + + " }" + + " }" + + " }" + + " inputFields {" + + " name" + + " description" + + " type {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " }" + + " }" + + " }" + + " }" + + " }" + + " }" + + "} " + ) + .getData(); + var type = response.get("__type"); + Assertions.assertEquals(typeName, type.get("name")); + Assertions.assertEquals(kind, type.get("kind")); + + if (name == null) { + return type; + } + + List> fields = (List>) type.get("fields"); + if (fields == null) { + fields = (List>) type.get("inputFields"); + } + var field = fields.stream().filter(map -> map.get("name").equals(name)).findAny().get(); + Assertions.assertEquals(name, field.get("name")); + return (Map) field.get("type"); + } + + @Test + public void testQueryCatFur() { + Map>> response = execute( + "query {animals{" + "name " + "... on Cat { " + " age " + " fur " + " calico " + "} " + "... on Dog {" + " age " + "} " + "}} " + ) + .getData(); + + var animals = response.get("animals"); + + var cat = animals.get(0); + var dog = animals.get(1); + + assertEquals("name", cat.get("name")); + assertEquals(3, cat.get("age")); + assertEquals(true, cat.get("fur")); + assertEquals(true, cat.get("calico")); + + assertEquals("name", dog.get("name")); + assertEquals(6, dog.get("age")); + } + + @Test + public void testQueryDogFur() { + Map>> response = execute( + "query {animals{" + "name " + "... on Cat { " + " age " + " calico " + "} " + "... on Dog {" + " age " + " fur " + "} " + "}} " + ) + .getData(); + + var animals = response.get("animals"); + + var cat = animals.get(0); + var dog = animals.get(1); + + assertEquals("name", cat.get("name")); + assertEquals(3, cat.get("age")); + assertEquals(true, cat.get("calico")); + + assertEquals("name", dog.get("name")); + assertEquals(6, dog.get("age")); + assertEquals("shaggy", dog.get("fur")); + } + + @Test + public void testBothFurFails() { + var result = execute( + "query {animals{" + "name " + "... on Cat { " + " age " + " fur " + " calico " + "} " + "... on Dog {" + " age " + " fur " + "} " + "}} " + ); + + assertFalse(result.getErrors().isEmpty()); + assertTrue(result.getErrors().get(0) instanceof ValidationError); + } + + @Test + public void testOneOf() { + Map>> response = execute( + "mutation {myAnimals(animals: [" + + "{cat: {fur: true, calico: false, name: \"socks\", age: 4}}," + + "{dog: {fur: \"short\", name: \"patches\", age: 5}}" + + "]){" + + "name " + + "... on Cat { " + + " age " + + " calico " + + "} " + + "... on Dog {" + + " age " + + " fur " + + "} " + + "}} " + ) + .getData(); + + var animals = response.get("myAnimals"); + + var cat = animals.get(0); + var dog = animals.get(1); + + assertEquals("socks", cat.get("name")); + assertEquals(4, cat.get("age")); + assertEquals(false, cat.get("calico")); + + assertEquals("patches", dog.get("name")); + assertEquals(5, dog.get("age")); + assertEquals("short", dog.get("fur")); + } + + @Test + public void testOptionalFieldNotSet() { + Map>> response = execute( + "mutation {myAnimals(animals: [" + + "{cat: {calico: false, name: \"socks\", age: 4}}," + + "]){" + + "name " + + "... on Cat { " + + " age " + + " calico " + + " fur " + + "} " + + "... on Dog {" + + " age " + + "} " + + "}} " + ) + .getData(); + + var animals = response.get("myAnimals"); + + var cat = animals.get(0); + + assertEquals("socks", cat.get("name")); + assertEquals(4, cat.get("age")); + assertEquals(false, cat.get("calico")); + assertEquals(true, cat.get("fur")); + } + + @Test + public void testOptionalFieldNull() { + Map>> response = execute( + "mutation {myAnimals(animals: [" + + "{cat: {fur: null, calico: false, name: \"socks\", age: 4}}," + + "]){" + + "name " + + "... on Cat { " + + " age " + + " calico " + + " fur " + + "} " + + "... on Dog {" + + " age " + + "} " + + "}} " + ) + .getData(); + + var animals = response.get("myAnimals"); + + var cat = animals.get(0); + + assertEquals("socks", cat.get("name")); + assertEquals(4, cat.get("age")); + assertEquals(false, cat.get("calico")); + assertNull(cat.get("fur")); + } + + @Test + public void testOneOfError() { + var result = execute( + "mutation {myAnimals(animals: [" + + "{cat: {fur: true, calico: false, name: \"socks\", age: 4}," + + "dog: {fur: \"short\", name: \"patches\", age: 5}}" + + "]){" + + "name " + + "... on Cat { " + + " age " + + " calico " + + "} " + + "... on Dog {" + + " age " + + " fur " + + "} " + + "}} " + ); + + assertFalse(result.getErrors().isEmpty()); + var exception = ((ExceptionWhileDataFetching) result.getErrors().get(0)).getException(); + assertTrue(exception.getMessage().contains("OneOf must only have a single field set. Fields: cat, dog")); + } + + @Test + public void testOneOfErrorEmpty() { + var result = execute( + "mutation {myAnimals(animals: [" + + "{}" + + "]){" + + "name " + + "... on Cat { " + + " age " + + " calico " + + "} " + + "... on Dog {" + + " age " + + " fur " + + "} " + + "}} " + ); + + assertFalse(result.getErrors().isEmpty()); + var exception = ((ExceptionWhileDataFetching) result.getErrors().get(0)).getException(); + assertTrue(exception.getMessage().contains("OneOf must have a field set")); + } + + @Test + public void testOneOfErrorField() { + var result = execute( + "mutation {myAnimals(animals: [" + + "{cat: {fur: null, calico: false, name: \"socks\", age: 4, error: \"fail\"}}" + + "]){" + + "name " + + "... on Cat { " + + " age " + + " calico " + + "} " + + "... on Dog {" + + " age " + + " fur " + + "} " + + "}} " + ); + + assertFalse(result.getErrors().isEmpty()); + var exception = ((ExceptionWhileDataFetching) result.getErrors().get(0)).getException(); + assertTrue(exception.getMessage().contains("ERROR")); + } + + private ExecutionResult execute(String query) { + return execute(query, null); + } + + private ExecutionResult execute(String query, Map variables) { + GraphQL schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.type")).build(); + var input = ExecutionInput.newExecutionInput(); + input.query(query); + if (variables != null) { + input.variables(variables); + } + return schema.execute(input); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeParsingTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeParsingTest.java new file mode 100644 index 00000000..feaec82e --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/TypeParsingTest.java @@ -0,0 +1,247 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import graphql.ExecutionResult; +import graphql.GraphQL; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TypeParsingTest { + + @Test + public void findTypes() throws ReflectiveOperationException { + Map>>> response = execute("{__schema {types {name}}} ").getData(); + var types = response.get("__schema").get("types"); + var count = types.stream().filter(map -> map.get("name").equals("SimpleType")).count(); + Assertions.assertEquals(1, count); + } + + @Test + public void testName() throws ReflectiveOperationException { + var name = getField("name"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void testDeleted() throws ReflectiveOperationException { + var name = getField("deleted"); + var nonNull = confirmNonNull(name); + confirmBoolean(nonNull); + } + + @Test + public void testAlive() throws ReflectiveOperationException { + var name = getField("alive"); + confirmBoolean(name); + } + + @Test + public void testParts() throws ReflectiveOperationException { + var type = getField("parts"); + type = confirmNonNull(type); + type = confirmArray(type); + type = confirmNonNull(type); + confirmString(type); + } + + @Test + public void testGappyParts() throws ReflectiveOperationException { + var type = getField("gappyParts"); + type = confirmNonNull(type); + type = confirmArray(type); + confirmString(type); + } + + @Test + public void testOptioanlParts() throws ReflectiveOperationException { + var type = getField("optionalParts"); + type = confirmArray(type); + type = confirmNonNull(type); + confirmString(type); + } + + @Test + public void testOptioanlGappyParts() throws ReflectiveOperationException { + var type = getField("optionalGappyParts"); + type = confirmArray(type); + confirmString(type); + } + + @Test + public void testNameFuture() throws ReflectiveOperationException { + var name = getField("nameFuture"); + var nonNull = confirmNonNull(name); + confirmString(nonNull); + } + + @Test + public void isDeletedFuture() throws ReflectiveOperationException { + var name = getField("deletedFuture"); + var nonNull = confirmNonNull(name); + confirmBoolean(nonNull); + } + + @Test + public void testAliveFuture() throws ReflectiveOperationException { + var name = getField("aliveFuture"); + confirmBoolean(name); + } + + @Test + public void testPartsFuture() throws ReflectiveOperationException { + var type = getField("partsFuture"); + type = confirmNonNull(type); + type = confirmArray(type); + type = confirmNonNull(type); + confirmString(type); + } + + @Test + public void testGappyPartsFuture() throws ReflectiveOperationException { + var type = getField("gappyPartsFuture"); + type = confirmNonNull(type); + type = confirmArray(type); + confirmString(type); + } + + @Test + public void testOptionalPartsFuture() throws ReflectiveOperationException { + var type = getField("optionalPartsFuture"); + type = confirmArray(type); + type = confirmNonNull(type); + confirmString(type); + } + + @Test + public void testOptionalGappyPartsFuture() throws ReflectiveOperationException { + var type = getField("optionalGappyPartsFuture"); + type = confirmArray(type); + confirmString(type); + } + + private void confirmString(Map type) { + Assertions.assertEquals("SCALAR", type.get("kind")); + Assertions.assertEquals("String", type.get("name")); + } + + private void confirmBoolean(Map type) { + Assertions.assertEquals("SCALAR", type.get("kind")); + Assertions.assertEquals("Boolean", type.get("name")); + } + + private Map confirmNonNull(Map type) { + Assertions.assertEquals("NON_NULL", type.get("kind")); + var toReturn = (Map) type.get("ofType"); + Assertions.assertNotNull(toReturn); + return toReturn; + } + + private Map confirmArray(Map type) { + Assertions.assertEquals("LIST", type.get("kind")); + var toReturn = (Map) type.get("ofType"); + Assertions.assertNotNull(toReturn); + return toReturn; + } + + public Map getField(String name) throws ReflectiveOperationException { + Map> response = execute( + "{" + + " __type(name: \"SimpleType\") {" + + " name" + + " fields {" + + " name" + + " type {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " ofType {" + + " name" + + " kind" + + " }" + + " }" + + " }" + + " }" + + " }" + + " }" + + "} " + ) + .getData(); + var type = response.get("__type"); + Assertions.assertEquals("SimpleType", type.get("name")); + + List> fields = (List>) type.get("fields"); + var field = fields.stream().filter(map -> map.get("name").equals(name)).findAny().get(); + Assertions.assertEquals(name, field.get("name")); + return (Map) field.get("type"); + } + + @Test + public void testQuery() throws ReflectiveOperationException { + Map> response = execute( + "query {simpleType{" + + "name " + + "deleted " + + "alive " + + "parts " + + "gappyParts " + + "optionalParts " + + "optionalGappyParts " + + "nameFuture " + + "deletedFuture " + + "aliveFuture " + + "partsFuture " + + "gappyPartsFuture " + + "optionalPartsFuture " + + "optionalGappyPartsFuture " + + "}} " + ) + .getData(); + + var simpleType = response.get("simpleType"); + assertEquals("green", simpleType.get("name")); + assertEquals(false, simpleType.get("deleted")); + assertEquals(null, simpleType.get("alive")); + assertEquals(Arrays.asList("green", "eggs"), simpleType.get("parts")); + assertEquals(Arrays.asList(null, "eggs"), simpleType.get("gappyParts")); + assertEquals(null, simpleType.get("optionalParts")); + assertEquals(Arrays.asList(), simpleType.get("optionalGappyParts")); + + assertEquals("green", simpleType.get("nameFuture")); + assertEquals(false, simpleType.get("deletedFuture")); + assertEquals(false, simpleType.get("aliveFuture")); + assertEquals(Arrays.asList(), simpleType.get("partsFuture")); + assertEquals(Arrays.asList(), simpleType.get("gappyPartsFuture")); + assertEquals(Arrays.asList(), simpleType.get("optionalPartsFuture")); + assertEquals(null, simpleType.get("optionalGappyPartsFuture")); + } + + private ExecutionResult execute(String query) throws ReflectiveOperationException { + var schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.type")).build(); + ExecutionResult result = schema.execute(query); + if (!result.getErrors().isEmpty()) { + throw new RuntimeException(result.getErrors().toString()); //TODO:cleanup + } + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/UnionTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/UnionTest.java new file mode 100644 index 00000000..58cec936 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/UnionTest.java @@ -0,0 +1,85 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import graphql.ExecutionResult; +import graphql.GraphQL; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class UnionTest { + + @Test + public void testUnion() throws ReflectiveOperationException { + Map>> response = execute( + "query {union{" + + " __typename" + + " ... on SimpleType {" + + " name" + + " }" + + " ... on UnionType {" + + " type {" + + " __typename" + + " ... on SimpleType {" + + " name" + + " }" + + " }" + + " }" + + "}} " + ) + .getData(); + var union = response.get("union"); + assertEquals("green", union.get(0).get("name")); + Map simple = (Map) union.get(1).get("type"); + assertEquals("green", simple.get("name")); + } + + @Test + public void testUnionFailure() throws ReflectiveOperationException { + var error = assertThrows( + RuntimeException.class, + () -> + execute( + "query {unionFailure{" + + " __typename" + + " ... on SimpleType {" + + " name" + + " }" + + " ... on UnionType {" + + " type {" + + " __typename" + + " ... on SimpleType {" + + " name" + + " }" + + " }" + + " }" + + "}} " + ) + ); + assertEquals("Union Union_SimpleType_UnionType Does not support type Boolean", error.getMessage()); + } + + private ExecutionResult execute(String query) throws ReflectiveOperationException { + var schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.type")).build(); + ExecutionResult result = schema.execute(query); + if (!result.getErrors().isEmpty()) { + throw new RuntimeException(result.getErrors().toString()); //TODO:cleanup + } + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/authorizer/Cat.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/authorizer/Cat.java new file mode 100644 index 00000000..e228c565 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/authorizer/Cat.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.authorizer; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Query; + +@Entity +public class Cat { + + public boolean isCalico() { + return true; + } + + public int getAge() { + return 3; + } + + public boolean getFur() { + return true; + } + + @Query + public static Cat getCat(String name) { + return new Cat(); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/authorizer/CatAuthorizer.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/authorizer/CatAuthorizer.java new file mode 100644 index 00000000..bc0fe8f6 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/authorizer/CatAuthorizer.java @@ -0,0 +1,21 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.authorizer; + +import com.fleetpin.graphql.builder.Authorizer; + +public class CatAuthorizer implements Authorizer { + + public boolean allow(String name) { + return "socks".equals(name); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/context/GraphContext.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/context/GraphContext.java new file mode 100644 index 00000000..22101ac5 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/context/GraphContext.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.context; + +import com.fleetpin.graphql.builder.annotations.Context; + +@Context +public class GraphContext { + + private final String something; + + public GraphContext(String something) { + super(); + this.something = something; + } + + public String getSomething() { + return something; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/context/Queries.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/context/Queries.java new file mode 100644 index 00000000..f4aefff3 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/context/Queries.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.context; + +import com.fleetpin.graphql.builder.annotations.Context; +import com.fleetpin.graphql.builder.annotations.Query; +import graphql.GraphQLContext; +import graphql.schema.DataFetchingEnvironment; + +public class Queries { + + @Query + public static boolean entireContext(GraphQLContext context) { + return context != null; + } + + @Query + public static boolean env(DataFetchingEnvironment context) { + return context != null; + } + + @Query + public static boolean deprecatedContext(GraphContext context) { + return context != null; + } + + @Query + public static boolean namedContext(GraphContext named) { + return named != null; + } + + @Query + public static boolean namedParemeterContext(@Context String context) { + return context != null; + } + + @Query + public static boolean missingContext(GraphContext notPresent) { + return false; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/Animal.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/Animal.java new file mode 100644 index 00000000..8969d943 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/Animal.java @@ -0,0 +1,73 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.generics; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Mutation; +import com.fleetpin.graphql.builder.annotations.Query; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +@Entity +public abstract class Animal { + + private final T fur; + + Animal(T fur) { + this.fur = fur; + } + + public String getName() { + return "name"; + } + + public T getFur() { + return fur; + } + + public List getFurs() { + return Arrays.asList(fur); + } + + @Query + public static List> animals() { + return Arrays.asList(new Cat(), new Dog()); + } + + @Mutation + public static MutationResponse makeCat() { + return new GenericMutationResponse<>(Optional.of(new Cat())); + } + + @Entity + public abstract static class MutationResponse { + + private Optional> item; + + public MutationResponse(Optional> item) { + this.item = item; + } + + public Optional> getItem() { + return item; + } + } + + @Entity + public static class GenericMutationResponse extends MutationResponse { + + public GenericMutationResponse(Optional> item) { + super(item); + } + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/Cat.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/Cat.java new file mode 100644 index 00000000..1a67ebd9 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/Cat.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.generics; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Mutation; + +@Entity +public class Cat extends CatFamily { + + public Cat() { + super(new CatFur()); + } + + @Mutation + public static Cat getCat() { + return null; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/CatFamily.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/CatFamily.java new file mode 100644 index 00000000..73a1d49b --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/CatFamily.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.generics; + +import com.fleetpin.graphql.builder.annotations.Entity; + +@Entity +public abstract class CatFamily extends Animal { + + CatFamily(R fur) { + super(fur); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/CatFamilyFur.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/CatFamilyFur.java new file mode 100644 index 00000000..07a9ea47 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/CatFamilyFur.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.generics; + +import com.fleetpin.graphql.builder.annotations.Entity; + +@Entity +public abstract class CatFamilyFur extends Fur { + + public boolean isLong() { + return true; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/CatFur.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/CatFur.java new file mode 100644 index 00000000..7a0730cc --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/CatFur.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.generics; + +import com.fleetpin.graphql.builder.annotations.Entity; + +@Entity +public class CatFur extends CatFamilyFur { + + public boolean isCalico() { + return true; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/Dog.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/Dog.java new file mode 100644 index 00000000..3e00a5c8 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/Dog.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.generics; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Mutation; + +@Entity +public class Dog extends Animal { + + public Dog() { + super(new DogFur()); + } + + public int getAge() { + return 6; + } + + @Mutation + public static Dog getDog() { + return null; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/DogFur.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/DogFur.java new file mode 100644 index 00000000..c91fb961 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/DogFur.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.generics; + +import com.fleetpin.graphql.builder.annotations.Entity; + +@Entity +public class DogFur extends Fur { + + public boolean isShaggy() { + return true; + } + + public String getLong() { + return "very"; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/Fur.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/Fur.java new file mode 100644 index 00000000..590e14dc --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/generics/Fur.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.generics; + +import com.fleetpin.graphql.builder.annotations.Entity; + +@Entity +public abstract class Fur { + + public int getLength() { + return 4; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/Animal.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/Animal.java new file mode 100644 index 00000000..e0e29ee6 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/Animal.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.inputgenerics; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.SchemaOption; + +@Entity +public abstract class Animal { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/AnimalInput.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/AnimalInput.java new file mode 100644 index 00000000..6f736b9b --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/AnimalInput.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.inputgenerics; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.SchemaOption; + +@Entity(SchemaOption.INPUT) +public class AnimalInput { + + String id; + T animal; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public T getAnimal() { + return animal; + } + + public void setAnimal(T animal) { + this.animal = animal; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/AnimalOuterWrapper.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/AnimalOuterWrapper.java new file mode 100644 index 00000000..5dfa8aa5 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/AnimalOuterWrapper.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.inputgenerics; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.SchemaOption; + +@Entity(SchemaOption.BOTH) +public class AnimalOuterWrapper { + + String id; + AnimalWrapper animal; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public AnimalWrapper getAnimal() { + return animal; + } + + public void setAnimal(AnimalWrapper animal) { + this.animal = animal; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/AnimalWrapper.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/AnimalWrapper.java new file mode 100644 index 00000000..19674dca --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/AnimalWrapper.java @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.inputgenerics; + +import com.fleetpin.graphql.builder.annotations.Entity; + +@Entity +public class AnimalWrapper { + + String id; + T animal; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public T getAnimal() { + return animal; + } + + public void setAnimal(T animal) { + this.animal = animal; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/Cat.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/Cat.java new file mode 100644 index 00000000..e0d45fe7 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/Cat.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.inputgenerics; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Mutation; +import com.fleetpin.graphql.builder.annotations.Query; +import com.fleetpin.graphql.builder.annotations.SchemaOption; + +@Entity(SchemaOption.BOTH) +public class Cat extends Animal { + + private boolean fur; + + public void setFur(boolean fur) { + this.fur = fur; + } + + public boolean isFur() { + return fur; + } + + @Query + public static String getCat() { + return "cat"; + } + + @Mutation + public static String addCat(CatAnimalInput input) { + return input.getAnimal().getName(); + } + + @Mutation + public static boolean addCatGenerics(AnimalInput input) { + return input.getAnimal().isFur(); + } + + @Mutation + public static AnimalOuterWrapper addNestedGenerics(AnimalInput input) { + var wrapper = new AnimalOuterWrapper(); + wrapper.setAnimal(new AnimalWrapper()); + wrapper.getAnimal().setAnimal(input.getAnimal()); + return wrapper; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/CatAnimalInput.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/CatAnimalInput.java new file mode 100644 index 00000000..560ef233 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenerics/CatAnimalInput.java @@ -0,0 +1,18 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.inputgenerics; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.SchemaOption; + +@Entity(SchemaOption.INPUT) +public class CatAnimalInput extends AnimalInput {} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenericsRecords/Change.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenericsRecords/Change.java new file mode 100644 index 00000000..f2f75338 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenericsRecords/Change.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.inputgenericsRecords; + +import com.fleetpin.graphql.builder.annotations.Query; +import jakarta.annotation.Nullable; +import java.util.List; + +public record Change(@Nullable Wrapper> name, @Nullable Wrapper> age, @Nullable Wrapper description) { + @Query + public static String doChange(Change input) { + if (input.name == null) { + return "empty"; + } + return input.name.wrap().getFirst() + input.age.wrap() + input.description.wrap(); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenericsRecords/Wrapper.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenericsRecords/Wrapper.java new file mode 100644 index 00000000..74365b8c --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/inputgenericsRecords/Wrapper.java @@ -0,0 +1,16 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.inputgenericsRecords; + +import jakarta.annotation.Nullable; + +public record Wrapper(@Nullable T wrap) {} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/methodArgs/Queries.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/methodArgs/Queries.java new file mode 100644 index 00000000..0d3acc5c --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/methodArgs/Queries.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.methodArgs; + +import com.fleetpin.graphql.builder.annotations.Query; + +public class Queries { + + @Query + public static InputType passthrough(InputType type) { + return type; + } + + static final class InputType { + + private final String name; + private final int age; + + private InputType(String name, int age) { + super(); + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public int getHeight(int height) { + return height; + } + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/methodArgsTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/methodArgsTest.java new file mode 100644 index 00000000..69ba7368 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/methodArgsTest.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.introspection.IntrospectionWithDirectivesSupport; +import java.util.Map; +import org.junit.jupiter.api.Test; + +//does not test all of records as needs newer version of java. But Classes that look like records +public class methodArgsTest { + + @Test + public void testEntireContext() { + var type = Map.of("name", "foo", "age", 4); + Map> response = execute( + "query passthrough($type: InputTypeInput!){passthrough(type: $type) {name age height(height: 12)}} ", + Map.of("type", type) + ) + .getData(); + var passthrough = response.get("passthrough"); + + assertEquals(Map.of("name", "foo", "age", 4, "height", 12), passthrough); + } + + private ExecutionResult execute(String query, Map variables) { + GraphQL schema = GraphQL + .newGraphQL(new IntrospectionWithDirectivesSupport().apply(SchemaBuilder.build("com.fleetpin.graphql.builder.methodArgs"))) + .build(); + var input = ExecutionInput.newExecutionInput(); + input.query(query); + if (variables != null) { + input.variables(variables); + } + ExecutionResult result = schema.execute(input); + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/parameter/Parameter.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/parameter/Parameter.java new file mode 100644 index 00000000..1480fad5 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/parameter/Parameter.java @@ -0,0 +1,117 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.parameter; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Id; +import com.fleetpin.graphql.builder.annotations.Query; +import java.util.List; +import java.util.Optional; + +@Entity +public class Parameter { + + public Optional getNullOptional() { + return null; + } + + @Query + public static String requiredString(String type) { + return type; + } + + @Query + public static Optional optionalString(Optional type) { + return type; + } + + @Query + @Id + public static String testRequiredId(@Id String type) { + return type; + } + + @Query + @Id + public static Optional optionalId(@Id Optional type) { + return type; + } + + @Query + public static Parameter optionalIdNull() { + return new Parameter(); + } + + @Query + public static List requiredListString(List type) { + return type; + } + + @Query + public static String[] requiredArrayString(String[] type) { + return type; + } + + @Query + public static Optional> optionalListString(Optional> type) { + return type; + } + + @Query + public static List> requiredListOptionalString(List> type) { + return type; + } + + @Query + public static Optional>> optionalListOptionalString(Optional>> type) { + return type; + } + + @Query + @Id + public static List requiredListId(@Id List type) { + return type; + } + + @Query + @Id + public static Optional> optionalListId(@Id Optional> type) { + return type; + } + + @Query + @Id + public static List> requiredListOptionalId(@Id List> type) { + return type; + } + + @Query + @Id + public static Optional>> optionalListOptionalId(@Id Optional>> type) { + return type; + } + + @Query + public static String multipleArguments(String first, String second) { + return first + ":" + second; + } + + @Query + public static String multipleArgumentsOptional(Optional first, Optional second) { + return first.orElse("") + ":" + second.orElse(""); + } + + @Query + public static String multipleArgumentsMix(String first, Optional second) { + return first + ":" + second.orElse(""); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/parameter/TypeInputParameter.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/parameter/TypeInputParameter.java new file mode 100644 index 00000000..81f6af8f --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/parameter/TypeInputParameter.java @@ -0,0 +1,89 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.parameter; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.GraphQLDescription; +import com.fleetpin.graphql.builder.annotations.Query; +import com.fleetpin.graphql.builder.annotations.SchemaOption; +import java.util.List; +import java.util.Optional; + +public class TypeInputParameter { + + @Entity + @GraphQLDescription("enum desc") + public enum AnimalType { + @GraphQLDescription("A cat") + CAT, + DOG, + } + + @Entity(SchemaOption.BOTH) + public static class InputTest { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + @Query + public static AnimalType enumTest(AnimalType type) { + return type; + } + + @Query + public static InputTest requiredType(InputTest type) { + type.getValue(); // makes sure is right type and not odd runtime passthrough + return type; + } + + @Query + public static Optional optionalType(Optional type) { + type.map(InputTest::getValue); + return type; + } + + @Query + public static List requiredListType(List type) { + for (var t : type) { + t.getValue(); + } + return type; + } + + @Query + public static Optional> optionalListType(Optional> type) { + type.map(tt -> tt.stream().map(t -> t.getValue())); + return type; + } + + @Query + public static List> requiredListOptionalType(List> type) { + for (var t : type) { + t.map(InputTest::getValue); + } + return type; + } + + @Query + public static Optional>> optionalListOptionalType(Optional>> type) { + type.map(tt -> tt.stream().map(t -> t.map(InputTest::getValue))); + return type; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/publishRestrictions/Test.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/publishRestrictions/Test.java new file mode 100644 index 00000000..ff0927a6 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/publishRestrictions/Test.java @@ -0,0 +1,64 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.publishRestrictions; + +import com.fleetpin.graphql.builder.RestrictType; +import com.fleetpin.graphql.builder.RestrictTypeFactory; +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Query; +import com.fleetpin.graphql.builder.annotations.Restrict; +import com.fleetpin.graphql.builder.annotations.Subscription; +import graphql.schema.DataFetchingEnvironment; +import io.reactivex.rxjava3.core.Flowable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import org.reactivestreams.Publisher; + +@Entity +@Restrict(Test.Restrictor.class) +public class Test { + + static Executor executor = CompletableFuture.delayedExecutor(100, TimeUnit.MILLISECONDS); + private boolean value; + + public Test(boolean value) { + this.value = value; + } + + public boolean isValue() { + return value; + } + + @Query + public static String MustHaveAQuery() { + return "String"; + } + + @Subscription + public static Publisher test() { + return Flowable.just(new Test(false)).flatMap(f -> Flowable.fromCompletionStage(CompletableFuture.supplyAsync(() -> f, executor))); + } + + public static class Restrictor implements RestrictTypeFactory, RestrictType { + + @Override + public CompletableFuture> create(DataFetchingEnvironment context) { + return CompletableFuture.supplyAsync(() -> this, executor); + } + + @Override + public CompletableFuture allow(Test obj) { + return CompletableFuture.supplyAsync(() -> false, executor); + } + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/record/Queries.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/record/Queries.java new file mode 100644 index 00000000..8c09be2e --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/record/Queries.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.record; + +import com.fleetpin.graphql.builder.annotations.GraphQLDescription; +import com.fleetpin.graphql.builder.annotations.InnerNullable; +import com.fleetpin.graphql.builder.annotations.Query; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; + +public class Queries { + + @Query + public static InputType passthrough(InputType type) { + return type; + } + + @Query + @Nullable + public static Boolean nullableTest(@Nullable Boolean type) { + return type; + } + + @Query + @Nullable + public static List nullableArrayTest(@Nullable List type) { + return type; + } + + @Query + @InnerNullable + public static List innerNullableArrayTest(@InnerNullable List type) { + return type; + } + + @Query + public static List> nullableInnerArrayTest(List> type) { + return type; + } + + @GraphQLDescription("record Type") + static final record InputType(@GraphQLDescription("the name") String name, int age, Optional weight) {} +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/rename/Queries.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/rename/Queries.java new file mode 100644 index 00000000..84be3fde --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/rename/Queries.java @@ -0,0 +1,47 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.rename; + +import com.fleetpin.graphql.builder.annotations.GraphQLName; +import com.fleetpin.graphql.builder.annotations.Query; + +public class Queries { + + @Query + @GraphQLName("passthroughClass") + public static ClassType passthroughClassWrong(@GraphQLName("type") ClassType typeWrong) { + return typeWrong; + } + + @Query + @GraphQLName("passthroughRecord") + public static RecordType passthroughRecordWrong(@GraphQLName("type") RecordType typeWrong) { + return typeWrong; + } + + public static record RecordType(@GraphQLName("name") String nameWrong) {} + + public static class ClassType { + + private String nameWrong; + + @GraphQLName("nameGet") + public String getNameWrong() { + return nameWrong; + } + + @GraphQLName("nameSet") + public void setNameWrong(String nameWrong) { + this.nameWrong = nameWrong; + } + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/EntityRestrictions.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/EntityRestrictions.java new file mode 100644 index 00000000..09a95a16 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/EntityRestrictions.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.restrictions; + +import com.fleetpin.graphql.builder.RestrictType; +import com.fleetpin.graphql.builder.RestrictTypeFactory; +import com.fleetpin.graphql.builder.restrictions.parameter.RestrictedEntity; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletableFuture; + +public class EntityRestrictions implements RestrictTypeFactory { + + @Override + public CompletableFuture> create(DataFetchingEnvironment context) { + return CompletableFuture.completedFuture(new DatabaseRestrict()); + } + + public static class DatabaseRestrict implements RestrictType { + + @Override + public CompletableFuture allow(RestrictedEntity obj) { + return CompletableFuture.completedFuture(obj.isAllowed()); + } + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/RestrictionTypesTest.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/RestrictionTypesTest.java new file mode 100644 index 00000000..62c766b4 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/RestrictionTypesTest.java @@ -0,0 +1,140 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.restrictions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fleetpin.graphql.builder.SchemaBuilder; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class RestrictionTypesTest { + + /* + * Really basic tests, just to ensure restrictions work on different types. + */ + + private static GraphQL schema; + + @BeforeAll + public static void init() throws ReflectiveOperationException { + schema = GraphQL.newGraphQL(SchemaBuilder.build("com.fleetpin.graphql.builder.restrictions.parameter")).build(); + } + + private static String singleQueryGql = "query entityQuery( $allowed: Boolean! ) { single(allowed: $allowed) { __typename } }"; + private static String singleOptionalQueryGql = "query entityQuery( $allowed: Boolean ) { singleOptional(allowed: $allowed) { __typename } }"; + private static String listQueryGql = "query entityQuery( $allowed: [Boolean!]! ) { list(allowed: $allowed) { __typename } }"; + private static String listOptionalQueryGql = "query entityQuery( $allowed: [Boolean!] ) { listOptional(allowed: $allowed) { __typename } }"; + private static final String LIST_QUERY_INHERITANCE_GPL = """ + query entityQuery( $allowed: [Boolean!]! ) { + listInheritance(allowed: $allowed) { + __typename + } + } + """; + + @Test + public void singleEntityQuery() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map variables = new HashMap<>(); + variables.put("allowed", true); + Map> response = execute(singleQueryGql, variables).getData(); + // No fancy checks. Just want to make ensure it executes without issue. + Assertions.assertTrue(response.get("single").containsKey("__typename")); + } + + @Test + public void singleOptionalEntityQuery() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map variables = new HashMap<>(); + + // Allowed + variables.put("allowed", true); + Map> responseAllowed = execute(singleOptionalQueryGql, variables).getData(); + Assertions.assertTrue(responseAllowed.get("singleOptional").containsKey("__typename")); + + // Not allowed + variables.put("allowed", false); + Map responseDenied = execute(singleOptionalQueryGql, variables).getData(); + Assertions.assertNull(responseDenied.get("singleOptional")); + } + + @Test + public void listEntityQuery() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map variables = new HashMap<>(); + + variables.put("allowed", Arrays.asList(true, true, true)); + Map> responseAllAllowed = execute(listQueryGql, variables).getData(); + Assertions.assertEquals(3, responseAllAllowed.get("list").size()); + + variables.put("allowed", Arrays.asList(true, false, true)); + Map> responseSomeAllowed = execute(listQueryGql, variables).getData(); + Assertions.assertEquals(2, responseSomeAllowed.get("list").size()); + + variables.put("allowed", Arrays.asList(false, false, false)); + Map> responseNoneAllowed = execute(listQueryGql, variables).getData(); + Assertions.assertEquals(0, responseNoneAllowed.get("list").size()); + } + + @Test + public void listEntityInheritanceQuery() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map variables = new HashMap<>(); + + variables.put("allowed", Arrays.asList(true, true, true)); + Map> responseAllAllowed = execute(LIST_QUERY_INHERITANCE_GPL, variables).getData(); + Assertions.assertEquals(3, responseAllAllowed.get("listInheritance").size()); + + variables.put("allowed", Arrays.asList(true, false, true)); + Map> responseSomeAllowed = execute(LIST_QUERY_INHERITANCE_GPL, variables).getData(); + Assertions.assertEquals(2, responseSomeAllowed.get("listInheritance").size()); + + variables.put("allowed", Arrays.asList(false, false, false)); + Map> responseNoneAllowed = execute(LIST_QUERY_INHERITANCE_GPL, variables).getData(); + Assertions.assertEquals(0, responseNoneAllowed.get("listInheritance").size()); + } + + @Test + public void optionalListEntityQuery() throws ReflectiveOperationException, JsonMappingException, JsonProcessingException { + Map variables = new HashMap<>(); + + // No list passed through + Map> responseNoVariables = execute(listOptionalQueryGql, variables).getData(); + Assertions.assertNull(responseNoVariables.get("listOptional")); + + variables.put("allowed", Arrays.asList(true, true, true)); + Map> responseAllAllowed = execute(listOptionalQueryGql, variables).getData(); + Assertions.assertEquals(3, responseAllAllowed.get("listOptional").size()); + + variables.put("allowed", Arrays.asList(true, false, true)); + Map> responseSomeAllowed = execute(listOptionalQueryGql, variables).getData(); + Assertions.assertEquals(2, responseSomeAllowed.get("listOptional").size()); + + variables.put("allowed", Arrays.asList(false, false, false)); + Map> responseNoneAllowed = execute(listOptionalQueryGql, variables).getData(); + Assertions.assertEquals(0, responseNoneAllowed.get("listOptional").size()); + } + + private static ExecutionResult execute(String query, Map variables) throws JsonMappingException, JsonProcessingException { + var input = ExecutionInput.newExecutionInput().query(query).variables(variables).build(); + ExecutionResult result = schema.execute(input); + if (!result.getErrors().isEmpty()) { + throw new RuntimeException(result.getErrors().toString()); + } + return result; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/parameter/RestrictedEntity.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/parameter/RestrictedEntity.java new file mode 100644 index 00000000..3d148750 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/parameter/RestrictedEntity.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.restrictions.parameter; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Query; +import com.fleetpin.graphql.builder.annotations.Restrict; +import com.fleetpin.graphql.builder.restrictions.EntityRestrictions; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Entity +@Restrict(EntityRestrictions.class) +public class RestrictedEntity { + + private boolean allowed; + + public boolean isAllowed() { + return allowed; + } + + public void setAllowed(boolean allowed) { + this.allowed = allowed; + } + + @Query + public static RestrictedEntity single(Boolean allowed) { + RestrictedEntity entity = new RestrictedEntity(); + entity.setAllowed(allowed); + return entity; + } + + @Query + public static Optional singleOptional(Optional allowed) { + if (allowed.isEmpty()) return Optional.empty(); + RestrictedEntity entity = new RestrictedEntity(); + entity.setAllowed(allowed.get()); + return Optional.of(entity); + } + + @Query + public static List list(List allowed) { + return allowed + .stream() + .map(isAllowed -> { + RestrictedEntity entity = new RestrictedEntity(); + entity.setAllowed(isAllowed); + return entity; + }) + .collect(Collectors.toList()); + } + + @Query + public static Optional> listOptional(Optional> allowed) { + if (allowed.isEmpty()) return Optional.empty(); + + return Optional.of( + allowed + .get() + .stream() + .map(isAllowed -> { + RestrictedEntity entity = new RestrictedEntity(); + entity.setAllowed(isAllowed); + return entity; + }) + .collect(Collectors.toList()) + ); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/parameter/RestrictedEntityInheritance.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/parameter/RestrictedEntityInheritance.java new file mode 100644 index 00000000..fa5b2181 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/parameter/RestrictedEntityInheritance.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.restrictions.parameter; + +import com.fleetpin.graphql.builder.annotations.Query; +import java.util.List; +import java.util.stream.Collectors; + +public class RestrictedEntityInheritance extends RestrictedEntityParent { + + @Query + public static List listInheritance(List allowed) { + return allowed + .stream() + .map(isAllowed -> { + RestrictedEntityInheritance entity = new RestrictedEntityInheritance(); + entity.setAllowed(isAllowed); + return entity; + }) + .collect(Collectors.toList()); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/parameter/RestrictedEntityParent.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/parameter/RestrictedEntityParent.java new file mode 100644 index 00000000..e7389276 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/restrictions/parameter/RestrictedEntityParent.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.restrictions.parameter; + +import com.fleetpin.graphql.builder.RestrictType; +import com.fleetpin.graphql.builder.RestrictTypeFactory; +import com.fleetpin.graphql.builder.annotations.Restrict; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletableFuture; + +@Restrict(RestrictedEntityParent.EntityRestrictions.class) +public abstract class RestrictedEntityParent { + + private boolean allowed; + + public boolean isAllowed() { + return allowed; + } + + public void setAllowed(boolean allowed) { + this.allowed = allowed; + } + + public static class EntityRestrictions implements RestrictTypeFactory { + + @Override + public CompletableFuture> create(DataFetchingEnvironment context) { + return CompletableFuture.completedFuture(new DatabaseRestrict()); + } + + public static class DatabaseRestrict implements RestrictType { + + @Override + public CompletableFuture allow(RestrictedEntityParent obj) { + return CompletableFuture.completedFuture(obj.isAllowed()); + } + } + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/Capture.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/Capture.java new file mode 100644 index 00000000..94a1f681 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/Capture.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.scalar; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.fleetpin.graphql.builder.annotations.Directive; +import graphql.introspection.Introspection.DirectiveLocation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Directive(DirectiveLocation.OBJECT) +@Retention(RUNTIME) +@Target({ ElementType.TYPE }) +public @interface Capture { +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/CaptureType.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/CaptureType.java new file mode 100644 index 00000000..413d7b43 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/CaptureType.java @@ -0,0 +1,18 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.scalar; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.SchemaOption; + +@Entity(SchemaOption.INPUT) +public class CaptureType {} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/Cat.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/Cat.java new file mode 100644 index 00000000..fcbdc9dd --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/Cat.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.scalar; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Query; + +@Entity +public class Cat { + + private final Fur fur; + + private long age; + + private Cat(Fur fur, long age) { + this.fur = fur; + this.age = age; + } + + public Fur getFur() { + return fur; + } + + public long getAge() { + return age; + } + + @Query + public static Cat getCat(Fur fur, Long age) { + return new Cat(fur, age); + } + + @Query + public static Shape getShape(Shape shape) { + return shape; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/Fur.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/Fur.java new file mode 100644 index 00000000..0c2f2a6b --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/Fur.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.scalar; + +import com.fleetpin.graphql.builder.annotations.GraphQLDescription; +import com.fleetpin.graphql.builder.annotations.Scalar; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; + +@Scalar(Fur.FurCoercing.class) +@GraphQLDescription("soft") +public class Fur { + + private String input; + + public Fur(String input) { + this.input = input; + } + + public String getInput() { + return input; + } + + public static class FurCoercing implements Coercing { + + @Override + public Fur serialize(Object dataFetcherResult) throws CoercingSerializeException { + return convertImpl(dataFetcherResult); + } + + @Override + public Fur parseValue(Object input) throws CoercingParseValueException { + return convertImpl(input); + } + + @Override + public Fur parseLiteral(Object input) throws CoercingParseLiteralException { + return convertImpl(input); + } + + private Fur convertImpl(Object input) { + if (input instanceof Fur) { + return (Fur) input; + } else if (input instanceof String) { + return new Fur((String) input); + } + throw new CoercingParseLiteralException(); + } + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/Shape.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/Shape.java new file mode 100644 index 00000000..fb73e844 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/scalar/Shape.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.scalar; + +import com.fleetpin.graphql.builder.annotations.Scalar; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; + +@Scalar(Shape.ShapeCoercing.class) +@Capture +public class Shape { + + private String input; + + public Shape(String input) { + this.input = input; + } + + public String getInput() { + return input; + } + + public static class ShapeCoercing implements Coercing { + + @Override + public Shape serialize(Object dataFetcherResult) throws CoercingSerializeException { + return convertImpl(dataFetcherResult); + } + + @Override + public Shape parseValue(Object input) throws CoercingParseValueException { + return convertImpl(input); + } + + @Override + public Shape parseLiteral(Object input) throws CoercingParseLiteralException { + return convertImpl(input); + } + + private Shape convertImpl(Object input) { + if (input instanceof Shape) { + return (Shape) input; + } else if (input instanceof String) { + return new Shape((String) input); + } + throw new CoercingParseLiteralException(); + } + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/Circular.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/Circular.java new file mode 100644 index 00000000..1f0bd46b --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/Circular.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Mutation; + +@Entity +public class Circular { + + public Circular getCircular() { + return null; + } + + @Mutation + public static Circular circularTest() { + return new Circular(); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/DeprecatedObject.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/DeprecatedObject.java new file mode 100644 index 00000000..c8e3800d --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/DeprecatedObject.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.GraphQLDeprecated; +import com.fleetpin.graphql.builder.annotations.Query; + +@Entity +public class DeprecatedObject { + + @GraphQLDeprecated("spelling") + public DeprecatedObject getNaame() { + return null; + } + + @Query + @GraphQLDeprecated("old") + public static DeprecatedObject deprecatedTest() { + return new DeprecatedObject(); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/DescriptionObject.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/DescriptionObject.java new file mode 100644 index 00000000..d3d58ba6 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/DescriptionObject.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.GraphQLDescription; +import com.fleetpin.graphql.builder.annotations.Query; + +@Entity +@GraphQLDescription("test description comes through") +public class DescriptionObject { + + @GraphQLDescription("first and last") + public DescriptionObject getName() { + return null; + } + + @Query + @GraphQLDescription("returns something") + public static DescriptionObject descriptionTest() { + return new DescriptionObject(); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/SimpleType.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/SimpleType.java new file mode 100644 index 00000000..9b21a5fa --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/SimpleType.java @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Query; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +@Entity +public class SimpleType { + + public String getName() { + return "green"; + } + + public boolean isDeleted() { + return false; + } + + public Optional getAlive() { + return Optional.empty(); + } + + public List getParts() { + return Arrays.asList("green", "eggs"); + } + + public List> getGappyParts() { + return Arrays.asList(Optional.empty(), Optional.of("eggs")); + } + + public Optional> getOptionalParts() { + return Optional.empty(); + } + + public Optional>> getOptionalGappyParts() { + return Optional.of(Arrays.asList()); + } + + public CompletableFuture getNameFuture() { + return CompletableFuture.completedFuture("green"); + } + + public CompletableFuture isDeletedFuture() { + return CompletableFuture.completedFuture(false); + } + + public CompletableFuture> getAliveFuture() { + return CompletableFuture.completedFuture(Optional.of(false)); + } + + public CompletableFuture> getPartsFuture() { + return CompletableFuture.completedFuture(Arrays.asList()); + } + + public CompletableFuture>> getGappyPartsFuture() { + return CompletableFuture.completedFuture(Arrays.asList()); + } + + public CompletableFuture>> getOptionalPartsFuture() { + return CompletableFuture.completedFuture(Optional.of(Arrays.asList())); + } + + public CompletableFuture>>> getOptionalGappyPartsFuture() { + return CompletableFuture.completedFuture(Optional.empty()); + } + + @Query + public static SimpleType simpleType() { + return new SimpleType(); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/UnionType.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/UnionType.java new file mode 100644 index 00000000..f754ba15 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/UnionType.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Query; +import com.fleetpin.graphql.builder.annotations.Union; +import java.util.List; + +@Entity +public class UnionType { + + private final Object type; + + public UnionType(Object type) { + this.type = type; + } + + @Union({ SimpleType.class, UnionType.class }) + public Object getType() { + return type; + } + + @Query + @Union({ SimpleType.class, UnionType.class }) + public static List union() { + return List.of(new SimpleType(), new UnionType(new SimpleType())); + } + + @Query + @Union({ SimpleType.class, UnionType.class }) + public static List unionFailure() { + return List.of(new UnionType(new UnionType(4d)), false); + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Admin.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Admin.java new file mode 100644 index 00000000..a1e1cfa4 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Admin.java @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type.directive; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.fleetpin.graphql.builder.DirectiveCaller; +import com.fleetpin.graphql.builder.annotations.DataFetcherWrapper; +import com.fleetpin.graphql.builder.annotations.Directive; +import graphql.introspection.Introspection; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@DataFetcherWrapper(Admin.Processor.class) +@Retention(RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface Admin { + String value(); + + static class Processor implements DirectiveCaller { + + @Override + public Object process(Admin annotation, DataFetchingEnvironment env, DataFetcher fetcher) throws Exception { + if (env.getArgument("name").equals(annotation.value())) { + return fetcher.get(env); + } + throw new RuntimeException("forbidden"); + } + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Capture.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Capture.java new file mode 100644 index 00000000..daa0d34b --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Capture.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type.directive; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.fleetpin.graphql.builder.annotations.Directive; +import graphql.introspection.Introspection; +import graphql.introspection.Introspection.DirectiveLocation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Directive({ Introspection.DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.SCHEMA }) +@Retention(RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface Capture { + String color(); +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/CaptureType.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/CaptureType.java new file mode 100644 index 00000000..3d7c9238 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/CaptureType.java @@ -0,0 +1,30 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type.directive; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.SchemaOption; + +@Entity(SchemaOption.INPUT) +public class CaptureType { + + private String color; + + public CaptureType setColor(String color) { + this.color = color; + return this; + } + + public String getColor() { + return color; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Cat.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Cat.java new file mode 100644 index 00000000..99354009 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Cat.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type.directive; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Query; + +@Entity +public class Cat { + + public boolean isCalico() { + return true; + } + + public int getAge() { + return 3; + } + + public boolean getFur() { + return true; + } + + @Query + @Capture(color = "meow") + public static Cat getCat() { + return new Cat(); + } + + @Query + @Uppercase + public static Cat getUpper() { + return new Cat(); + } + + @Query + @Admin("tabby") + public static String allowed(String name) { + return name; + } + + @Query + public static String getNickname(@Input("TT") String nickName) { + return nickName; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/CatSchema.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/CatSchema.java new file mode 100644 index 00000000..bc463f09 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/CatSchema.java @@ -0,0 +1,17 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type.directive; + +import com.fleetpin.graphql.builder.SchemaConfiguration; + +@Capture(color = "top") +public class CatSchema implements SchemaConfiguration {} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Input.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Input.java new file mode 100644 index 00000000..c83a3606 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Input.java @@ -0,0 +1,16 @@ +package com.fleetpin.graphql.builder.type.directive; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.fleetpin.graphql.builder.annotations.Directive; +import graphql.introspection.Introspection; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Directive(Introspection.DirectiveLocation.ARGUMENT_DEFINITION) +@Retention(RUNTIME) +@Target({ ElementType.PARAMETER }) +public @interface Input { + String value(); +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Uppercase.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Uppercase.java new file mode 100644 index 00000000..9886ba33 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/directive/Uppercase.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type.directive; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.fleetpin.graphql.builder.annotations.Directive; +import graphql.introspection.Introspection; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Directive(Introspection.DirectiveLocation.FIELD_DEFINITION) +@Retention(RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface Uppercase { +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/inheritance/Animal.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/inheritance/Animal.java new file mode 100644 index 00000000..99b24ea0 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/inheritance/Animal.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type.inheritance; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.GraphQLDescription; +import com.fleetpin.graphql.builder.annotations.Mutation; +import com.fleetpin.graphql.builder.annotations.OneOf; +import com.fleetpin.graphql.builder.annotations.Query; +import com.fleetpin.graphql.builder.annotations.SchemaOption; +import java.util.Arrays; +import java.util.List; + +@Entity(SchemaOption.BOTH) +@GraphQLDescription("animal desc") +@OneOf(value = { @OneOf.Type(name = "cat", type = Cat.class), @OneOf.Type(name = "dog", type = Dog.class, description = "A dog") }) +public abstract class Animal { + + private String name = "name"; + + @GraphQLDescription("the name") + public String getName() { + return name; + } + + @Query + public static List animals() { + return Arrays.asList(new Cat(), new Dog()); + } + + public void setName(String name) { + this.name = name; + } + + @Mutation + public static List myAnimals(List animals) { + return animals; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/inheritance/Cat.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/inheritance/Cat.java new file mode 100644 index 00000000..e1211879 --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/inheritance/Cat.java @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type.inheritance; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.GraphQLDescription; +import com.fleetpin.graphql.builder.annotations.Mutation; +import com.fleetpin.graphql.builder.annotations.SchemaOption; +import java.util.Optional; + +@Entity(SchemaOption.BOTH) +@GraphQLDescription("cat type") +public class Cat extends Animal { + + private boolean calico; + private int age; + private Optional fur; + + public Cat() { + calico = true; + age = 3; + fur = Optional.of(true); + } + + public Cat(boolean calico, int age, Optional fur) { + super(); + this.calico = calico; + this.age = age; + this.fur = fur; + } + + public boolean isCalico() { + return calico; + } + + public void setCalico(boolean calico) { + this.calico = calico; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @GraphQLDescription("get fur") + public Optional getFur() { + return fur; + } + + public Optional getWeight(@GraphQLDescription("whole number") Boolean round) { + return fur; + } + + @GraphQLDescription("set fur") + public void setFur(Optional fur) { + this.fur = fur; + } + + public void setError(Optional ignore) { + throw new RuntimeException("ERROR"); + } + + @Mutation + @GraphQLDescription("cat endpoint") + public static Cat getCat(@GraphQLDescription("sample") Optional age) { + return null; + } +} diff --git a/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/inheritance/Dog.java b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/inheritance/Dog.java new file mode 100644 index 00000000..6c7bbf4c --- /dev/null +++ b/graphql-builder/src/test/java/com/fleetpin/graphql/builder/type/inheritance/Dog.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.fleetpin.graphql.builder.type.inheritance; + +import com.fleetpin.graphql.builder.annotations.Entity; +import com.fleetpin.graphql.builder.annotations.Mutation; +import com.fleetpin.graphql.builder.annotations.SchemaOption; + +@Entity(SchemaOption.BOTH) +public class Dog extends Animal { + + private int age = 6; + private String fur = "shaggy"; + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getFur() { + return fur; + } + + public void setFur(String fur) { + this.fur = fur; + } + + @Mutation + public static Dog getDog() { + return null; + } +} diff --git a/pom.xml b/pom.xml index 74af136c..fa76cb37 100644 --- a/pom.xml +++ b/pom.xml @@ -13,9 +13,9 @@ + graphql-builder graphql-database-manager-core graphql-database-manager-test - graphql-database-manager-dynamo graphql-database-dynmodb-history-lambda