Skip to content

feat(respect-graphqlname-input): GraphQLName Annotation for Input… #1949

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

Closed
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 @@ -20,4 +20,10 @@ package com.expediagroup.graphql.generator.annotations
* Set the GraphQL name to be picked up by the schema generator.
*/
@Target(AnnotationTarget.CLASS, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION)
annotation class GraphQLName(val value: String)
annotation class GraphQLName(val value: String, val target: GraphQLNameTarget = GraphQLNameTarget.BOTH)

enum class GraphQLNameTarget {
INPUT,
OUTPUT,
BOTH
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2020 Expedia, Inc
*
* 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
*
* https://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.expediagroup.graphql.generator.exceptions

/**
* This exception is thrown when there is an invalid GraphQL type in the schema being generated.
*
* This can occur when a class is marked as an input or output type but does not meet the necessary conditions.
* For example, a class marked as an input type that is not an input class, or a class marked as an output type
* that is not an output class.
*
* @property message the detail message string of this throwable.
* @constructor Constructs a new InvalidGraphQLTypeException with the specified detail message.
*/

class InvalidGraphQLTypeException(message: String) : GraphQLKotlinException(message)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
import com.expediagroup.graphql.generator.annotations.GraphQLName
import com.expediagroup.graphql.generator.annotations.GraphQLNameTarget
import com.expediagroup.graphql.generator.annotations.GraphQLType
import com.expediagroup.graphql.generator.annotations.GraphQLUnion
import kotlin.reflect.KAnnotatedElement
Expand All @@ -29,6 +30,7 @@ import kotlin.reflect.full.findAnnotation
internal fun KAnnotatedElement.getGraphQLDescription(): String? = this.findAnnotation<GraphQLDescription>()?.value

internal fun KAnnotatedElement.getGraphQLName(): String? = this.findAnnotation<GraphQLName>()?.value
internal fun KAnnotatedElement.getGraphQLNameTarget(): GraphQLNameTarget? = this.findAnnotation<GraphQLName>()?.target

internal fun KAnnotatedElement.getDeprecationReason(): String? =
this.findAnnotation<Deprecated>()?.getReason()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package com.expediagroup.graphql.generator.internal.extensions

import com.expediagroup.graphql.generator.annotations.GraphQLNameTarget
import com.expediagroup.graphql.generator.exceptions.CouldNotGetNameOfKClassException
import com.expediagroup.graphql.generator.exceptions.InvalidGraphQLTypeException
import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
import com.expediagroup.graphql.generator.internal.filters.functionFilters
import com.expediagroup.graphql.generator.internal.filters.propertyFilters
Expand Down Expand Up @@ -64,8 +66,7 @@ internal fun KClass<*>.isUnion(fieldAnnotations: List<Annotation> = emptyList())

private fun KClass<*>.isDeclaredUnion() = this.isInterface() && this.declaredMemberProperties.isEmpty() && this.declaredMemberFunctions.isEmpty()

internal fun KClass<*>.isAnnotationUnion(fieldAnnotations: List<Annotation>): Boolean = (this.isInstance(Any::class) || this.isAnnotation()) &&
fieldAnnotations.getUnionAnnotation() != null
internal fun KClass<*>.isAnnotationUnion(fieldAnnotations: List<Annotation>): Boolean = (this.isInstance(Any::class) || this.isAnnotation()) && fieldAnnotations.getUnionAnnotation() != null

internal fun KClass<*>.isAnnotation(): Boolean = this.isSubclassOf(Annotation::class)

Expand All @@ -84,16 +85,42 @@ internal fun KClass<*>.isListType(isDirective: Boolean = false): Boolean = this.

@Throws(CouldNotGetNameOfKClassException::class)
internal fun KClass<*>.getSimpleName(isInputClass: Boolean = false): String {
val name = this.getGraphQLName()
?: this.simpleName
?: throw CouldNotGetNameOfKClassException(this)

return when {
isInputClass -> if (name.endsWith(INPUT_SUFFIX, true)) name else "$name$INPUT_SUFFIX"
else -> name
val name = this.getGraphQLName() ?: this.simpleName ?: throw CouldNotGetNameOfKClassException(this)
val target = this.getGraphQLNameTarget()

when (target) {
GraphQLNameTarget.INPUT -> {
if (isInputClass) {
return name
} else {
throw InvalidGraphQLTypeException("Class $name is marked as an input type but is not an input class and suggest to change it to target INPUT/ BOTH")
}
}

GraphQLNameTarget.OUTPUT -> {
if (!isInputClass) {
return name
} else {
throw InvalidGraphQLTypeException("Class $name is marked as an output type but is not an output class and suggest to change it to target OUTPUT/ BOTH")
}
}

GraphQLNameTarget.BOTH -> {
if (isInputClass) {
return if (name.endsWith(INPUT_SUFFIX, true)) name else "$name$INPUT_SUFFIX"
} else {
return name
}
}

else -> return if (isInputClass) {
if (name.endsWith(INPUT_SUFFIX, true)) name else "$name$INPUT_SUFFIX"
} else {
name
}
}
}

internal fun KClass<*>.getQualifiedName(): String = this.qualifiedName.orEmpty()

internal fun KClass<*>.isPublic(): Boolean = this.visibility == KVisibility.PUBLIC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ package com.expediagroup.graphql.generator.internal.extensions

import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
import com.expediagroup.graphql.generator.annotations.GraphQLName
import com.expediagroup.graphql.generator.annotations.GraphQLNameTarget
import com.expediagroup.graphql.generator.annotations.GraphQLUnion
import com.expediagroup.graphql.generator.exceptions.CouldNotGetNameOfKClassException
import com.expediagroup.graphql.generator.exceptions.InvalidGraphQLTypeException
import com.expediagroup.graphql.generator.hooks.NoopSchemaGeneratorHooks
import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KProperty
Expand Down Expand Up @@ -70,6 +73,18 @@ open class KClassExtensionsTest {
@GraphQLName("MyTestClassRenamed")
private class MyTestClassCustomName

@GraphQLName("MyInputTestClass", target = GraphQLNameTarget.INPUT)
private class MyInputTestClassCustomName

@GraphQLName("MyOutputTestClass", target = GraphQLNameTarget.OUTPUT)
private class MyOutputTestClassCustomName

@GraphQLName("MyOutputTestClass", target = GraphQLNameTarget.INPUT)
private class MyOutputTestClassWithInputTarget

@GraphQLName("MyInputTestClass", target = GraphQLNameTarget.OUTPUT)
private class MyInputTestClassWithOutputTarget

internal class MyInternalClass

class MyClassInput
Expand Down Expand Up @@ -378,4 +393,28 @@ open class KClassExtensionsTest {
assertFalse(IgnoredClass::class.isValidAdditionalType(true))
assertFalse(IgnoredClass::class.isValidAdditionalType(false))
}

@Test
fun `test class name with GraphQLName And GraphQLNameTarget INPUT`() {
assertEquals("MyInputTestClass", MyInputTestClassCustomName::class.getSimpleName(true))
}

@Test
fun `test class name with GraphQLName And GraphQLNameTarget OUTPUT`() {
assertEquals("MyOutputTestClass", MyOutputTestClassCustomName::class.getSimpleName())
}

@Test
fun `test Input class name with GraphQLName And GraphQLNameTarget OUTPUT expect InvalidGraphQLTypeException`() {
assertThrows<InvalidGraphQLTypeException> {
MyInputTestClassWithOutputTarget::class.getSimpleName(true)
}
}

@Test
fun `test Output class name with GraphQLName And GraphQLNameTarget INPUT expect InvalidGraphQLTypeException`() {
assertThrows<InvalidGraphQLTypeException> {
MyOutputTestClassWithInputTarget::class.getSimpleName()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.expediagroup.graphql.generator.internal.types

import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import com.expediagroup.graphql.generator.annotations.GraphQLName
import com.expediagroup.graphql.generator.annotations.GraphQLNameTarget
import com.expediagroup.graphql.generator.annotations.GraphQLValidObjectLocations
import com.expediagroup.graphql.generator.exceptions.InvalidGraphQLNameException
import com.expediagroup.graphql.generator.exceptions.InvalidObjectLocationException
Expand All @@ -40,7 +41,14 @@ class GenerateInputObjectTest : TypeTestHelper() {

@Suppress("Detekt.UnusedPrivateClass")
@GraphQLName("InputClassRenamed")
class InputClassCustomName {
class CustomClassName {
@GraphQLName("myFieldRenamed")
val myField: String = "car"
}

@Suppress("Detekt.UnusedPrivateClass")
@GraphQLName("InputClassRenamed", target = GraphQLNameTarget.INPUT)
class InputCustomClassName {
@GraphQLName("myFieldRenamed")
val myField: String = "car"
}
Expand All @@ -67,13 +75,19 @@ class GenerateInputObjectTest : TypeTestHelper() {

@Test
fun `Test custom naming on classes`() {
val result = generateInputObject(generator, InputClassCustomName::class)
val result = generateInputObject(generator, CustomClassName::class)
assertEquals("InputClassRenamedInput", result.name)
}

@Test
fun `Test custom naming on input classes`() {
val result = generateInputObject(generator, InputCustomClassName::class)
assertEquals("InputClassRenamed", result.name)
}

@Test
fun `Test custom naming on arguments`() {
val result = generateInputObject(generator, InputClassCustomName::class)
val result = generateInputObject(generator, CustomClassName::class)
assertEquals(expected = 1, actual = result.fields.size)
assertEquals("myFieldRenamed", result.fields.first().name)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
id: renaming-classes
title: Renaming Classes
---
The schema generator uses the simple name of the class for type names and function/property names for fields by default. You can override this behavior using the `@GraphQLName` annotation with a `target` value. The `GraphQLNameTarget` accepts three values:

- `GraphQLNameTarget.INPUT`
- `GraphQLNameTarget.OUTPUT`
- `GraphQLNameTarget.BOTH` (default)

Where `GraphQLNameTarget.BOTH` is the default value.

Here's an example of renaming a Kotlin `Widget` class to `MyCustomName` GraphQL type:

```kotlin
@GraphQLName("MyCustomName", target=GraphQLNameTarget.INPUT)
data class Widget(
@GraphQLName("myCustomField")
val value: Int?
)
```

```graphql
input MyCustomName {
myCustomField: Int
}
```

### Exceptions
You might encounter the following exceptions while renaming classes:
1. `InvalidGraphQLTypeException`: This occurs when the target is not valid for the class. For example, defining an `INPUT` target with output classes or an `OUTPUT` target with input classes.
2. `CouldNotGetNameOfKClassException`: This is thrown when neither the `GraphQL name` of the class nor the `simple name` of the class is available.

### Implementation Details
1. The function retrieves the GraphQL name of the class if it exists, otherwise, it uses the simple name of the class. If neither is available, it throws a `CouldNotGetNameOfKClassException`.

2. Depending on the `GraphQLNameTarget` of the class, the function behaves differently:
* `INPUT`: Returns the name if the class is an input class, otherwise throws an `InvalidGraphQLTypeException`.
* `OUTPUT`: Returns the name if the class is not an input class, otherwise throws an `InvalidGraphQLTypeException`.
* `BOTH` or not specified: Returns the name as is for output classes. For input classes, it appends the `Input` suffix to the name if it doesn't already have it.

## Note
Previously, clients couldn't override the names of `Input` classes and the `Input` suffix was forcibly appended to the class name. Now, with the `@GraphQLName` annotation, clients can override the names of Input classes and append the `Input` suffix to the class name if it doesn't already have it.