Skip to content

Commit 5ed0498

Browse files
authored
Custom types with annotation (ExpediaGroup#1298)
### 📝 Description This adds a new annotation `@GraphQLType` which you can use to specify the return type of any field to be different than the Kotlin type. It explicitly takes a string for the type name and not just another `KClass` reference, because what we can do is return the field type as a type reference, but then allow devs to specify custom additional types that they have built up with their own GraphQLType builders. The prime use case for this is what we were trying to accomplish with `@GraphQLUnion`. In the scenario where you want to define a new union that is many different Kotlin classes but maybe those come from other libraries and you can't modify the class to have it implement the empty interface, this can solve that solution. But now you can also specify directives, descriptions, and deprecations on the type. Plus this solution scales beyond just unions and can work for any custom type that you can define in Kotlin code. If we feel like this is a decent approach, I would even consider deprecating the `@GraphQLUnion` annotation in favor of this generic approach that anyone can use and even extend now that we have added the ability to pass in `GraphQLType`s in the config. ### 🔗 Related Issues
1 parent d63c250 commit 5ed0498

File tree

26 files changed

+15033
-4118
lines changed

26 files changed

+15033
-4118
lines changed

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/SchemaGenerator.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.expediagroup.graphql.generator.exceptions.InvalidPackagesException
2020
import com.expediagroup.graphql.generator.internal.state.AdditionalType
2121
import com.expediagroup.graphql.generator.internal.state.ClassScanner
2222
import com.expediagroup.graphql.generator.internal.state.TypesCache
23+
import com.expediagroup.graphql.generator.internal.types.GraphQLKTypeMetadata
2324
import com.expediagroup.graphql.generator.internal.types.generateGraphQLType
2425
import com.expediagroup.graphql.generator.internal.types.generateMutations
2526
import com.expediagroup.graphql.generator.internal.types.generateQueries
@@ -109,18 +110,18 @@ open class SchemaGenerator(internal val config: SchemaGeneratorConfig) : Closeab
109110
* Generate the GraphQL type for all the `additionalTypes`.
110111
*
111112
* If you need to provide more custom additional types that were not picked up from reflection of the schema objects,
112-
* you can provide more types to be added through [generateSchema].
113+
* you can provide more types to be added through [generateSchema] or the config.
113114
*
114115
* This function loops because while generating the additionalTypes it is possible to create more additional types that need to be processed.
115116
*/
116117
protected fun generateAdditionalTypes(): Set<GraphQLType> {
117-
val graphqlTypes = mutableSetOf<GraphQLType>()
118+
val graphqlTypes = this.config.additionalTypes.toMutableSet()
118119
while (this.additionalTypes.isNotEmpty()) {
119120
val currentlyProcessedTypes = LinkedHashSet(this.additionalTypes)
120121
this.additionalTypes.clear()
121122
graphqlTypes.addAll(
122123
currentlyProcessedTypes.map {
123-
GraphQLTypeUtil.unwrapNonNull(generateGraphQLType(this, it.kType, it.inputType))
124+
GraphQLTypeUtil.unwrapNonNull(generateGraphQLType(this, it.kType, GraphQLKTypeMetadata(inputType = it.inputType)))
124125
}
125126
)
126127
}

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/SchemaGeneratorConfig.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.expediagroup.graphql.generator.execution.KotlinDataFetcherFactoryProv
2020
import com.expediagroup.graphql.generator.execution.SimpleKotlinDataFetcherFactoryProvider
2121
import com.expediagroup.graphql.generator.hooks.NoopSchemaGeneratorHooks
2222
import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
23+
import graphql.schema.GraphQLType
2324

2425
/**
2526
* Settings for generating the schema.
@@ -29,5 +30,6 @@ open class SchemaGeneratorConfig(
2930
open val topLevelNames: TopLevelNames = TopLevelNames(),
3031
open val hooks: SchemaGeneratorHooks = NoopSchemaGeneratorHooks,
3132
open val dataFetcherFactoryProvider: KotlinDataFetcherFactoryProvider = SimpleKotlinDataFetcherFactoryProvider(),
32-
open val introspectionEnabled: Boolean = true
33+
open val introspectionEnabled: Boolean = true,
34+
open val additionalTypes: Set<GraphQLType> = emptySet()
3335
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2021 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.generator.annotations
18+
19+
/**
20+
* Can be used on any field to set the return type.
21+
* The type name must match exactly to some additional type that was provided to the schema
22+
* generator, or it will fail on building the schema.
23+
*
24+
* Internally, the generator will check for this anntation first and just return a type reference with this name
25+
* instead of running reflection on the Kotlin code. This means that you can use this annotation with any Kotlin return type.
26+
* That does mean you could have runtime exceptions if the model you actually return doesn't match the GraphQL schema type.
27+
*/
28+
annotation class GraphQLType(val typeName: String)

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/extensions/annotationExtensions.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.expediagroup.graphql.generator.internal.extensions
1919
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
2020
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
2121
import com.expediagroup.graphql.generator.annotations.GraphQLName
22+
import com.expediagroup.graphql.generator.annotations.GraphQLType
2223
import com.expediagroup.graphql.generator.annotations.GraphQLUnion
2324
import kotlin.reflect.KAnnotatedElement
2425
import kotlin.reflect.full.findAnnotation
@@ -33,6 +34,8 @@ internal fun KAnnotatedElement.isGraphQLIgnored(): Boolean = this.findAnnotation
3334

3435
internal fun List<Annotation>.getUnionAnnotation(): GraphQLUnion? = this.filterIsInstance(GraphQLUnion::class.java).firstOrNull()
3536

37+
internal fun List<Annotation>.getCustomTypeAnnotation(): GraphQLType? = this.filterIsInstance(GraphQLType::class.java).firstOrNull()
38+
3639
internal fun Deprecated.getReason(): String {
3740
val builder = StringBuilder()
3841
builder.append(this.message)

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/state/TypesCache.kt

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ import com.expediagroup.graphql.generator.annotations.GraphQLUnion
2020
import com.expediagroup.graphql.generator.exceptions.ConflictingTypesException
2121
import com.expediagroup.graphql.generator.exceptions.InvalidCustomUnionException
2222
import com.expediagroup.graphql.generator.exceptions.TypeNotSupportedException
23+
import com.expediagroup.graphql.generator.internal.extensions.getCustomTypeAnnotation
2324
import com.expediagroup.graphql.generator.internal.extensions.getKClass
2425
import com.expediagroup.graphql.generator.internal.extensions.getSimpleName
2526
import com.expediagroup.graphql.generator.internal.extensions.getUnionAnnotation
2627
import com.expediagroup.graphql.generator.internal.extensions.isAnnotationUnion
2728
import com.expediagroup.graphql.generator.internal.extensions.isListType
2829
import com.expediagroup.graphql.generator.internal.extensions.qualifiedName
30+
import com.expediagroup.graphql.generator.internal.types.GraphQLKTypeMetadata
2931
import graphql.schema.GraphQLNamedType
3032
import graphql.schema.GraphQLType
3133
import graphql.schema.GraphQLTypeReference
@@ -40,8 +42,8 @@ internal class TypesCache(private val supportedPackages: List<String>) : Closeab
4042
private val cache: MutableMap<String, KGraphQLType> = mutableMapOf()
4143
private val typesUnderConstruction: MutableSet<TypesCacheKey> = mutableSetOf()
4244

43-
internal fun get(type: KType, inputType: Boolean, annotations: List<Annotation>): GraphQLNamedType? {
44-
val cacheKey = generateCacheKey(type, inputType, annotations)
45+
internal fun get(type: KType, typeInfo: GraphQLKTypeMetadata): GraphQLNamedType? {
46+
val cacheKey = generateCacheKey(type, typeInfo)
4547
return get(cacheKey)
4648
}
4749

@@ -73,22 +75,26 @@ internal class TypesCache(private val supportedPackages: List<String>) : Closeab
7375
return null
7476
}
7577

76-
private fun generateCacheKey(type: KType, inputType: Boolean, annotations: List<Annotation> = emptyList()): TypesCacheKey {
78+
private fun generateCacheKey(type: KType, typeInfo: GraphQLKTypeMetadata): TypesCacheKey {
7779
if (type.getKClass().isListType()) {
78-
return TypesCacheKey(type, inputType)
80+
return TypesCacheKey(type, typeInfo.inputType)
7981
}
8082

81-
val unionAnnotation = annotations.getUnionAnnotation()
83+
val customTypeAnnotation = typeInfo.fieldAnnotations.getCustomTypeAnnotation()
84+
if (customTypeAnnotation != null) {
85+
return TypesCacheKey(type, typeInfo.inputType, customTypeAnnotation.typeName)
86+
}
8287

83-
return if (unionAnnotation != null) {
84-
if (type.getKClass().isAnnotationUnion(annotations)) {
85-
TypesCacheKey(type = type, inputType = inputType, name = getCustomUnionNameKey(unionAnnotation))
88+
val unionAnnotation = typeInfo.fieldAnnotations.getUnionAnnotation()
89+
if (unionAnnotation != null) {
90+
if (type.getKClass().isAnnotationUnion(typeInfo.fieldAnnotations)) {
91+
return TypesCacheKey(type, typeInfo.inputType, getCustomUnionNameKey(unionAnnotation))
8692
} else {
8793
throw InvalidCustomUnionException(type)
8894
}
89-
} else {
90-
TypesCacheKey(type = type, inputType = inputType)
9195
}
96+
97+
return TypesCacheKey(type, typeInfo.inputType)
9298
}
9399

94100
private fun getCustomUnionNameKey(union: GraphQLUnion): String {
@@ -128,16 +134,16 @@ internal class TypesCache(private val supportedPackages: List<String>) : Closeab
128134

129135
private fun isTypeNotSupported(type: KType): Boolean = supportedPackages.none { type.qualifiedName.startsWith(it) }
130136

131-
internal fun buildIfNotUnderConstruction(kClass: KClass<*>, inputType: Boolean, annotations: List<Annotation>, build: (KClass<*>) -> GraphQLType): GraphQLType {
137+
internal fun buildIfNotUnderConstruction(kClass: KClass<*>, typeInfo: GraphQLKTypeMetadata, build: (KClass<*>) -> GraphQLType): GraphQLType {
132138
if (kClass.isListType()) {
133139
return build(kClass)
134140
}
135141

136-
val cacheKey = generateCacheKey(kClass.starProjectedType, inputType, annotations)
142+
val cacheKey = generateCacheKey(kClass.starProjectedType, typeInfo)
137143
val cachedType = get(cacheKey)
138144
return when {
139145
cachedType != null -> cachedType
140-
typesUnderConstruction.contains(cacheKey) -> GraphQLTypeReference.typeRef(kClass.getSimpleName(inputType))
146+
typesUnderConstruction.contains(cacheKey) -> GraphQLTypeReference.typeRef(kClass.getSimpleName(typeInfo.inputType))
141147
else -> {
142148
typesUnderConstruction.add(cacheKey)
143149
val newType = build(kClass)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2021 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.generator.internal.types
18+
19+
/**
20+
* Internal metadata class we can use to forward info about the type we are generating.
21+
* If there is no metadata to add, create the class with default values.
22+
*/
23+
internal data class GraphQLKTypeMetadata(
24+
val inputType: Boolean = false,
25+
val fieldName: String? = null,
26+
val fieldAnnotations: List<Annotation> = emptyList()
27+
)

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateArgument.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ internal fun generateArgument(generator: SchemaGenerator, parameter: KParameter)
4747
throw InvalidInputFieldTypeException(parameter)
4848
}
4949

50-
val graphQLType = generateGraphQLType(generator = generator, type = unwrappedType, inputType = true)
50+
val typeInfo = GraphQLKTypeMetadata(inputType = true, fieldName = parameter.getName(), fieldAnnotations = parameter.annotations)
51+
val graphQLType = generateGraphQLType(generator = generator, type = unwrappedType, typeInfo)
5152

5253
// Deprecation of arguments is currently unsupported: https://github.com/facebook/graphql/issues/197
5354
val builder = GraphQLArgument.newArgument()

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateFunction.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ internal fun generateFunction(generator: SchemaGenerator, fn: KFunction<*>, pare
5151

5252
val typeFromHooks = generator.config.hooks.willResolveMonad(fn.returnType)
5353
val returnType = getWrappedReturnType(typeFromHooks)
54-
val graphQLOutputType = generateGraphQLType(generator = generator, type = returnType, annotations = fn.annotations).safeCast<GraphQLOutputType>()
54+
val typeInfo = GraphQLKTypeMetadata(fieldName = functionName, fieldAnnotations = fn.annotations)
55+
val graphQLOutputType = generateGraphQLType(generator = generator, type = returnType, typeInfo).safeCast<GraphQLOutputType>()
5556
val graphQLType = builder.type(graphQLOutputType).build()
5657
val coordinates = FieldCoordinates.coordinates(parentName, functionName)
5758

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateGraphQLType.kt

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.expediagroup.graphql.generator.internal.types
1818

1919
import com.expediagroup.graphql.generator.SchemaGenerator
2020
import com.expediagroup.graphql.generator.extensions.unwrapType
21+
import com.expediagroup.graphql.generator.internal.extensions.getCustomTypeAnnotation
2122
import com.expediagroup.graphql.generator.internal.extensions.getKClass
2223
import com.expediagroup.graphql.generator.internal.extensions.getUnionAnnotation
2324
import com.expediagroup.graphql.generator.internal.extensions.isEnum
@@ -33,11 +34,11 @@ import kotlin.reflect.KType
3334
/**
3435
* Return a basic GraphQL type given all the information about the kotlin type.
3536
*/
36-
internal fun generateGraphQLType(generator: SchemaGenerator, type: KType, inputType: Boolean = false, annotations: List<Annotation> = emptyList()): GraphQLType {
37+
internal fun generateGraphQLType(generator: SchemaGenerator, type: KType, typeInfo: GraphQLKTypeMetadata = GraphQLKTypeMetadata()): GraphQLType {
3738
val hookGraphQLType = generator.config.hooks.willGenerateGraphQLType(type)
3839
val graphQLType = hookGraphQLType
3940
?: generateScalar(generator, type)
40-
?: objectFromReflection(generator, type, inputType, annotations)
41+
?: objectFromReflection(generator, type, typeInfo)
4142

4243
// Do not call the hook on GraphQLTypeReference as we have not generated the type yet
4344
val unwrappedType = graphQLType.unwrapType()
@@ -49,26 +50,38 @@ internal fun generateGraphQLType(generator: SchemaGenerator, type: KType, inputT
4950
return typeWithNullability
5051
}
5152

52-
private fun objectFromReflection(generator: SchemaGenerator, type: KType, inputType: Boolean, annotations: List<Annotation> = emptyList()): GraphQLType {
53-
val cachedType = generator.cache.get(type, inputType, annotations)
53+
private fun objectFromReflection(generator: SchemaGenerator, type: KType, typeInfo: GraphQLKTypeMetadata): GraphQLType {
54+
val cachedType = generator.cache.get(type, typeInfo)
5455

5556
if (cachedType != null) {
5657
return cachedType
5758
}
5859

5960
val kClass = type.getKClass()
6061

61-
return generator.cache.buildIfNotUnderConstruction(kClass, inputType, annotations) {
62-
val graphQLType = getGraphQLType(generator, kClass, inputType, type, annotations)
62+
return generator.cache.buildIfNotUnderConstruction(kClass, typeInfo) {
63+
val graphQLType = getGraphQLType(generator, kClass, type, typeInfo)
6364
generator.config.hooks.willAddGraphQLTypeToSchema(type, graphQLType)
6465
}
6566
}
6667

67-
private fun getGraphQLType(generator: SchemaGenerator, kClass: KClass<*>, inputType: Boolean, type: KType, fieldAnnotations: List<Annotation> = emptyList()): GraphQLType = when {
68-
kClass.isEnum() -> @Suppress("UNCHECKED_CAST") (generateEnum(generator, kClass as KClass<Enum<*>>))
69-
kClass.isListType() -> generateList(generator, type, inputType, fieldAnnotations)
70-
kClass.isUnion(fieldAnnotations) -> generateUnion(generator, kClass, fieldAnnotations.getUnionAnnotation())
71-
kClass.isInterface() -> generateInterface(generator, kClass)
72-
inputType -> generateInputObject(generator, kClass)
73-
else -> generateObject(generator, kClass)
68+
private fun getGraphQLType(
69+
generator: SchemaGenerator,
70+
kClass: KClass<*>,
71+
type: KType,
72+
typeInfo: GraphQLKTypeMetadata
73+
): GraphQLType {
74+
val customTypeAnnotation = typeInfo.fieldAnnotations.getCustomTypeAnnotation()
75+
if (customTypeAnnotation != null) {
76+
return GraphQLTypeReference.typeRef(customTypeAnnotation.typeName)
77+
}
78+
79+
return when {
80+
kClass.isEnum() -> @Suppress("UNCHECKED_CAST") (generateEnum(generator, kClass as KClass<Enum<*>>))
81+
kClass.isListType() -> generateList(generator, type, typeInfo)
82+
kClass.isUnion(typeInfo.fieldAnnotations) -> generateUnion(generator, kClass, typeInfo.fieldAnnotations.getUnionAnnotation())
83+
kClass.isInterface() -> generateInterface(generator, kClass)
84+
typeInfo.inputType -> generateInputObject(generator, kClass)
85+
else -> generateObject(generator, kClass)
86+
}
7487
}

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateInputProperty.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.expediagroup.graphql.generator.internal.types
1818

1919
import com.expediagroup.graphql.generator.SchemaGenerator
20+
import com.expediagroup.graphql.generator.internal.extensions.getPropertyAnnotations
2021
import com.expediagroup.graphql.generator.internal.extensions.getPropertyDescription
2122
import com.expediagroup.graphql.generator.internal.extensions.getPropertyName
2223
import com.expediagroup.graphql.generator.internal.extensions.safeCast
@@ -33,10 +34,12 @@ internal fun generateInputProperty(generator: SchemaGenerator, prop: KProperty<*
3334
// Verfiy that the unwrapped GraphQL type is a valid input type
3435
val inputTypeFromHooks = generator.config.hooks.willResolveInputMonad(prop.returnType)
3536
val unwrappedType = inputTypeFromHooks.unwrapOptionalInputType()
36-
val graphQLInputType = generateGraphQLType(generator = generator, type = unwrappedType, inputType = true).safeCast<GraphQLInputType>()
37+
val propertyName = prop.getPropertyName(parentClass)
38+
val typeInfo = GraphQLKTypeMetadata(inputType = true, fieldName = propertyName, fieldAnnotations = prop.getPropertyAnnotations(parentClass))
39+
val graphQLInputType = generateGraphQLType(generator = generator, type = unwrappedType, typeInfo).safeCast<GraphQLInputType>()
3740

41+
builder.name(propertyName)
3842
builder.description(prop.getPropertyDescription(parentClass))
39-
builder.name(prop.getPropertyName(parentClass))
4043
builder.type(graphQLInputType)
4144

4245
generateDirectives(generator, prop, DirectiveLocation.INPUT_FIELD_DEFINITION, parentClass).forEach {

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateList.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import com.expediagroup.graphql.generator.internal.extensions.getWrappedType
2121
import graphql.schema.GraphQLList
2222
import kotlin.reflect.KType
2323

24-
internal fun generateList(generator: SchemaGenerator, type: KType, inputType: Boolean, annotations: List<Annotation> = emptyList()): GraphQLList {
25-
val wrappedType = generateGraphQLType(generator, type.getWrappedType(), inputType, annotations)
24+
internal fun generateList(generator: SchemaGenerator, type: KType, typeInfo: GraphQLKTypeMetadata): GraphQLList {
25+
val wrappedType = generateGraphQLType(generator, type.getWrappedType(), typeInfo)
2626
return GraphQLList.list(wrappedType)
2727
}

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateProperty.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,9 @@ import kotlin.reflect.KProperty
3333

3434
internal fun generateProperty(generator: SchemaGenerator, prop: KProperty<*>, parentClass: KClass<*>): GraphQLFieldDefinition {
3535
val typeFromHooks = generator.config.hooks.willResolveMonad(prop.returnType)
36-
val propertyType = generateGraphQLType(generator, type = typeFromHooks, annotations = prop.getPropertyAnnotations(parentClass))
37-
.safeCast<GraphQLOutputType>()
38-
3936
val propertyName = prop.getPropertyName(parentClass)
37+
val typeInfo = GraphQLKTypeMetadata(fieldName = propertyName, fieldAnnotations = prop.getPropertyAnnotations(parentClass))
38+
val propertyType = generateGraphQLType(generator, type = typeFromHooks, typeInfo).safeCast<GraphQLOutputType>()
4039

4140
val fieldBuilder = GraphQLFieldDefinition.newFieldDefinition()
4241
.description(prop.getPropertyDescription(parentClass))

0 commit comments

Comments
 (0)