How to compare GraphQL query tree in java

How to compare GraphQL query tree in java


3

My spring-boot application is generating GraphQL queries, however I want to compare that query in my test.

So basically I have two strings where the first one is containing the actual value and the latter one the expected value.
I want to parse that in a class or tree node so I can compare them if both of them are equal.
So even if the order of the fields are different, I need to know if it’s the same query.

So for example we have these two queries:
Actual:

query Query {
 car {
  brand
  color
  year
 }
 person {
  name
  age
 }
}

Expected

query Query {
 person {
  age
  name
 }
 car {
  brand
  color
  year
 }
}

I expect that these queries both are semantically the same.

I tried

Parser parser = new Parser();
Document expectedDocument = parser.parseDocument(expectedValue);
Document actualDocument = parser.parseDocument(actualValue);

if (expectedDocument.isEqualTo(actualDocument)) {
    return MatchResult.exactMatch();
}

But found out that it does nothing since the isEqualTo is doing this:

public boolean isEqualTo(Node o) {
    if (this == o) {
        return true;
    } else {
        return o != null && this.getClass() == o.getClass();
    }
}

I know with JSON I can use Jackson for this purpose and compare treenodes, or parsing it into a Java object and have my own equals() implementation, but I don’t know how to do that for GraphQL Java.

How can I parse my GraphQL query string into an object so that I can compare it?

2

  • What's the full name of Parser ?

    – OscarRyz

    May 11, 2022 at 14:34

  • @OscarRyz graphql-java

    – com2ghz

    May 11, 2022 at 14:39

3 Answers
3


2

I have recently solved this problem myself. You can reduce the query to a hash and compare the values. You can account for varied query order by utilizing a tree structure. You can take advantage of the QueryTraverser and QueryReducer to accomplish this.

First you can create the QueryTraverser, the exact method for creating this will depend on your execution point. Assuming you are doing it in the AsyncExecutor with access to the ExecutionContext the below code snippet will suffice. But you can do this in instrumentation or the data fetcher itself if you so choose;

val queryTraverser = QueryTraverser.newQueryTraverser()
            .schema(context.graphQLSchema)
            .document(context.document
            .operationName(context.operationDefinition.name)
            .variables(context.executionInput?.variables ?: emptyMap())
            .build()

Next you will need to provide an implementation of the reducer, and some accumulation object that can add each field to a tree structure. Here is a simplified version of an accumulation object

class MyAccumulation {

    /**
     * A sorted map of the field node of the query and its arguments
     */
    private val fieldPaths = TreeMap<String, String>()

    /**
     * Add a given field and arguments to the sorted map.
     */
    fun addFieldPath(path: String, arguments: String) {
        fields[path] = arguments
    }

    /**
     * Function to generate the query hash
     */
    fun toHash(): String {

        val joinedFields = fieldPaths.entries
            .joinToString("") { "${it.key}[${it.value}]" }

        return HashingLibrary.hashingfunction(joinedFields)
}

A sample reducer implementation would look like the below;

class MyReducer : QueryReducer<MyAccumulation> {

    override fun reduceField(
        fieldEnvironment: QueryVisitorFieldEnvironment,
        acc: MyAccumulation
    ): MyAccumulation {
        if (fieldEnvironment.isTypeNameIntrospectionField) {
            return acc
        }
        
        // Get your field path, this should account for
        // the same node with different parents, and you should recursively
        // traverse the parent environment to construct this
        val fieldPath = getFieldPath(fieldEnvironment)

        // Provide a reproduceable stringified arguments string
        val arguments = getArguments(fieldEnvironment.arguments)

        acc.addFieldPath(fieldPath, arguments)

        return acc
    }
}

Finally put it all together;

val queryHash = queryTraverser
    .reducePreOrder(MyReducer(), MyAccumulation())
    .toHash()

You can now generate a repdocueable hash for a query that does not care about the query structure, only the actual fields that were requested.

Note: These code snippets are in kotlin but are transposable to Java.

1

  • 1

    Nice solution. I made my own GraphQL compare library based on graphql-java components with a WireMock component. I just need to make a library of it so I can share it.

    – com2ghz

    Feb 20 at 7:13


0

Depending on how important is to perform this comparison you can inspect all the elements of Document to determine equality.

If this is to optimize and return the same result for the same input I would totally recommend just compare the strings and kept two entries (one for each string input).

If you really want to go for the deep compare route, you can check the selectionSet and compare each selection.

Take a look at the screenshot:

How to compare GraphQL query tree in java

You can also give EqualsBuilder.html.reflectionEquals(Object,Object) a try but it might inspect too deep (I tried and returned false)


0

It appears that graphql-java has classes AstSorter and AstComparator that can help with sorting and comparing, respectively.

import graphql.language.AstComparator
import graphql.language.AstSorter
import graphql.language.Document
import graphql.parser.Parser;


Parser parser = new Parser();
AstSorter astSorter = new AstSorter();
Document expectedDocument = astSorter.sort(parser.parseDocument(expectedValue));
Document actualDocument = astSorter.sort(parser.parseDocument(actualValue));

if (AstComparator.isEqual(expectedDocument, actualDocument)) {
    // Documents are equal irrespective to field order.
}



Leave a Reply

Your email address will not be published. Required fields are marked *