diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1fd1cfa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + workflow_call: + +permissions: + contents: read + +jobs: + build: + strategy: + matrix: + include: + - target: iosSimulatorArm64Test + os: macos-latest + - target: jvmTest + os: ubuntu-latest + - target: linuxX64Test + os: ubuntu-latest + - target: testDebugUnitTest + os: ubuntu-latest + - target: testReleaseUnitTest + os: ubuntu-latest + - target: jsTest + os: ubuntu-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v3 + - uses: actions/cache@v4 + with: + path: | + ~/.konan + key: ${{ runner.os }}-${{ hashFiles('**/.lock') }} + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + - name: Build with Gradle + uses: gradle/gradle-build-action@093dfe9d598ec5a42246855d09b49dc76803c005 + with: + arguments: ${{ matrix.target }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index f936052..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,58 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle - -name: Deploy to central - -on: workflow_dispatch - -permissions: - contents: read - -env: - ORG_GRADLE_PROJECT_mavenCentralUsername: '${{ secrets.MAVEN_CENTRAL_USERNAME }}' - ORG_GRADLE_PROJECT_mavenCentralPassword: '${{ secrets.MAVEN_CENTRAL_PASSWORD }}' - ORG_GRADLE_PROJECT_signingInMemoryKeyId: '${{ secrets.SIGNING_KEY_ID }}' - ORG_GRADLE_PROJECT_signingInMemoryKey: '${{ secrets.SIGNING_KEY }}' - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: '${{ secrets.SIGNING_KEY_PASSWORD }}' - -jobs: - build: - uses: ./.github/workflows/gradle.yml - deploy: - needs: build - strategy: - matrix: - include: - - target: publishIosArm64PublicationToMavenCentral - os: macos-latest - - target: publishAndroidReleasePublicationToMavenCentral - os: ubuntu-latest - - target: publishJvmPublicationToMavenCentral - os: ubuntu-latest - - target: publishLinuxX64PublicationToMavenCentral - os: ubuntu-latest - - target: publishKotlinMultiplatformPublicationToMavenCentral - os: ubuntu-latest - - target: publishJsPublicationToMavenCentral - os: ubuntu-latest - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v3 - - uses: actions/cache@v4 - with: - path: | - ~/.konan - key: ${{ runner.os }}-${{ hashFiles('**/.lock') }} - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - name: Publish to Maven Central - run: ./gradlew publish --no-configuration-cache \ No newline at end of file diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml deleted file mode 100644 index f2092ba..0000000 --- a/.github/workflows/gradle.yml +++ /dev/null @@ -1,56 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle - -name: Java CI with Gradle - -on: - push: - branches: - - main - pull_request: - workflow_call: - -permissions: - contents: read - -jobs: - build: - strategy: - matrix: - include: - - target: iosSimulatorArm64Test - os: macos-latest - - target: jvmTest - os: ubuntu-latest - - target: linuxX64Test - os: ubuntu-latest - - target: testDebugUnitTest - os: ubuntu-latest - - target: testReleaseUnitTest - os: ubuntu-latest - - target: jsTest - os: ubuntu-latest - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v4 - - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v3 - - uses: actions/cache@v4 - with: - path: | - ~/.konan - key: ${{ runner.os }}-${{ hashFiles('**/.lock') }} - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - name: Build with Gradle - uses: gradle/gradle-build-action@093dfe9d598ec5a42246855d09b49dc76803c005 - with: - arguments: ${{ matrix.target }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..39146fe --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,34 @@ +name: Publish +on: + release: + types: [ released, prereleased ] + +jobs: + publish: + name: Release build and publish + runs-on: macOS-latest + steps: + - uses: actions/checkout@v4 + - name: Validate Gradle Wrapper + + uses: gradle/wrapper-validation-action@v3 + - uses: actions/cache@v4 + with: + path: | + ~/.konan + key: ${{ runner.os }}-${{ hashFiles('**/.lock') }} + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Publish to Maven Central + run: ./gradlew publish --no-configuration-cache + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: '${{ secrets.MAVEN_CENTRAL_USERNAME }}' + ORG_GRADLE_PROJECT_mavenCentralPassword: '${{ secrets.MAVEN_CENTRAL_PASSWORD }}' + ORG_GRADLE_PROJECT_signingInMemoryKeyId: '${{ secrets.SIGNING_KEY_ID }}' + ORG_GRADLE_PROJECT_signingInMemoryKey: '${{ secrets.SIGNING_KEY }}' + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: '${{ secrets.SIGNING_KEY_PASSWORD }}' \ No newline at end of file diff --git a/README.md b/README.md index db6f5f8..56adca0 100644 --- a/README.md +++ b/README.md @@ -175,10 +175,8 @@ object DataModule : Di.Module { Di.init( // Registers the following modules in the DI container - modules = setOf( AppModule, DataModule, - ) ) Di.get() ``` @@ -191,13 +189,13 @@ To re-use and encapsulate DI logic you can create `Di.Module`s that you can late Ivy DI supports grouping your dependencies into scopes. This way you can manage their lifecycle and free resources as soon as they are no longer needed. **AppScope** and **FeatureScope** -are built-in but you can easily define your own scopes using `Di.newScope("my-scope")`. +are built-in, but you can easily define your own scopes using `Di.newScope("my-scope")`. ```kotlin -data class UserInfo(val id: String, name: String) +data class UserInfo(val id: String, val name: String) val UserScope = Di.newScope("user") -fun Di.userScope(block: Di.Scope.() -> Unit) = Di.scope(UserScope, block) // helper function (optional) +fun Di.userScope(block: Di.Scope.() -> Unit) = Di.inScope(UserScope, block) // helper function (optional) suspend fun login() { val userInfo = loginInternally() // UserInfo("1", "John") @@ -221,10 +219,33 @@ suspend fun logout() { } ``` -### 2. Multibindings 🚧 +Scopes are also extremely useful for defining multiple dependencies of the same type +and picking the most appropriate one based on the scope using **affinity**. + +```kotlin +data class Screen(val name: String) +Di.appScope { + register { "Hello from app!" } + autoWire(::Screen) +} +Di.featureScope { + register { "Hello from feature!" } + autoWire(::Screen) +} + +Di.get(affinity = AppScope) // "Hello from app!" +Di.get(affinity = AppScope) // Screen(name="Hello from app!") +Di.get(affinity = FeatureScope) // "Hello from feature!" +Di.get(affinity = FeatureScope) // Screen(name="Hello from feature!") +``` + +### 2. Multi-bindings 🚧 Currently not supported, investigating this use-case and whether we can support it nicely. +> [!WARNING] +> So far, we haven't found a nice and efficient solution. + ### 3. Lazy initialization By default, all instances in Ivy DI are lazily initialized only after `Di.get()` is called. diff --git a/di/src/commonMain/kotlin/ivy/di/DiContainer.kt b/di/src/commonMain/kotlin/ivy/di/DiContainer.kt index c3cdcdb..e815e26 100644 --- a/di/src/commonMain/kotlin/ivy/di/DiContainer.kt +++ b/di/src/commonMain/kotlin/ivy/di/DiContainer.kt @@ -1,5 +1,6 @@ package ivy.di +import ivy.di.Di.Scope import kotlin.jvm.JvmInline import kotlin.reflect.KClass @@ -11,22 +12,23 @@ val FeatureScope = Di.newScope("feature") object Di { private val scopes = mutableSetOf() + @DiInternalApi val factories = mutableMapOf() + + @DiInternalApi val singletons = mutableSetOf>() + + @DiInternalApi val singletonInstances = mutableMapOf() /** - * Initializes a set of modules by calling their [Module.init] functions. + * Initializes [modules] by calling their [Module.init] functions. + * @param modules an array of modules to be initialized one by one */ - fun init(modules: Set) { - modules.forEach(::init) + fun init(vararg modules: Module) { + modules.forEach(Module::init) } - /** - * Initializes a module by calling its [Module.init] function. - */ - fun init(module: Module) = module.init() - /** * Scope used to register dependencies for the entire lifetime of the application. */ @@ -47,7 +49,7 @@ object Di { /** * Utility function for registering dependencies in a specific scope. */ - fun scope(scope: Scope, block: Scope.() -> Unit) = scope.block() + fun inScope(scope: Scope, block: Scope.() -> Unit) = scope.block() /** * Registers a factory for a dependency [T]. @@ -89,12 +91,19 @@ object Di { /** * The same as [get] but returns a [Lazy] instance. * @param named An optional qualifier to distinguish between multiple dependencies of the same type. + * @param affinity preferred scope in which to look for the dependency first * @throws DependencyInjectionError if no factory for [T] with qualifier [named] is registered. */ @Throws(DependencyInjectionError::class) - inline fun getLazy(named: Any? = null): Lazy { - factoryOrThrow(T::class, named) // ensure that factory exists - return lazy { get(named) } + inline fun getLazy( + named: Any? = null, + affinity: Scope? = null, + ): Lazy { + val classKey = T::class + // ensure that factory exists + affinity?.factoryOrNull(classKey, named) + ?: factoryOrThrow(classKey, named) // this will throw in case of null + return lazy { get(named, affinity) } } /** @@ -102,12 +111,17 @@ object Di { * Each call to [get] will return a new instance using your registered factory. * If [T] is a [singleton], the same instance will be returned on subsequent calls. * @param named An optional qualifier to distinguish between multiple dependencies of the same type. + * @param affinity preferred scope in which to look for the dependency first * @throws DependencyInjectionError if no factory for [T] with qualifier [named] is registered. */ @Throws(DependencyInjectionError::class) - inline fun get(named: Any? = null): T { + inline fun get( + named: Any? = null, + affinity: Scope? = null, + ): T { val classKey = T::class - val (scope, factory) = factoryOrThrow(classKey, named) + val (scope, factory) = affinity?.factoryOrNull(classKey, named) + ?: factoryOrThrow(classKey, named) val depKey = DependencyKey(scope, classKey, named) return if (classKey in singletons) { if (depKey in singletonInstances) { @@ -130,15 +144,32 @@ object Di { * Returns a factory for a dependency identified by [classKey] and [named]. * @throws DependencyInjectionError if no factory is registered. */ + @DiInternalApi @Throws(DependencyInjectionError::class) fun factoryOrThrow( classKey: KClass<*>, named: Any?, ): Pair = scopes .firstNotNullOfOrNull { scope -> - scopedFactoryOrNull(scope, classKey, named) + scope.factoryOrNull(classKey, named) } ?: throw DependencyInjectionError(diErrorMsg(classKey, named)) + @DiInternalApi + fun Scope.factoryOrNull( + classKey: KClass<*>, + named: Any?, + ): Pair? = scopedFactoryOrNull(this, classKey, named) + + @DiInternalApi + fun scopedFactoryOrNull( + scope: Scope, + classKey: KClass<*>, + named: Any?, + ): Pair Any>? = factories[DependencyKey(scope, classKey, named)] + ?.let { factory -> + scope to factory + } + private fun diErrorMsg(classKey: KClass<*>, named: Any?): String = buildString { append("No factory") if (named != null) { @@ -157,17 +188,8 @@ object Di { append("\nDid you forget to register '$dependencyId' in Ivy DI?") } - private fun scopedFactoryOrNull( - scope: Scope, - classKey: KClass<*>, - named: Any?, - ): Pair Any>? = factories[DependencyKey(scope, classKey, named)] - ?.let { factory -> - scope to factory - } - /** - * Clears all instances in the given [scope]. + * Clears all instances in the given [Di.Scope]. */ fun clear(scope: Scope) { singletonInstances.keys.forEach { instanceKey -> @@ -179,12 +201,16 @@ object Di { /** * Resets the DI container by clearing all instances, singletons and factories. - * Note: [scopes] aren't clear for performance reasons. */ fun reset() { singletonInstances.clear() factories.clear() singletons.clear() + + // Reset scopes to the default state + scopes.clear() + scopes.add(AppScope) + scopes.add(FeatureScope) } /** @@ -201,11 +227,32 @@ object Di { ) /** - * A DI scope. Scopes are used to group dependencies and manage their lifecycle. + * Scopes are used to group dependencies together and manage their lifecycle. + * + * __Note:__ A dependency (class) can be registered into multiple scopes and its factory + * will be picked based on affinity or scopes registration order. + * + * ```kotlin + * Di.appScope { + * register { "hello" } + * } + * Di.featureScope { + * register { "world" } + * } + * + * Di.get(affinity = AppScope) // "hello" + * Di.get(affinity = FeatureScope) // "world" + * Di.get() // not deterministic + * ``` */ @JvmInline value class Scope internal constructor(val value: String) + /** + * DI module that you can use to group and re-use dependency injection logic. + * + * _Tip: creating DI modules by layer (e.g. data, domain) or features (login, main) is a good idea!_ + */ interface Module { /** * Register your DI dependencies in this function. @@ -214,7 +261,20 @@ object Di { } } +/** + * Same as [Di.get] but with affinity set to the receiver [Di.Scope] - [this]. + * Read **[Di.get]**. + */ +inline fun Scope.get( + named: Any? = null, +): T = Di.get(named, this) + /** * An exception thrown when a factory for a dependency is not found in the DI container. */ -class DependencyInjectionError(msg: String) : IllegalStateException(msg) \ No newline at end of file +class DependencyInjectionError(msg: String) : IllegalStateException(msg) + +/** + * Internal API, please don't use it. + */ +annotation class DiInternalApi \ No newline at end of file diff --git a/di/src/commonMain/kotlin/ivy/di/autowire/new.kt b/di/src/commonMain/kotlin/ivy/di/autowire/new.kt index 1b4a21e..a36d00d 100644 --- a/di/src/commonMain/kotlin/ivy/di/autowire/new.kt +++ b/di/src/commonMain/kotlin/ivy/di/autowire/new.kt @@ -1,6 +1,7 @@ package ivy.di.autowire import ivy.di.Di +import ivy.di.get import kotlin.jvm.JvmName /** @@ -14,615 +15,615 @@ inline fun new( * @see new */ @JvmName("new1") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1) -> R, -): R = constructor(Di.get()) +): R = constructor(get()) /** * @see new */ @JvmName("new2") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2) -> R, -): R = constructor(Di.get(), Di.get()) +): R = constructor(get(), get()) /** * @see new */ @JvmName("new3") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3) -> R, -): R = constructor(Di.get(), Di.get(), Di.get()) +): R = constructor(get(), get(), get()) /** * @see new */ @JvmName("new4") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4) -> R, -): R = constructor(Di.get(), Di.get(), Di.get(), Di.get()) +): R = constructor(get(), get(), get(), get()) /** * @see new */ @JvmName("new5") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5) -> R, -): R = constructor(Di.get(), Di.get(), Di.get(), Di.get(), Di.get()) +): R = constructor(get(), get(), get(), get(), get()) /** * @see new */ @JvmName("new6") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6) -> R, -): R = constructor(Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get()) +): R = constructor(get(), get(), get(), get(), get(), get()) /** * @see new */ @JvmName("new7") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7) -> R, -): R = constructor(Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get()) +): R = constructor(get(), get(), get(), get(), get(), get(), get()) /** * @see new */ @JvmName("new8") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8) -> R, -): R = constructor(Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get()) +): R = constructor(get(), get(), get(), get(), get(), get(), get(), get()) /** * @see new */ @JvmName("new9") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R, -): R = constructor(Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get()) +): R = constructor(get(), get(), get(), get(), get(), get(), get(), get(), get()) /** * @see new */ @JvmName("new10") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) -> R, -): R = constructor(Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get()) +): R = constructor(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) /** * @see new */ @JvmName("new11") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new12") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new13") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new14") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new15") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new16") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new17") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new18") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new19") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new20") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new21") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new22") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new23") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new24") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new25") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new26") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new27") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26, T27) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new28") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26, T27, T28) -> R, ): R = constructor( - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get(), - Di.get() + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() ) /** * @see new */ @JvmName("new29") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26, T27, T28, T29) -> R, ): R = constructor( - Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), - Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), - Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get() + get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), + get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), + get(), get(), get(), get(), get(), get(), get(), get(), get() ) /** * @see new */ @JvmName("new30") -inline fun new( +inline fun Di.Scope.new( crossinline constructor: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26, T27, T28, T29, T30) -> R, ): R = constructor( - Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), - Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), - Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get(), Di.get() + get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), + get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), + get(), get(), get(), get(), get(), get(), get(), get(), get(), get() ) \ No newline at end of file diff --git a/di/src/commonTest/kotlin/ivy/di/AffinityTest.kt b/di/src/commonTest/kotlin/ivy/di/AffinityTest.kt new file mode 100644 index 0000000..43c3b21 --- /dev/null +++ b/di/src/commonTest/kotlin/ivy/di/AffinityTest.kt @@ -0,0 +1,193 @@ +package ivy.di + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.withClue +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import ivy.di.Di.register +import ivy.di.autowire.autoWire +import ivy.di.autowire.autoWireSingleton +import ivy.di.testsupport.FakeStateHolder +import kotlin.test.BeforeTest +import kotlin.test.Test + +class AffinityTest { + + @BeforeTest + fun setup() { + Di.reset() + } + + @Test + fun gets_without_affinity() { + // Given + Di.featureScope { + register { "Hello" } + } + + // When + val str = Di.get() + + // Then + str shouldBe "Hello" + } + + @Test + fun gets_with_affinity() { + // Given + Di.appScope { + register { "app" } + } + Di.featureScope { + register { "feature" } + } + + // When + val fromAppScope = Di.get(affinity = AppScope) + val fromFeatureScope = Di.get(affinity = FeatureScope) + + // Then + fromAppScope shouldBe "app" + fromFeatureScope shouldBe "feature" + } + + @Test + fun get_throws_for_not_registered_classes() { + // When + val thrownException = shouldThrow { + Di.get(affinity = FeatureScope) + } + + // Then + thrownException.message.shouldNotBeNull() + } + + @Test + fun getLazy_throws_for_not_registered_classes() { + // When + val thrownException = shouldThrow { + Di.getLazy(affinity = FeatureScope) + } + + // Then + thrownException.message.shouldNotBeNull() + } + + @Test + fun affinity_autoWire() { + // Given + Di.appScope { register { "app" } } + Di.featureScope { register { "feature" } } + Di.appScope { autoWire(::Screen) } + Di.featureScope { autoWire(::Screen) } + + // When + val appScreen = Di.get(affinity = AppScope) + val featureScope = Di.get(affinity = FeatureScope) + + // Then + appScreen.name shouldBe "app" + featureScope.name shouldBe "feature" + } + + @Test + fun affinity_autoWireSingleton() { + // Given + Di.appScope { + register { "app" } + autoWireSingleton(::Screen) + } + Di.featureScope { + register { "feature" } + autoWireSingleton(::Screen) + } + + // When + val appScreen = Di.get(affinity = AppScope) + val featureScope = Di.get(affinity = FeatureScope) + + // Then + withClue("App screen") { + appScreen.name shouldBe "app" + withClue("should be single instance") { + Di.get(affinity = AppScope) shouldBe appScreen + } + } + withClue("Feature screen") { + featureScope.name shouldBe "feature" + withClue("should be single instance") { + Di.get(affinity = FeatureScope) shouldBe featureScope + } + } + } + + @Test + fun builds_complex_screen() { + // Given + Di.appScope { + register { 42 } + register { "Global" } + } + val screenScope = Di.newScope("my-screen") + Di.inScope(screenScope) { + register { "My Screen" } + autoWire(::ComplexScreen) + } + + // When + val screen = Di.get(affinity = screenScope) + + // Then + screen shouldBe ComplexScreen( + name = "My Screen", + int = 42, + ) + } + + @Test + fun overwrites_dependency_with_affinity() { + // Give + Di.appScope { + register { "global" } + } + Di.featureScope { + register { "old" } + register { "new" } + } + + // When + val appScope = Di.get(affinity = AppScope) + val featureScope = Di.get(affinity = FeatureScope) + + // Then + appScope shouldBe "global" + featureScope shouldBe "new" + } + + @Test + fun qualifiers_and_affinity() { + // Given + Di.appScope { + register(named = "1") { "a1" } + register(named = "2") { "a2" } + } + Di.featureScope { + register(named = "1") { "f1" } + register(named = "2") { "f2" } + } + + // When + shouldThrow { + Di.get() + } + + // Then + Di.get(named = "1", affinity = AppScope) shouldBe "a1" + Di.get(named = "2", affinity = AppScope) shouldBe "a2" + Di.get(named = "1", affinity = FeatureScope) shouldBe "f1" + Di.get(named = "2", affinity = FeatureScope) shouldBe "f2" + } + + data class ComplexScreen(val name: String, val int: Int) + data class Screen(val name: String) +} \ No newline at end of file diff --git a/di/src/commonTest/kotlin/ivy/di/DiContainerTest.kt b/di/src/commonTest/kotlin/ivy/di/DiContainerTest.kt index 0aaac3d..29c78ac 100644 --- a/di/src/commonTest/kotlin/ivy/di/DiContainerTest.kt +++ b/di/src/commonTest/kotlin/ivy/di/DiContainerTest.kt @@ -123,9 +123,9 @@ class DiContainerTest { } @Test - fun moduleRegistration() { + fun module_registration() { // Given - Di.init(setOf(FakeDiModule)) + Di.init(FakeDiModule) // When val instance = Di.get() @@ -134,13 +134,22 @@ class DiContainerTest { instance.shouldNotBeNull() } + @Test + fun modules_init_empty_modules() { + // When + Di.init() + + // Then + // no crashes + } + @Test fun create_DI_scope() { // Given val customScope = Di.newScope("new") // When - Di.scope(customScope) { + Di.inScope(customScope) { register { FakeStateHolder() } } val instance = Di.get() diff --git a/gradle.properties b/gradle.properties index e5de1e5..a12e91a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ android.nonTransitiveRClass=true # Pomfile definitions GROUP=com.ivy-apps POM_ARTIFACT_ID=di -VERSION_NAME=0.0.5 +VERSION_NAME=0.0.6 SONATYPE_HOST=CENTRAL_PORTAL RELEASE_SIGNING_ENABLED=true POM_DESCRIPTION=A simple DI container for Kotlin Multiplatform apps. diff --git a/samples/src/jvmMain/kotlin/affiinity/AffinityDemo.kt b/samples/src/jvmMain/kotlin/affiinity/AffinityDemo.kt new file mode 100644 index 0000000..a5b661c --- /dev/null +++ b/samples/src/jvmMain/kotlin/affiinity/AffinityDemo.kt @@ -0,0 +1,12 @@ +package affiinity + +import ivy.di.Di +import ivy.di.get +import module.AndroidModule + +fun main() { + Di.init(AndroidModule) + Di.appScope { + get() + } +} \ No newline at end of file