Skip to content

Support multiple beans, primary, etc. #2

@tothlp

Description

@tothlp

Tried in Worklog. Cannot set container as private, cannot properly handle subtypes, etc.

Snippet:

package hu.tothlp.worklog.core.di

import com.github.ajalt.mordant.terminal.Terminal
import kotlin.reflect.KClass

//TODO: Migrate to Beanject "lib"
/**
 * Simple DI container. It handles registering and retrieving beans, with a simple DSL.
 */
object Beanject {
	data class Bean(
		val implementation: Any,
		var name: String? = null,
		var primary: Boolean = false
	)

	 val t = Terminal()
	// Internal container for beans. Two variables are needed, because the container is mutable,
	// but the external view should be immutable. We also need a publicly accessible view of the container, so the reified functions can access it.
	internal val internalBeanContainer = mutableMapOf<KClass<*>, MutableList<Bean>>()
	val beanContainer: Map<KClass<*>, List<Bean>> get() = internalBeanContainer

	fun getBeanByName(name: String): Any {
		return beanContainer.values.flatten().firstOrNull { it.name == name } ?: throw IllegalArgumentException("No bean found with name $name")
	}

	inline fun <reified T> getBean(name: String? = null): T {
		t.println(beanContainer)
		if (beanContainer.values.flatten().none {  (it.implementation as? T) != null }) throw IllegalArgumentException("No bean found for type ${T::class.simpleName}")
		return if (!name.isNullOrBlank()) getBeanByName(name) as T
		else beanContainer.values.first { it is T } as T
	}

	internal inline fun <reified T> registerBean(name: String, implementation: Any, primary: Boolean? = false) {
		validateBean<T>(name, primary ?: false)
		val bean = Bean(implementation, name, primary ?: false)
		internalBeanContainer.getOrPut(T::class) { mutableListOf() }.add(bean)
	}

	/**
	 * Validates the bean registration. It should meet the following criteria:
	 *
	 * - Name and primary flag are needed only if more than one bean exist for a type.
	 * - Only one bean can be registered with the same name, no matter the type.
	 * - Only one primary bean can be registered for a type.
	 * - If more than one bean exist for a type, one of them should be marked as primary.
	 * - If more than one bean exist for a type, non-primary ones should have a name.
	 *
	 */
	internal inline fun <reified T> validateBean(name: String?, primary: Boolean) {
		val nonPrimaryNameError = "If more than one bean exist for a type, non-primary ones should have a name."

		val beansForClass = internalBeanContainer[T::class]

		// Check if a primary bean already exists for the given type
		val containsPrimary = beansForClass?.any { it.primary } ?: false

		// Check if a non-primary bean already exists for the given type
		val containsNonPrimary = beansForClass?.any { !it.primary } ?: false

		// Check if a non-primary bean already exists for the given type without a name
		val containsNonPrimaryWithoutName = containsNonPrimary && (beansForClass?.any { it.name.isNullOrBlank() } ?: false)

		val validationError: String? = when {
			!name.isNullOrEmpty() && internalBeanContainer.entries.any { it.value.any { it.name == name } } -> "Bean with name $name already exists"

			// We want to register a primary bean, but a primary bean already exists for the given type
			primary && containsPrimary -> "Primary bean already exists for type ${T::class.simpleName}"

			// We want to register a primary bean, but a non-primary bean already exists for the given type without a name
			primary && containsNonPrimaryWithoutName -> "A non-primary bean already exists for type ${T::class.simpleName}. $nonPrimaryNameError"

			// We want to register a non-primary bean, and at least one non-primary bean exists, but no primary bean. We should also check the new beans and the existing ones for names.
			!primary && containsNonPrimary && !containsPrimary ->
				"A bean already exists for type ${T::class.simpleName}, one of them should be marked as primary.".appendIf(name.isNullOrEmpty() || containsNonPrimaryWithoutName, nonPrimaryNameError)

			// We want to register a non-primary bean, and a primary bean exists, but either the new bean or one of the existing ones don't have a name.
			!primary && containsPrimary && (name.isNullOrEmpty() || containsNonPrimaryWithoutName) -> nonPrimaryNameError
			else -> null
		}
		validationError?.let { throw IllegalArgumentException(it) }
	}

	fun String.appendIf(condition: Boolean, appendix: String) = if (condition) this + appendix else this

	/**
	 * DSL for defining beans.
	 */
	class BeanDefinitionDsl

	/**
	 * Creates the bean registration environment by creating a [BeanDefinitionDsl] and calling the [init] function on it.
	 */
	fun beans(init: BeanDefinitionDsl.() -> Unit): BeanDefinitionDsl = BeanDefinitionDsl().apply(init)

	/**
	 * Registers a bean with the given name and creator function.
	 */
	internal inline fun <reified T : Any> BeanDefinitionDsl.bean(name: String, primary: Boolean? = false, beanCreator: BeanDefinitionDsl.() -> T): T =
		beanCreator().also { registerBean<T>(name, it, primary) }

}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions