All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jvmMain.graphql.GraphSystemBuilder.kt Maven / Gradle / Ivy

There is a newer version: 0.26.0
Show newest version
package io.fluidsonic.raptor.graphql.internal

import io.fluidsonic.graphql.*
import io.fluidsonic.raptor.*
import io.fluidsonic.stdlib.*
import kotlin.reflect.*
import kotlin.reflect.full.*


internal class GraphSystemBuilder private constructor(
	private val typeSystem: GraphTypeSystem,
) {

	private val interfaceTypesByKotlinType = typeSystem.types
		.filterIsInstance()
		.associateBy { it.kotlinType }


	private fun build() = GraphSystem(
		schema = buildSchema(),
		typeSystem = typeSystem
	)


	// TODO validate
	private fun buildSchema() = GSchema(
		document = GDocument(definitions = buildDirectiveDefinitions() + buildTypeDefinitions()),
		supportOptional = true,
	)


	private fun buildDirectiveDefinitions(): List = buildList {
		val referencedDirectiveNames = findReferencedDirectiveNames()

		if (referencedDirectiveNames.contains(GraphDirective.optional.name))
			add(GDirectiveDefinition(
				description = "An argument with this directive does not require a value. " +
					"Providing no value may lead to a different behavior than providing a null value.",
				name = GraphDirective.optional.name,
				locations = setOf(GDirectiveLocation.ARGUMENT_DEFINITION, GDirectiveLocation.INPUT_FIELD_DEFINITION)
			))
	}


	private fun buildEnumDefinition(type: EnumGraphType) = GEnumType(
		description = type.description,
		name = type.name,
		values = type.values.map { GEnumValueDefinition(name = it) }.sortedBy { it.name },
		extensions = GNodeExtensionSet {
			outputCoercer = EnumCoercer
			nodeInputCoercer = EnumCoercer
			raptorType = type
			variableInputCoercer = EnumCoercer
		}
	)


	private fun buildFieldArgumentDefinition(argument: GraphArgument) = GFieldArgumentDefinition(
		defaultValue = argument.defaultValue,
		description = argument.description,
		directives = argument.directives.map { GDirective(name = it.name) }.sortedBy { it.name },
		name = argument.name,
		type = typeRef(argument.kotlinType, isInput = true),
		extensions = GNodeExtensionSet {
			raptorArgument = argument
			raptorType = underlyingType(argument.kotlinType, isInput = true)
		}
	)


	private fun buildFieldDefinition(field: GraphField) = GFieldDefinition(
		argumentDefinitions = field.arguments.map(::buildFieldArgumentDefinition).sortedBy { it.name },
		description = field.description,
		name = field.name,
		type = typeRef(field.kotlinType, isInput = false),
		extensions = GNodeExtensionSet {
			raptorField = field
			raptorType = underlyingType(field.kotlinType, isInput = false)
			resolver = FieldResolver
		}
	)


	private fun buildInputObjectArgumentDefinition(argument: GraphArgument) = GInputObjectArgumentDefinition(
		defaultValue = argument.defaultValue,
		description = argument.description,
		directives = argument.directives.map { GDirective(name = it.name) }.sortedBy { it.name },
		name = argument.name,
		type = typeRef(argument.kotlinType, isInput = true),
		extensions = GNodeExtensionSet {
			raptorArgument = argument
			raptorType = underlyingType(argument.kotlinType, isInput = true)
		}
	)


	private fun buildInputObjectDefinition(type: InputObjectGraphType) = GInputObjectType(
		argumentDefinitions = type.arguments.map(::buildInputObjectArgumentDefinition).sortedBy { it.name },
		description = type.description,
		name = type.name,
		extensions = GNodeExtensionSet {
			nodeInputCoercer = InputObjectCoercer
			raptorType = type
			variableInputCoercer = InputObjectCoercer
		}
	)


	private fun buildInterfaceDefinition(type: InterfaceGraphType) = GInterfaceType(
		description = type.description,
		fieldDefinitions = type.fields.map(::buildFieldDefinition).sortedBy { it.name },
		name = type.name,
		extensions = GNodeExtensionSet {
			raptorType = type
		}
	)


	private fun buildObjectDefinition(type: ObjectGraphType) = GObjectType(
		description = type.description,
		fieldDefinitions = type.fields.map(::buildFieldDefinition).sortedBy { it.name },
		interfaces = interfaceTypeRefsForKotlinType(type.kotlinType).sortedBy { it.name },
		name = type.name,
		extensions = GNodeExtensionSet {
			kotlinType = type.kotlinType.classifier
			raptorType = type
		}
	)


	private fun buildScalarDefinition(type: ScalarGraphType) = GCustomScalarType(
		description = type.description,
		name = type.name,
		extensions = GNodeExtensionSet {
			nodeInputCoercer = ScalarCoercer
			outputCoercer = ScalarCoercer
			raptorType = type
			variableInputCoercer = ScalarCoercer
		}
	)


	private fun buildTypeDefinition(type: NamedGraphType) = when (type) {
		is EnumGraphType -> buildEnumDefinition(type)
		is InputObjectGraphType -> buildInputObjectDefinition(type)
		is InterfaceGraphType -> buildInterfaceDefinition(type)
		is ObjectGraphType -> buildObjectDefinition(type)
		is ScalarGraphType -> buildScalarDefinition(type)
		is UnionGraphType -> buildUnionDefinition(type)
	}


	private fun buildTypeDefinitions(): List =
		typeSystem.types.filterIsInstance().map(::buildTypeDefinition).sortedBy { it.name }


	private fun buildUnionDefinition(type: UnionGraphType) = GUnionType(
		description = type.description,
		name = type.name,
		possibleTypes = resolvePossibleTypesForKotlinType(type.kotlinType),
		extensions = GNodeExtensionSet {
			raptorType = type
		}
	)


	private fun findReferencedDirectiveNames(): Set =
		typeSystem.types.flatMapTo(hashSetOf()) { type ->
			when (type) {
				is AliasGraphType,
				is EnumGraphType,
				is ScalarGraphType,
				is UnionGraphType,
				->
					emptyList()

				is InputObjectGraphType ->
					type.arguments.flatMap { it.directives }.map { it.name }

				is InterfaceGraphType ->
					type.fields.flatMap { it.arguments }.flatMap { it.directives }.map { it.name }

				is ObjectGraphType ->
					type.fields.flatMap { it.arguments }.flatMap { it.directives }.map { it.name }
			}
		}


	private fun interfaceTypeRefsForKotlinType(kotlinType: KotlinType): List {
		val typeNames = mutableSetOf()
		interfaceTypeNamesForObjectValueClass(kotlinType, target = typeNames)

		return typeNames.map(::GNamedTypeRef)
	}


	private fun interfaceTypeNamesForObjectValueClass(kotlinType: KotlinType, target: MutableSet) {
		for (superType in kotlinType.classifier.supertypes) {
			val superClass = superType.classifier as? KClass<*> ?: continue
			if (superClass.typeParameters.isNotEmpty())
				continue // TODO Won't work for generic interfaces

			val superKotlinType = KotlinType(classifier = superClass, isNullable = false)

			val gqlSuperClassName = interfaceTypesByKotlinType[superKotlinType]?.name
			if (gqlSuperClassName !== null)
				target += gqlSuperClassName

			interfaceTypeNamesForObjectValueClass(superKotlinType, target = target)
		}
	}


	private fun resolvePossibleTypesForKotlinType(kotlinType: KotlinType): List =
		when (kotlinType.classifier) {
			RaptorUnion2::class -> listOf(
				// TODO hack
				GNamedTypeRef(((typeSystem.resolveOutputType(kotlinType.typeArguments[0]!!)
					?: error("Cannot resolve GraphQL type for Kotlin type '${kotlinType.typeArguments[0]}'.")) as NamedGraphType).name),
				GNamedTypeRef(((typeSystem.resolveOutputType(kotlinType.typeArguments[1]!!)
					?: error("Cannot resolve GraphQL type for Kotlin type '${kotlinType.typeArguments[1]}'.")) as NamedGraphType).name),
			)

			else -> typeSystem.types
				.filterIsInstance()
				.filterNot { it.kotlinType.isGeneric }
				.filter { it.kotlinType.classifier.isSubclassOf(kotlinType.classifier) }
				.ifEmpty { error("Cannot find any possible types for union type '$kotlinType'.") }
				.map { it.name }
				.sorted()
				.map(::GNamedTypeRef)
		}


	private fun typeRef(kotlinType: KotlinType, isInput: Boolean): GTypeRef {
		val nonNullKotlinType = kotlinType.withNullable(false)

		return when (nonNullKotlinType.classifier) {
			Collection::class, List::class, Set::class -> // TODO improve
				GListTypeRef(typeRef(checkNotNull(nonNullKotlinType.typeArguments.single()), isInput = isInput))

			Maybe::class ->
				return typeRef(checkNotNull(nonNullKotlinType.typeArguments.single()), isInput = isInput)

			else -> when (isInput) {
				true -> typeSystem.resolveInputType(nonNullKotlinType)
				false -> typeSystem.resolveOutputType(nonNullKotlinType)
			}
				.ifNull { error("Cannot resolve GraphQL type for Kotlin type '$nonNullKotlinType'.") } // TODO print stacktrace of usage(s) here
				.let { type ->
					when (type) {
						is AliasGraphType -> when {
							type.isId -> GIdTypeRef
							else -> typeRef(type.referencedKotlinType, isInput = isInput).nullableRef
						}

						is NamedGraphType ->
							GNamedTypeRef(type.name)
					}
				}
		}.let { typeRef ->
			when (kotlinType.isNullable) {
				true -> typeRef
				false -> GNonNullTypeRef(typeRef)
			}
		}
	}


	private fun underlyingType(kotlinType: KotlinType, isInput: Boolean): GraphType {
		@Suppress("NAME_SHADOWING")
		val kotlinType = kotlinType.withNullable(false)

		return when (kotlinType.classifier) {
			Collection::class, List::class, Maybe::class, Set::class -> // TODO improve
				underlyingType(checkNotNull(kotlinType.typeArguments.single()), isInput = isInput)

			else -> when (isInput) {
				true -> typeSystem.resolveInputType(kotlinType)
				false -> typeSystem.resolveOutputType(kotlinType)
			} ?: error("Cannot resolve GraphQL type for Kotlin type '$kotlinType'.")
		}
	}


	companion object {

		fun build(typeSystem: GraphTypeSystem): GraphSystem =
			GraphSystemBuilder(typeSystem = typeSystem).build()
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy