Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for auto resolving generic types #210

Open
IceBlizz6 opened this issue Feb 2, 2023 · 4 comments
Open

Support for auto resolving generic types #210

IceBlizz6 opened this issue Feb 2, 2023 · 4 comments

Comments

@IceBlizz6
Copy link

IceBlizz6 commented Feb 2, 2023

I want to suggest a possible way for the library to handle generic types without requiring behavior that is explicitly defined by the user.

I've been messing around a bit with the codebase and it seems like the problem is that the lookup in some caches and functions is KClass<*>.
KType.jvmErasure will erase the provided generic type.

We need to retain the information in the KType object.
So i tried to replace the lookup type with a combination of KType and KClass<*> and the result seems promising.
I think maybe just KType may be enough but my understanding of the codebase is still very limited.

I'm not entirely sure how this would be combined with the existing user provided generic type resolver.
I think we could run it through the provided type resolver first and then let the system process it after if the returned type still has atleast one generic type parameter.

Idea here is that we construct a new type for each combination of the provided generic type.
MyGenericType<MyUser> becomes MyGenericType_MyUser
MyGenericType<MyDirectory> becomes MyGenericType_MyDirectory

class MyGenericType<T>(
    val name: String,
    val value: T
)

class MyUser(
    val name: String,
    val id: Int
)

class MyDirectory(
    val name: String,
    val id: Int
)

private fun a(): MyGenericType<MyUser> {
    TODO()
}

private fun b(): MyGenericType<MyDirectory> {
    TODO()
}

private fun c(): MyGenericType<MyUser> {
    TODO()
}
@Test
fun `generic type handling`() {
    val testedSchema = defaultSchema {
        query("myTest1") {
            resolver<MyGenericType<MyUser>> {
                a()
            }
        }
        query("myTest2") {
            resolver<MyGenericType<MyDirectory>> {
                b()
            }
        }
        query("myTest3") {
            resolver<MyGenericType<MyUser>> {
                c()
            }
        }
    }
    val typeNames = testedSchema.model.types.map { it.name }
    typeNames.forEach { println(it) }
}

Ouput

__Schema
__Directive
__InputValue
__Type
__EnumValue
__Field
__Type
__Type
__Directive
MyGenericType_MyUser
MyUser
MyGenericType_MyDirectory
MyDirectory
__Schema
__TypeKind
__DirectiveLocation
String
Boolean
Float
Short
Int
Long
Query

I was considering a pull request myself but i think my code is just a bit too messy.

@tiagonuneslx
Copy link

tiagonuneslx commented Feb 2, 2023

I'm very much interested in this feature as well. I am currently studying the library codebase with the same exact purpose.

My main use case is I want a resolver to return a Connection of a generic type:

data class Connection<T>(
    val totalCount: Int,
    val edges: List<Edge<T>>,
    val pageInfo: PageInfo
) {
    data class Edge<T>(
        val node: T,
        val cursor: Int
    )

    data class PageInfo(
        val endCursor: Int,
        val hasNextPage: Boolean
    )
}

I really like the design you're proposing, as a starting point!

In this case, if I wanted a resolver to return a Connection<User>, two types would be added to the Schema:

  • Connection_User
  • Edge_User

And eventually, we could add support for multiple generics. Then we could specify the type of cursor with Connection<T, C>.
Then, we would have these types added to the Schema when returning a Connection<User, Int>

  • Connection_User_Int
  • Edge_User_Int
  • PageInfo_Int

I really, really hope we're able to achieve this 🙂

Edit: Of course the syntax would need to be revised if we wanted to support deeper level generics (e.g. Connection<Entity<User>, Int>>), but as a starting point it looks good to me.

tiagonuneslx added a commit to tiagonuneslx/KGraphQL that referenced this issue Feb 7, 2023
@tiagonuneslx
Copy link

tiagonuneslx commented Feb 7, 2023

Progress Update

The main issue is that getting the actual returnType of a KProperty with a generic returnType is not trivial.

e.g.

val kType = typeOf<Connection<Person<String>, Int>>()
println(kType) // Connection<Person<String>, Int> ✅
(kType.classifier as KClass<*>).memberProperties.forEach { property ->
    println(property.returnType) // List<Edge<T, K>> ❌ Should be List<Edge<String, Int>>
}

Unfortunately, kotlin reflection is still very limited, and this is something that's not yet supported.

I've been studying how other projects using reflection deal with this issue:

  • Jackson: uses TypeReference and TypeFactory
  • gson: uses TypeToken.getParameterized (based on Guava)
  • Moshi (reflection): Most functional approach: Util.resolve (based on gson and Guava)

These libraries, using java reflection and Super Type Tokens pattern, are able to resolve a ParameterizedType to a java.lang.reflect.Type with the actual type parameters.

I chose to experiment with the Moshi approach, copying everything that was necessary to resolve a property returnType.

Sadly, there is no way to resolve to a KType, only to a java.lang.reflect.Type. All these libraries I studied work exclusively with java.lang.reflect.Type under the hood.

This differs from the KGraphQL library, which associates definitions by KClass<*>, and relies on KType and KClass in many places to compile a schema. We'll never be able to work with generics while we associate by KClass<*>, because it doesn't hold information about type parameters. We also can't rely on KType, because for compiling properties with generic returnType, we don't have access to a KType with resolved type parameters (only to a java.lang.reflect.Type with resolved type parameters).

Conclusion: For this to work, internal changes, mainly to the compilation and definitions, need to be done to rely exclusively on java.lang.reflect.Type.

Note: I have tried very hard to find a way to get the properties returnTypes as KType with resolved type parameters, so that we could associate definitions by KType and keep compiling using KType and KClass<*>, but it's not possible (I'd love for someone to prove me wrong).

I am working on these changes and I've made a quick POF, pushed to this branch. It's still lacking a lot, but I'd say the results are already very promising:

class Connection<T : Any, K : Any>(
    val totalCount: Int,
    val edges: List<Edge<T, K>>,
    val pageInfo: PageInfo<K>
) {
    class Edge<T : Any, K: Any>(
        val node: T,
        val cursor: K
    )

    class PageInfo<K: Any>(
        val endCursor: K,
        val hasNextPage: Boolean
    )
}

val names = listOf("Kai", "Eliana", "Jayden", "Ezra", "Luca", "Rowan", "Nova", "Amara")

fun main() {
    val schema = KGraphQL.schema {
        configure {
            this.useDefaultPrettyPrinter = true
        }
        query("names") {
            resolver { ->
                Connection(
                    totalCount = names.size,
                    edges = names.subList(0, 2).mapIndexed { index, name ->
                        Connection.Edge(
                            node = name,
                            cursor = index
                        )
                    },
                    pageInfo = Connection.PageInfo(
                        endCursor = 1,
                        hasNextPage = true
                    )
                )
            }.returns<Connection<String, Int>>()
        }
    }

    schema.types.forEach { println(it.name) }
    println()
    println(schema.executeBlocking("{ names { totalCount, edges { node, cursor }, pageInfo { endCursor, hasNextPage } } }"))

Console:

__Schema
__Directive
__InputValue
__Type
__EnumValue
__Field
Connection<String, Integer>
Edge<String, Integer>
PageInfo<Integer>
__TypeKind
__DirectiveLocation
String
Boolean
Float
Short
Int
Long
Query

{
  "data" : {
    "names" : {
      "totalCount" : 8,
      "edges" : [ {
        "node" : "Kai",
        "cursor" : 0
      }, {
        "node" : "Eliana",
        "cursor" : 1
      } ],
      "pageInfo" : {
        "endCursor" : 1,
        "hasNextPage" : true
      }
    }
  }
}

tiagonuneslx added a commit to tiagonuneslx/KGraphQL that referenced this issue Feb 7, 2023
tiagonuneslx added a commit to tiagonuneslx/KGraphQL that referenced this issue Feb 7, 2023
@afgarcia86
Copy link

This would be great to see

@IceBlizz6
Copy link
Author

This would be great to see

I think i could finish this,
But the project looks pretty dead at the moment.
I've been waiting a long time for subscription support which i'm not sure is in development anymore.

If the future looked promising then i would love to assist more,
but as it stands i have started to look into alternatives like ExpediaGroup/graphql-kotlin.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants