diff --git a/commons/build.gradle b/commons/build.gradle index fd698f75c..c293eaae4 100644 --- a/commons/build.gradle +++ b/commons/build.gradle @@ -31,6 +31,8 @@ dependencies { implementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}" implementation group: 'org.ballerinalang', name: 'ballerina-parser', version: "${ballerinaLangVersion}" implementation group: 'org.ballerinalang', name: 'ballerina-tools-api', version: "${ballerinaLangVersion}" + + testImplementation group: 'org.testng', name: 'testng', version: "${testngVersion}" } def excludePattern = '**/module-info.java' @@ -89,6 +91,14 @@ spotbugsTest { enabled = false } +test { + useTestNG() + testLogging { + events "passed", "skipped", "failed" + showStandardStreams = false + } +} + compileJava { doFirst { options.compilerArgs = [ diff --git a/commons/src/main/java/io/ballerina/stdlib/graphql/commons/utils/SdlSchemaStringGenerator.java b/commons/src/main/java/io/ballerina/stdlib/graphql/commons/utils/SdlSchemaStringGenerator.java index d30c741db..1e5b255f3 100644 --- a/commons/src/main/java/io/ballerina/stdlib/graphql/commons/utils/SdlSchemaStringGenerator.java +++ b/commons/src/main/java/io/ballerina/stdlib/graphql/commons/utils/SdlSchemaStringGenerator.java @@ -174,17 +174,61 @@ private boolean isFederatedDirective(Directive directive) { } private String getTypes() { - List types = new ArrayList<>(); + List typeList = new ArrayList<>(); for (Map.Entry entry : this.schema.getTypes().entrySet()) { if (!isIntrospectionType(entry.getValue()) && !isBuiltInScalarType(entry.getValue())) { if (!isSubgraphSdlIntrospection && isDefaultFederatedType(entry.getValue())) { continue; } - types.add(createType(entry.getValue())); + typeList.add(entry.getValue()); + } + } + + typeList.sort((t1, t2) -> { + int priority1 = getTypeSortPriority(t1); + int priority2 = getTypeSortPriority(t2); + if (priority1 != priority2) { + return Integer.compare(priority1, priority2); } + return t1.getName().compareTo(t2.getName()); + }); + + List types = new ArrayList<>(); + for (Type type : typeList) { + types.add(createType(type)); } return String.join(LINE_SEPARATOR + LINE_SEPARATOR, types); } + + private int getTypeSortPriority(Type type) { + if (this.schema.getQueryType() != null && type.getName().equals(this.schema.getQueryType().getName())) { + return 0; + } + if (this.schema.getMutationType() != null && type.getName().equals(this.schema.getMutationType().getName())) { + return 1; + } + if (this.schema.getSubscriptionType() != null && + type.getName().equals(this.schema.getSubscriptionType().getName())) { + return 2; + } + + switch (type.getKind()) { + case INTERFACE: + return 3; + case OBJECT: + return 4; + case INPUT_OBJECT: + return 5; + case ENUM: + return 6; + case UNION: + return 7; + case SCALAR: + return 8; + default: + return 9; + } + } private boolean isDefaultFederatedType(Type type) { if (!isSubgraph) { diff --git a/commons/src/test/java/io/ballerina/stdlib/graphql/commons/utils/SdlSchemaStringGeneratorTest.java b/commons/src/test/java/io/ballerina/stdlib/graphql/commons/utils/SdlSchemaStringGeneratorTest.java new file mode 100644 index 000000000..fb9ae72c3 --- /dev/null +++ b/commons/src/test/java/io/ballerina/stdlib/graphql/commons/utils/SdlSchemaStringGeneratorTest.java @@ -0,0 +1,150 @@ +package io.ballerina.stdlib.graphql.commons.utils; + +import io.ballerina.stdlib.graphql.commons.types.EnumValue; +import io.ballerina.stdlib.graphql.commons.types.Field; +import io.ballerina.stdlib.graphql.commons.types.InputValue; +import io.ballerina.stdlib.graphql.commons.types.Schema; +import io.ballerina.stdlib.graphql.commons.types.Type; +import io.ballerina.stdlib.graphql.commons.types.TypeKind; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SdlSchemaStringGeneratorTest { + + @Test + public void testSchemaTypeOrdering() { + Schema schema = new Schema(null, false); + + Type queryType = schema.addType("Query", TypeKind.OBJECT, null); + Type stringType = schema.addType("String", TypeKind.SCALAR, null); + queryType.addField(new Field("greeting", stringType)); + queryType.addField(new Field("user", null)); + schema.setQueryType(queryType); + + Type mutationType = schema.addType("Mutation", TypeKind.OBJECT, null); + mutationType.addField(new Field("createUser", null)); + schema.setMutationType(mutationType); + + Type subscriptionType = schema.addType("Subscription", TypeKind.OBJECT, null); + subscriptionType.addField(new Field("userUpdated", null)); + schema.setSubscriptionType(subscriptionType); + + Type profileInterface = schema.addType("Profile", TypeKind.INTERFACE, null); + profileInterface.addField(new Field("id", stringType)); + profileInterface.addField(new Field("name", stringType)); + + Type userType = schema.addType("User", TypeKind.OBJECT, null); + userType.addField(new Field("id", stringType)); + userType.addField(new Field("name", stringType)); + userType.addField(new Field("email", stringType)); + userType.addInterface(profileInterface); + + Type addressType = schema.addType("Address", TypeKind.OBJECT, null); + addressType.addField(new Field("street", stringType)); + addressType.addField(new Field("city", stringType)); + + Type createUserInput = schema.addType("CreateUserInput", TypeKind.INPUT_OBJECT, null); + createUserInput.addInputField(new InputValue("name", stringType, null, null)); + createUserInput.addInputField(new InputValue("email", stringType, null, null)); + + Type statusEnum = schema.addType("Status", TypeKind.ENUM, null); + statusEnum.addEnumValue(new EnumValue("ACTIVE", null)); + statusEnum.addEnumValue(new EnumValue("INACTIVE", null)); + + Type searchResultUnion = schema.addType("SearchResult", TypeKind.UNION, null); + searchResultUnion.addPossibleType(userType); + searchResultUnion.addPossibleType(addressType); + + Type dateTimeScalar = schema.addType("DateTime", TypeKind.SCALAR, null); + + String sdlSchema = SdlSchemaStringGenerator.generate(schema); + + int queryIndex = findTypeIndex(sdlSchema, "type Query"); + int mutationIndex = findTypeIndex(sdlSchema, "type Mutation"); + int subscriptionIndex = findTypeIndex(sdlSchema, "type Subscription"); + int interfaceIndex = findTypeIndex(sdlSchema, "interface Profile"); + int addressIndex = findTypeIndex(sdlSchema, "type Address"); + int userIndex = findTypeIndex(sdlSchema, "type User"); + int inputIndex = findTypeIndex(sdlSchema, "input CreateUserInput"); + int enumIndex = findTypeIndex(sdlSchema, "enum Status"); + int unionIndex = findTypeIndex(sdlSchema, "union SearchResult"); + int scalarIndex = findTypeIndex(sdlSchema, "scalar DateTime"); + + Assert.assertTrue(queryIndex != -1, "Query type should be present"); + Assert.assertTrue(mutationIndex != -1, "Mutation type should be present"); + Assert.assertTrue(subscriptionIndex != -1, "Subscription type should be present"); + Assert.assertTrue(interfaceIndex != -1, "Interface should be present"); + Assert.assertTrue(userIndex != -1, "Object types should be present"); + Assert.assertTrue(inputIndex != -1, "Input type should be present"); + Assert.assertTrue(enumIndex != -1, "Enum type should be present"); + Assert.assertTrue(unionIndex != -1, "Union type should be present"); + Assert.assertTrue(scalarIndex != -1, "Scalar type should be present"); + + Assert.assertTrue(queryIndex < mutationIndex, "Query should come before Mutation"); + Assert.assertTrue(mutationIndex < subscriptionIndex, "Mutation should come before Subscription"); + Assert.assertTrue(subscriptionIndex < interfaceIndex, "Subscription should come before Interface"); + Assert.assertTrue(interfaceIndex < addressIndex, "Interface should come before Object types"); + Assert.assertTrue(interfaceIndex < userIndex, "Interface should come before Object types"); + Assert.assertTrue(addressIndex < inputIndex, "Object types should come before Input"); + Assert.assertTrue(userIndex < inputIndex, "Object types should come before Input"); + Assert.assertTrue(inputIndex < enumIndex, "Input should come before Enum"); + Assert.assertTrue(enumIndex < unionIndex, "Enum should come before Union"); + Assert.assertTrue(unionIndex < scalarIndex, "Union should come before Scalar"); + } + + @Test + public void testAlphabeticalOrderingWithinSameTypeKind() { + Schema schema = new Schema(null, false); + + Type queryType = schema.addType("Query", TypeKind.OBJECT, null); + Type stringType = schema.addType("String", TypeKind.SCALAR, null); + queryType.addField(new Field("test", stringType)); + schema.setQueryType(queryType); + + Type zebraType = schema.addType("Zebra", TypeKind.OBJECT, null); + zebraType.addField(new Field("name", stringType)); + + Type appleType = schema.addType("Apple", TypeKind.OBJECT, null); + appleType.addField(new Field("color", stringType)); + + Type mangoType = schema.addType("Mango", TypeKind.OBJECT, null); + mangoType.addField(new Field("taste", stringType)); + + String sdlSchema = SdlSchemaStringGenerator.generate(schema); + + int appleIndex = findTypeIndex(sdlSchema, "type Apple"); + int mangoIndex = findTypeIndex(sdlSchema, "type Mango"); + int zebraIndex = findTypeIndex(sdlSchema, "type Zebra"); + + Assert.assertTrue(appleIndex < mangoIndex, "Apple should come before Mango (alphabetical order)"); + Assert.assertTrue(mangoIndex < zebraIndex, "Mango should come before Zebra (alphabetical order)"); + } + + @Test + public void testMinimalSchemaWithOnlyQuery() { + Schema schema = new Schema(null, false); + + Type queryType = schema.addType("Query", TypeKind.OBJECT, null); + Type stringType = schema.addType("String", TypeKind.SCALAR, null); + queryType.addField(new Field("hello", stringType)); + schema.setQueryType(queryType); + + String sdlSchema = SdlSchemaStringGenerator.generate(schema); + + Assert.assertTrue(sdlSchema.contains("type Query"), "Schema should contain Query type"); + Assert.assertTrue(sdlSchema.contains("hello"), "Query should have hello field"); + } + + private int findTypeIndex(String schema, String typePattern) { + Pattern pattern = Pattern.compile("^" + Pattern.quote(typePattern), Pattern.MULTILINE); + Matcher matcher = pattern.matcher(schema); + if (matcher.find()) { + return matcher.start(); + } + return -1; + } +} +