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

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

The newest version!
package io.fluidsonic.raptor.graphql.internal

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


// TODO check duplicate op field names
internal class GraphSystemDefinitionBuilder private constructor(
	private val initialDefinitions: Collection,
) {

	private val aliasTypeDefinition: MutableCollection = mutableListOf()
	private val definitionsToResolveReferences: MutableList = mutableListOf()
	private val inputTypeDefinitionRegistry = TypeDefinitionRegistry(inputOrOutput = "input")
	private val operationDefinitionsByType: MutableMap> = mutableMapOf()
	private val outputTypeDefinitionRegistry = TypeDefinitionRegistry(inputOrOutput = "output")
	private val registeredDefinitions: MutableSet = hashSetOf()
	private val typeDefinitionsByName: MutableMap = hashMapOf()
	private val unspecializedDefinitionsByClassifier: MutableMap> = hashMapOf()

	private val interfaceTypeExtensionDefinitionsByKotlinType: MutableMap> =
		hashMapOf()

	private val objectTypeExtensionDefinitionsByKotlinType: MutableMap> =
		hashMapOf()


	fun build(): GraphSystemDefinition {
		process()

		return GraphSystemDefinition(
			definitions = registeredDefinitions.mapNotNull { definition -> // TODO refactor
				when (definition) {
					is GraphTypeSystemDefinition -> when (definition.kotlinType.isSpecialized) {
						true -> definition
						false -> {
							val typeArguments = definition.kotlinType.classifier.typeParameters.map { it.upperBounds.single() }
							if (typeArguments.any { it.classifier == Any::class }) // TODO ok?
								return@mapNotNull null

							definition.specialize(
								typeArguments = typeArguments.map { typeArgument ->
									KotlinType.of(
										type = typeArgument,
										containingType = null,
										allowMaybe = false,
										allowNull = true,
										allowedVariance = KVariance.OUT,
										requireSpecialization = true
									) // TODO
								},
								namePrefix = ""
							)
						}
					}

					is GraphOperationDefinition -> definition
				}
			}
		)
	}


	private fun checkExtensionFieldNameCollisions() {
		for (definition in typeDefinitionsByName.values)
			when (definition) {
				is InterfaceGraphDefinition -> checkExtensionFieldNameCollisions(
					fieldDefinitions = definition.fieldDefinitions +
						interfaceTypeExtensionDefinitionsByKotlinType[definition.kotlinType]?.flatMap { it.fieldDefinitions }.orEmpty(),
					typeDefinition = definition,
					typeKind = "interface"
				)

				is ObjectGraphDefinition -> checkExtensionFieldNameCollisions(
					fieldDefinitions = definition.fieldDefinitions +
						objectTypeExtensionDefinitionsByKotlinType[definition.kotlinType]?.flatMap { it.fieldDefinitions }.orEmpty(),
					typeDefinition = definition,
					typeKind = "object"
				)

				else ->
					Unit
			}
	}


	private fun checkExtensionFieldNameCollisions(
		fieldDefinitions: Collection,
		typeDefinition: NamedGraphTypeDefinition,
		typeKind: String,
	) {
		val fieldDefinitionsByName = hashMapOf()
		for (fieldDefinition in fieldDefinitions)
			fieldDefinitionsByName.put(fieldDefinition.name, fieldDefinition)?.let { existingFieldDefinition ->
				error(
					"An extension defines a duplicate field '${fieldDefinition.name}' on $typeKind type '${typeDefinition.name}'.\n" +
						"$fieldDefinition\n" +
						"Previous: $existingFieldDefinition"
				)
			}
	}


	private fun process() {
		registerDefinitions(defaultDefinitions)
		registerDefinitions(initialDefinitions)
		resolveAliasTypeDefinitions()
		resolveAllReferences()

		checkExtensionFieldNameCollisions()
	}


	private fun registerAliasDefinition(definition: AliasGraphTypeDefinition) {
		aliasTypeDefinition += definition
	}


	private fun registerDefinition(definition: RaptorGraphDefinition) {
		if (!registeredDefinitions.add(definition))
			return

		definitionsToResolveReferences += definition

		when (definition) {
			is GraphOperationDefinition -> registerOperationDefinition(definition)
			is GraphTypeSystemDefinition -> registerTypeSystemDefinition(definition)
		}

		registerDefinitions(definition.additionalDefinitions)
	}


	private fun registerDefinitions(definitions: Collection) {
		for (definition in definitions)
			registerDefinition(definition)
	}


	private fun registerInterfaceExtensionDefinition(definition: InterfaceExtensionGraphDefinition) {
		interfaceTypeExtensionDefinitionsByKotlinType.getOrPut(definition.kotlinType, ::mutableListOf).add(definition)
	}


	private fun registerNamedTypeDefinition(definition: NamedGraphTypeDefinition) {
		if (!definition.kotlinType.isSpecialized)
			return

		when (definition.name) {
			GLanguage.defaultMutationTypeName,
			GLanguage.defaultQueryTypeName,
			GLanguage.defaultSubscriptionTypeName,
			->
				error("A GraphQL type definition must not use the default operation type name '${definition.name}':\n$definition\n---")
		}

		typeDefinitionsByName.put(definition.name, definition)?.let { existingDefinition ->
			error(
				"More than one GraphQL type definition has been provided with type name '${definition.name}':\n" +
					"1st: $existingDefinition\n" +
					"2nd: $definition\n---"
			)
		}
	}


	private fun registerObjectExtensionDefinition(definition: ObjectExtensionGraphDefinition) {
		objectTypeExtensionDefinitionsByKotlinType.getOrPut(definition.kotlinType, ::mutableListOf).add(definition)
	}


	private fun registerOperationDefinition(definition: GraphOperationDefinition) {
		operationDefinitionsByType
			.getOrPut(definition.operationType, ::hashMapOf)
			.put(definition.fieldDefinition.name, definition)
			?.let { existingDefinition ->
				error(
					"More than one GraphQL ${definition.operationType} operation definition has been provided " +
						"with name '${definition.fieldDefinition.name}':\n" +
						"1st: $existingDefinition\n" +
						"2nd: $definition\n---"
				)
			}
	}


	private fun registerTypeDefinition(definition: GraphTypeDefinition) {
		if (definition.isInput)
			inputTypeDefinitionRegistry.register(definition)

		if (definition.isOutput)
			outputTypeDefinitionRegistry.register(definition)

		when (definition) {
			is AliasGraphTypeDefinition -> registerAliasDefinition(definition)
			is NamedGraphTypeDefinition -> registerNamedTypeDefinition(definition)
		}
	}


	private fun registerTypeExtensionDefinition(definition: GraphTypeExtensionDefinition) {
		when (definition) {
			is InterfaceExtensionGraphDefinition -> registerInterfaceExtensionDefinition(definition)
			is ObjectExtensionGraphDefinition -> registerObjectExtensionDefinition(definition)
		}
	}


	private fun registerTypeSystemDefinition(definition: GraphTypeSystemDefinition) {
		if (!definition.kotlinType.isSpecialized)
			unspecializedDefinitionsByClassifier.getOrPut(definition.kotlinType, ::mutableListOf).add(definition)

		when (definition) {
			is GraphTypeDefinition -> registerTypeDefinition(definition)
			is GraphTypeExtensionDefinition -> registerTypeExtensionDefinition(definition)
		}
	}


	private fun resolveAliasTypeDefinition(definition: AliasGraphTypeDefinition) {
		val inputDefinition = definition.isInput.thenTake {
			inputTypeDefinitionRegistry.resolve(definition.referencedKotlinType, referee = definition)
		}
		val outputDefinition = definition.isOutput.thenTake {
			outputTypeDefinitionRegistry.resolve(definition.referencedKotlinType, referee = definition)
		}

		if (inputDefinition == null && outputDefinition == null) {
			val inputOrOutput = when {
				definition.isInput && definition.isOutput -> "input or output"
				definition.isInput -> "input"
				definition.isOutput -> "output"
				else -> error("Definition is neither input nor output type.")
			}

			error(
				"No GraphQL $inputOrOutput type definition was provided for Kotlin type '${definition.referencedKotlinType}' " +
					"referenced by Kotlin type '${definition.kotlinType}':\n" +
					"$definition\n---"
			)
		}

		if (inputDefinition is AliasGraphTypeDefinition)
			error(
				"GraphQL alias Kotlin type '${definition.kotlinType}' cannot reference alias Kotlin type '${definition.referencedKotlinType}':\n" +
					"Alias definition: $definition\n" +
					"Referenced alias: $inputDefinition\n---"
			)

		if (outputDefinition is AliasGraphTypeDefinition)
			error(
				"GraphQL alias Kotlin type '${definition.kotlinType}' cannot reference alias Kotlin type '${definition.referencedKotlinType}':\n" +
					"Alias: $definition\n" +
					"Referenced alias: $outputDefinition\n---"
			)
	}


	private fun resolveAliasTypeDefinitions() {
		for (definition in aliasTypeDefinition)
			resolveAliasTypeDefinition(definition)
	}


	private tailrec fun resolveAllReferences() {
		resolveReferences(definitionsToResolveReferences.removeLastOrNull() ?: return)
		resolveAllReferences()

		// Recursive because specialization may add new type definitions as needed.
	}


	// TODO rework
	private fun resolveReferences(definition: RaptorGraphDefinition) {
		when (definition) {
			is AliasGraphTypeDefinition -> Unit
			is EnumGraphDefinition -> Unit
			is ScalarGraphDefinition -> Unit
			is UnionGraphDefinition -> Unit

			is InputObjectGraphDefinition -> definition.argumentDefinitions.forEach { argument ->
				inputTypeDefinitionRegistry.resolve(argument.kotlinType, referee = argument)
			}

			is InterfaceGraphDefinition -> definition.fieldDefinitions.forEach { field ->
				outputTypeDefinitionRegistry.resolve(field.kotlinType, referee = field)
			}

			is InterfaceExtensionGraphDefinition -> {
				outputTypeDefinitionRegistry.resolve(definition.kotlinType, referee = definition)

				definition.fieldDefinitions.forEach { field ->
					outputTypeDefinitionRegistry.resolve(field.kotlinType, referee = field)
				}
			}

			is ObjectGraphDefinition -> definition.fieldDefinitions.forEach { field ->
				outputTypeDefinitionRegistry.resolve(field.kotlinType, referee = field)
			}

			is ObjectExtensionGraphDefinition -> {
				outputTypeDefinitionRegistry.resolve(definition.kotlinType, referee = definition)

				definition.fieldDefinitions.forEach { field ->
					outputTypeDefinitionRegistry.resolve(field.kotlinType, referee = field)
				}
			}

			is GraphOperationDefinition -> {
				val outputType = definition.fieldDefinition.kotlinType
				val resolved = outputTypeDefinitionRegistry.resolve(outputType, referee = definition.fieldDefinition)
				if (resolved == null && outputType.classifier != Collection::class && outputType.classifier != List::class) // TODO
					error("Cannot resolve output type of $definition")
			}
		}
	}


	private fun specializeGenericTypeDefinition(kotlinType: KotlinType, typeArgument: KotlinType, argumentType: GraphTypeDefinition) {
		argumentType as NamedGraphTypeDefinition // TODO

		val definitions = checkNotNull(unspecializedDefinitionsByClassifier[kotlinType.withNullable(false)])
		for (definition in definitions)
			registerDefinition(definition.specialize(typeArguments = listOf(typeArgument), namePrefix = argumentType.name)) // TODO multiple
	}


	companion object {

		private val defaultDefinitions: Collection = listOf(
			Boolean.graphDefinition(),
			Double.graphDefinition(),
			GraphId.graphDefinition(),
			Int.graphDefinition(),
			String.graphDefinition()
		)


		fun build(definitions: Collection) =
			GraphSystemDefinitionBuilder(initialDefinitions = definitions).build()
	}


	private inner class TypeDefinitionRegistry(
		private val inputOrOutput: String,
	) {

		private val definitionsByKotlinType: MutableMap = hashMapOf()


		fun register(definition: GraphTypeDefinition) {
			definitionsByKotlinType.put(definition.kotlinType, definition)?.let { existingDefinition ->
				error(
					"More than one GraphQL $inputOrOutput type definition has been provided for Kotlin type '${definition.kotlinType}':\n" +
						"1st: $existingDefinition\n" +
						"2nd: $definition\n---"
				)
			}
		}


		// TODO handle alias and generic alias argument
		fun resolve(kotlinType: KotlinType, referee: RaptorGraphNode): GraphTypeDefinition? {
			@Suppress("NAME_SHADOWING")
			val kotlinType = kotlinType.withNullable(false)

			definitionsByKotlinType[kotlinType]
				?.let { return it }

			// TODO nesting
			val typeArgument = kotlinType.typeArguments.singleOrNull() ?: run { // TODO
				check(!unspecializedDefinitionsByClassifier.containsKey(kotlinType)) {
					"A GraphQL definition cannot reference the generic Kotlin type '$kotlinType' without specifying a type argument:\n" +
						"$referee\n---"
				}

				return null
			}

			val kotlinTypeWithoutTypeArgument = kotlinType.withoutTypeArguments()
			val genericDefinition = definitionsByKotlinType[kotlinTypeWithoutTypeArgument]
				?: return null

			check(genericDefinition.kotlinType.isGeneric) {
				"A GraphQL definition cannot specify a type argument when referencing the non-generic Kotlin type '$kotlinTypeWithoutTypeArgument':\n" +
					"$referee\n---"
			}

			val argumentType = resolve(typeArgument, referee = referee)
				?: return null

			specializeGenericTypeDefinition(kotlinTypeWithoutTypeArgument, typeArgument = typeArgument, argumentType = argumentType)

			return checkNotNull(definitionsByKotlinType[kotlinType])
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy