leangen / graphql-spqr

Build a GraphQL service in seconds
Apache License 2.0
1.09k stars 181 forks source link

Ignore Kotlin companion classes #390

Open dave-at-season opened 3 years ago

dave-at-season commented 3 years ago

Hello! Trying to retrofit this lib onto an existing Kotlin domain model. I'm running into trouble because its picking up the companion classes on the models and throwing the following error:

Caused by: graphql.schema.validation.InvalidSchemaException: invalid schema:
"Companion" must define one or more fields.

I tried to @GraphQLIgnore the companion object, but that didn't help any. Is there anyway to skip over Kotlin companion classes?

dave-at-season commented 3 years ago

I was able to workaround this by mapping types with name containing "$Companion" to Strings. Kinda janky but unblocks me for now. Any thoughts on a cleaner approach?

class CompanionTypeMapper : TypeMapper {
    override fun toGraphQLType(
        javaType: AnnotatedType?,
        mappersToSkip: MutableSet<Class<out TypeMapper>>?,
        env: TypeMappingEnvironment?
    ): GraphQLOutputType {
        val name = "_ignore_${javaType!!.type.typeName.split('.').last().split("$").joinToString("")}"
        // TODO: figure out what the non-deprecated way to do this is
        return GraphQLScalarType(
            name,
            "DELETE ME PLZ",
            GraphqlStringCoercing()
        )
    }

    override fun toGraphQLInputType(
        javaType: AnnotatedType?,
        mappersToSkip: MutableSet<Class<out TypeMapper>>?,
        env: TypeMappingEnvironment?
    ): GraphQLInputType {
        val name = "_ignore_${javaType!!.type.typeName.split('.').last().split("$").joinToString("")}"
        return GraphQLScalarType(
            name,
            "DELETE ME PLZ",
            GraphqlStringCoercing()
        )
    }

    override fun supports(element: AnnotatedElement?, type: AnnotatedType?): Boolean {
        return (type!!).type.typeName.contains("\$Companion")
    }
}

// ...

private val schema = GraphQLSchemaGenerator()
        .withOperationsFromSingleton(topLevelQueries)
        .withTypeMappers(CompanionTypeMapper())
        .withSchemaTransformers(schemaTransformerExtensionProvider())
        .generate()
kaqqao commented 3 years ago

As I noted in your other issue, I know very little about Kotlin, so I'm not sure I even know exactly what a companion object is 👀 But, you can exclude anything from anything by implementing a custom InclusionStrategy and registering it via generator.withInclusionStrategy. You could globally exclude fields/input fields/arguments that are declared in a companion object.

Is this something that is always needed in Kotlin or just in your specific case? If useful in general, it could/should be a part of the Kotlin compat module I mentioned in your other issue.

dave-at-season commented 3 years ago

Thanks. FWIW I'm actually pretty new to Kotlin (and Java) myself! A companion object is an object of which all instances of a class share the same instance. Comparable to making things static in Java. For my use case I don't need anything in companion objects to be represented in the schema, so I think an InclusionStrategy would work. I wasn't aware that was a thing!

Thanks, will report back

dave-at-season commented 3 years ago

This works!

class CompanionClassFilteringInclusionStrategy() : DefaultInclusionStrategy() {
    override fun includeOperation(elements: List<AnnotatedElement>, declaringType: AnnotatedType): Boolean {
        return if (isCompanionClass(elements)) {
            false
        } else {
            super.includeOperation(elements, declaringType)
        }
    }

    private fun isCompanionClass(elements: List<AnnotatedElement>): Boolean =
        (elements.firstOrNull { it is Field } as Field?)?.type?.typeName?.contains("\$Companion") ?: false
}

Thanks for your help