Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,7 @@ public InputTypeBuilder buildResolver() {

@Override
protected GraphQLNamedOutputType buildType() {
try {
return typeBuilder.buildType();
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
return typeBuilder.buildType();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@
import com.phocassoftware.graphql.builder.annotations.Entity;
import com.phocassoftware.graphql.builder.annotations.GraphQLDescription;
import com.phocassoftware.graphql.builder.annotations.GraphQLIgnore;
import com.phocassoftware.graphql.builder.exceptions.DuplicateMethodNameException;
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.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;

public abstract class TypeBuilder {

Expand All @@ -33,7 +38,7 @@ public TypeBuilder(EntityProcessor entityProcessor, TypeMeta meta) {
this.meta = meta;
}

public GraphQLNamedOutputType buildType() throws ReflectiveOperationException {
public GraphQLNamedOutputType buildType() {
Builder graphType = GraphQLObjectType.newObject();
String typeName = EntityUtil.getName(meta);
graphType.name(typeName);
Expand Down Expand Up @@ -154,8 +159,7 @@ private void addInterface(Builder graphType, GraphQLInterfaceType.Builder interf
interfaceBuilder.withInterface(interfaceName);
}

protected abstract void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder)
throws ReflectiveOperationException;
protected abstract void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder);

public static class ObjectType extends TypeBuilder {

Expand All @@ -164,22 +168,35 @@ public ObjectType(EntityProcessor entityProcessor, TypeMeta meta) {
}

@Override
protected void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder)
throws ReflectiveOperationException {
protected void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder) {
var type = meta.getType();
for (Method method : type.getMethods()) {
try {
var methods = type.getMethods();

var validMethods = new HashMap<String, Method>();

var duplicates = Arrays
.stream(methods)
.filter(method -> {
var name = EntityUtil.getter(method);
if (name.isEmpty()) {
continue;
}
var f = entityProcessor.getMethodProcessor().process(null, FieldCoordinates.coordinates(typeName, name.get()), meta, method);
if (name.isEmpty()) return false;
return validMethods.put(name.get(), method) != null;
Copy link
Contributor

@ashley-taylor ashley-taylor Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for filters like this. To reduce cognitive load they should all be pure functions. Don't mind it reading from out of scope but modifying makes reasoning hard

})
.map(Method::getName)
.toArray(String[]::new);

if (duplicates.length > 0) {
throw new DuplicateMethodNameException(typeName, duplicates);
}

validMethods.forEach((name, method) -> {
try {
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 " + method, e);
}
}
});
}
}

Expand All @@ -190,43 +207,39 @@ public Record(EntityProcessor entityProcessor, TypeMeta meta) {
}

@Override
protected void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder)
throws ReflectiveOperationException {
protected void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder) {
var type = meta.getType();

for (var field : type.getDeclaredFields()) {
var methods = Arrays
.stream(type.getDeclaredFields())
.filter(field -> !field.isSynthetic())
.filter(field -> !field.getDeclaringClass().equals(Object.class))
.filter(field -> !field.isAnnotationPresent(GraphQLIgnore.class))
.filter(field -> !Modifier.isAbstract(field.getModifiers()))
.filter(field -> !field.getDeclaringClass().isInterface())
.filter(field -> !Modifier.isStatic(field.getModifiers()))
.toList();
var validMethods = new HashMap<String, Method>();
var duplicates = methods.stream().filter(field -> {
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) {
var method = type.getMethod(field.getName());
if (method.isAnnotationPresent(GraphQLIgnore.class)) return false;
var name = EntityUtil.getName(field.getName(), field, method);
return validMethods.put(name, method) != null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the above filter, it modifies the state outside its scope

} catch (NoSuchMethodException e) {
throw new RuntimeException("Failed to process method " + field, e);
}
}).map(Field::getName).toArray(String[]::new);

if (duplicates.length > 0) {
throw new DuplicateMethodNameException(typeName, duplicates);
}

validMethods.forEach((name, method) -> {
var f = entityProcessor.getMethodProcessor().process(null, FieldCoordinates.coordinates(typeName, name), meta, method);
graphType.field(f);
interfaceBuilder.field(f);
});
}
}
}
Original file line number Diff line number Diff line change
@@ -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.phocassoftware.graphql.builder.exceptions;

import java.util.Arrays;

public class DuplicateMethodNameException extends RuntimeException {

private static final String MESSAGE_TEMPLATE = "Class: %s has overloaded method(s): %s. GraphQLBuilder does not support overloading methods";

public DuplicateMethodNameException(String typeName, String... methodNames) {
super(MESSAGE_TEMPLATE.formatted(typeName, Arrays.toString(methodNames)));
}
}
Original file line number Diff line number Diff line change
@@ -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.phocassoftware.graphql.builder;

import com.phocassoftware.graphql.builder.exceptions.DuplicateMethodNameException;
import graphql.GraphQL;
import graphql.introspection.IntrospectionWithDirectivesSupport;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class DuplicateMethodNameTest {

@Test
void testFailsToBuildSchema() {
assertThrows(
DuplicateMethodNameException.class,
() -> GraphQL
.newGraphQL(
new IntrospectionWithDirectivesSupport().apply(SchemaBuilder.build("com.phocassoftware.graphql.builder.duplicateMethodNames.invalid"))
)
.build()
);
}

@Test
void testSucceedsWithOverloadsAnnotatedWithIgnore() {
var schema = GraphQL
.newGraphQL(new IntrospectionWithDirectivesSupport().apply(SchemaBuilder.build("com.phocassoftware.graphql.builder.duplicateMethodNames.valid")))
.build();
assertNotNull(schema);
}

}
Original file line number Diff line number Diff line change
@@ -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.phocassoftware.graphql.builder.duplicateMethodNames.invalid;

import com.phocassoftware.graphql.builder.annotations.Query;

public class House {

@Query
public static House getHouse() {
return new House();
}

public String getStreetAddress() {
return "House Name";
}

public String getStreetAddress(String append) {
return getStreetAddress() + append;
}
}
Original file line number Diff line number Diff line change
@@ -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.phocassoftware.graphql.builder.duplicateMethodNames.valid;

import com.phocassoftware.graphql.builder.annotations.GraphQLIgnore;
import com.phocassoftware.graphql.builder.annotations.Query;

public class Hotel {

@Query
public static Hotel getHotel() {
return new Hotel();
}

@GraphQLIgnore
public String getStreetAddress() {
return "House Name";
}

public String getStreetAddress(String append) {
return getStreetAddress() + append;
}
}
Loading