diff --git a/.gitignore b/.gitignore
index 52fe2f7..ffab9a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,62 +1,8 @@
-# Xcode
-#
-# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+## MacOS
+.DS_Store
## User settings
xcuserdata/
-## Obj-C/Swift specific
-*.hmap
-
-## App packaging
-*.ipa
-*.dSYM.zip
-*.dSYM
-
-## Playgrounds
-timeline.xctimeline
-playground.xcworkspace
-
-# Swift Package Manager
-#
-# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
-# Packages/
-# Package.pins
-# Package.resolved
-# *.xcodeproj
-#
-# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
-# hence it is not needed unless you have added a package configuration file to your project
-# .swiftpm
-
+## SPM
.build/
-
-# CocoaPods
-#
-# We recommend against adding the Pods directory to your .gitignore. However
-# you should judge for yourself, the pros and cons are mentioned at:
-# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
-#
-# Pods/
-#
-# Add this line if you want to avoid checking in source code from the Xcode workspace
-# *.xcworkspace
-
-# Carthage
-#
-# Add this line if you want to avoid checking in source code from Carthage dependencies.
-# Carthage/Checkouts
-
-Carthage/Build/
-
-# fastlane
-#
-# It is recommended to not store the screenshots in the git repo.
-# Instead, use fastlane to re-generate the screenshots whenever they are needed.
-# For more information about the recommended setup visit:
-# https://docs.fastlane.tools/best-practices/source-control/#source-control
-
-fastlane/report.xml
-fastlane/Preview.html
-fastlane/screenshots/**/*.png
-fastlane/test_output
diff --git a/.mise.toml b/.mise.toml
new file mode 100644
index 0000000..a64b7a1
--- /dev/null
+++ b/.mise.toml
@@ -0,0 +1,8 @@
+[tools]
+swiftlint = "0.54.0"
+swiftformat = "0.53.3"
+
+[settings]
+jobs = 6
+http_timeout = 30
+experimental = true
diff --git a/.mise/helpers/common b/.mise/helpers/common
new file mode 100644
index 0000000..799e36e
--- /dev/null
+++ b/.mise/helpers/common
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+set -euo pipefail
+
+# Predefined variables
+readonly EXT_BUILD_DIR="$MISE_PROJECT_ROOT/.build"
diff --git a/.mise/tasks/autocorrect b/.mise/tasks/autocorrect
new file mode 100755
index 0000000..373d7ac
--- /dev/null
+++ b/.mise/tasks/autocorrect
@@ -0,0 +1,7 @@
+#!/bin/bash
+# mise description="Clean build directory"
+set -euo pipefail
+
+source "$MISE_PROJECT_ROOT/.mise/helpers/common"
+
+swiftlint autocorrect --quiet
diff --git a/.mise/tasks/build b/.mise/tasks/build
new file mode 100755
index 0000000..b8ac9c7
--- /dev/null
+++ b/.mise/tasks/build
@@ -0,0 +1,7 @@
+#!/bin/bash
+# mise description="Build the project"
+set -euo pipefail
+
+source "$MISE_PROJECT_ROOT/.mise/helpers/common"
+
+xcrun swift build
diff --git a/.mise/tasks/clean b/.mise/tasks/clean
new file mode 100755
index 0000000..88324f6
--- /dev/null
+++ b/.mise/tasks/clean
@@ -0,0 +1,7 @@
+#!/bin/bash
+# mise description="Clean build directory"
+set -euo pipefail
+
+source "$MISE_PROJECT_ROOT/.mise/helpers/common"
+
+rm -rf "$EXT_BUILD_DIR"
diff --git a/.mise/tasks/env b/.mise/tasks/env
new file mode 100755
index 0000000..6673a62
--- /dev/null
+++ b/.mise/tasks/env
@@ -0,0 +1,9 @@
+#!/bin/bash
+# mise description="Show environment"
+set -euo pipefail
+
+source "$MISE_PROJECT_ROOT/.mise/helpers/common"
+
+xcrun sw_vers
+xcrun xcode-select -p
+xcrun xcodebuild -version
diff --git a/.mise/tasks/format b/.mise/tasks/format
new file mode 100755
index 0000000..9e0baca
--- /dev/null
+++ b/.mise/tasks/format
@@ -0,0 +1,5 @@
+#!/bin/bash
+# mise description="Format the project"
+set -euo pipefail
+
+swiftformat $MISE_PROJECT_ROOT
diff --git a/.mise/tasks/lint b/.mise/tasks/lint
new file mode 100755
index 0000000..a7464b4
--- /dev/null
+++ b/.mise/tasks/lint
@@ -0,0 +1,8 @@
+#!/bin/bash
+# mise description="Lint the project"
+set -euo pipefail
+
+source "$MISE_PROJECT_ROOT/.mise/helpers/common"
+
+swiftlint --strict --quiet
+swiftformat . --lint
diff --git a/.mise/tasks/test b/.mise/tasks/test
new file mode 100755
index 0000000..121e79c
--- /dev/null
+++ b/.mise/tasks/test
@@ -0,0 +1,7 @@
+#!/bin/bash
+# mise description="Test the project"
+set -euo pipefail
+
+source "$MISE_PROJECT_ROOT/.mise/helpers/common"
+
+xcrun swift test
diff --git a/.swiftformat b/.swiftformat
new file mode 100644
index 0000000..1c39ff6
--- /dev/null
+++ b/.swiftformat
@@ -0,0 +1,10 @@
+--swiftversion 5.10
+--exclude .build
+--maxwidth 120
+--allman false
+--disable wrapMultilineStatementBraces
+--funcattributes prev-line
+--typeattributes prev-line
+--varattributes prev-line
+--wraparguments before-first
+--wrapparameters before-first
diff --git a/.swiftlint.yml b/.swiftlint.yml
new file mode 100644
index 0000000..0b76b32
--- /dev/null
+++ b/.swiftlint.yml
@@ -0,0 +1,16 @@
+disabled_rules:
+ - trailing_comma
+ - opening_brace
+ - force_try
+
+included:
+ - Sources/
+ - Tests/
+
+identifier_name:
+ excluded:
+ - r
+
+type_name:
+ excluded:
+ - T
diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..23f8687
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,48 @@
+mise := ~/.local/bin/mise
+
+define HELP_BODY
+USAGE: make
+
+SUBCOMMANDS:
+ help Show help.
+ setup Set up development environment.
+ clean Clean build folder.
+ env Show build environment.
+ build Build.
+ test Run tests.
+ format Format source code.
+ autocorrect Autocorrect lint issues if possible.
+ lint Lint source code.
+
+endef
+export HELP_BODY
+
+help:
+ @echo "$$HELP_BODY"
+
+setup:
+ curl "https://mise.run" | sh
+
+clean:
+ @$(mise) run clean
+
+env:
+ @$(mise) run env
+
+build: env
+ @$(mise) run build
+
+test:
+ @$(mise) run test
+
+format:
+ @$(mise) install
+ @$(mise) run format
+
+autocorrect:
+ @$(mise) install
+ @$(mise) run autocorrect
+
+lint:
+ @$(mise) install
+ @$(mise) run lint
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..85eed67
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,27 @@
+// swift-tools-version: 5.10
+
+import PackageDescription
+
+let package = Package(
+ name: "SwiftCommons",
+ platforms: [
+ .macOS(.v10_13),
+ .iOS(.v15),
+ ],
+ products: [
+ .library(
+ name: "SCInject",
+ targets: ["SCInject"]
+ ),
+ ],
+ targets: [
+ .target(
+ name: "SCInject",
+ dependencies: []
+ ),
+ .testTarget(
+ name: "SCInjectTests",
+ dependencies: ["SCInject"]
+ ),
+ ]
+)
diff --git a/Sources/SCInject/Assembler.swift b/Sources/SCInject/Assembler.swift
new file mode 100644
index 0000000..94140e4
--- /dev/null
+++ b/Sources/SCInject/Assembler.swift
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 Marcin Iwanicki and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+
+public final class Assembler {
+ private let container: Container
+
+ public init(container: Container) {
+ self.container = container
+ }
+
+ @discardableResult
+ public func assemble(_ assemblies: [Assembly]) -> Assembler {
+ for assembly in assemblies {
+ assembly.assemble(container)
+ }
+ return self
+ }
+
+ public func resolver() -> Resolver {
+ container
+ }
+}
diff --git a/Sources/SCInject/Assembly.swift b/Sources/SCInject/Assembly.swift
new file mode 100644
index 0000000..b5150d7
--- /dev/null
+++ b/Sources/SCInject/Assembly.swift
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 Marcin Iwanicki and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+
+public protocol Assembly {
+ func assemble(_ registry: Registry)
+}
diff --git a/Sources/SCInject/Container.swift b/Sources/SCInject/Container.swift
new file mode 100644
index 0000000..d69ba28
--- /dev/null
+++ b/Sources/SCInject/Container.swift
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2024 Marcin Iwanicki and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+
+public protocol Container: Registry, Resolver {}
+
+public final class DefaultContainer: Container {
+ private let parent: DefaultContainer?
+ private let lock = NSRecursiveLock()
+ private let defaultScope = Scope.transient
+ private var resolvers: [ResolverIdentifier: ReferenceResolver] = [:]
+
+ public init(parent: DefaultContainer? = nil) {
+ self.parent = parent
+ }
+
+ // MARK: - Registry
+
+ public func register(_ type: T.Type, closure: @escaping (Resolver) -> T) {
+ register(type: type, id: nil, scope: nil, closure: closure)
+ }
+
+ public func register(_ type: T.Type, _ scope: Scope, closure: @escaping (Resolver) -> T) {
+ register(type: type, id: nil, scope: scope, closure: closure)
+ }
+
+ public func register(_ type: T.Type, id: String, closure: @escaping (Resolver) -> T) {
+ register(type: type, id: .init(rawValue: id), scope: nil, closure: closure)
+ }
+
+ public func register(_ type: T.Type, id: String, _ scope: Scope, closure: @escaping (Resolver) -> T) {
+ register(type: type, id: .init(rawValue: id), scope: scope, closure: closure)
+ }
+
+ public func register(_ type: T.Type, id: Identifier, closure: @escaping (Resolver) -> T) {
+ register(type: type, id: id, scope: nil, closure: closure)
+ }
+
+ public func register(_ type: T.Type, id: Identifier, _ scope: Scope, closure: @escaping (Resolver) -> T) {
+ register(type: type, id: id, scope: scope, closure: closure)
+ }
+
+ // MARK: - Resolver
+
+ public func resolve(_ type: T.Type) -> T {
+ guard let instance = tryResolve(type) else {
+ let message = errorMessage("Failed to resolve given type -- TYPE=\(type)")
+ fatalError(message)
+ }
+ return instance
+ }
+
+ public func resolve(_ type: T.Type, id: String) -> T {
+ resolve(type, id: .init(rawValue: id))
+ }
+
+ public func resolve(_ type: T.Type, id: Identifier) -> T {
+ guard let instance = tryResolve(type, id: id) else {
+ let message = errorMessage("Failed to resolve given type -- TYPE=\(type) ID=\(id.rawValue)")
+ fatalError(message)
+ }
+ return instance
+ }
+
+ // MARK: - Public
+
+ public func tryResolve(_ type: T.Type) -> T? {
+ tryResolve(type: type, id: nil, container: self)
+ }
+
+ public func tryResolve(_ type: T.Type, id: String) -> T? {
+ tryResolve(type: type, id: .init(rawValue: id), container: self)
+ }
+
+ public func tryResolve(_ type: T.Type, id: Identifier) -> T? {
+ tryResolve(type: type, id: id, container: self)
+ }
+
+ // MARK: - Private
+
+ private func register(
+ type: T.Type,
+ id: Identifier?,
+ scope: Scope?,
+ closure: @escaping (Resolver) -> T
+ ) {
+ lock.lock(); defer { lock.unlock() }
+ let identifier = identifier(of: type, id: id)
+ if resolvers[identifier] != nil {
+ let message =
+ errorMessage("Given type is already registered -- TYPE=\(type) ID=\(id?.rawValue ?? "nil")")
+ fatalError(message)
+ }
+ resolvers[identifier] = makeResolver(scope ?? defaultScope, closure: closure)
+ }
+
+ private func tryResolve(type: T.Type, id: Identifier? = nil, container: Container) -> T? {
+ lock.lock(); defer { lock.unlock() }
+ if let resolver = resolvers[identifier(of: type, id: id)] {
+ return resolver.resolve(with: container) as? T
+ }
+ if let parent {
+ return parent.tryResolve(type: type, id: id, container: container)
+ }
+ return nil
+ }
+
+ private func makeResolver(_ scope: Scope, closure: @escaping (Resolver) -> some Any) -> ReferenceResolver {
+ switch scope {
+ case .transient:
+ TransientReferenceResolver(factory: closure)
+ case .container:
+ ContainerReferenceResolver(factory: closure)
+ }
+ }
+
+ private func identifier(of type: (some Any).Type, id: Identifier?) -> ResolverIdentifier {
+ ResolverIdentifier(
+ id: id,
+ typeIdentifier: ObjectIdentifier(type),
+ description: String(describing: type)
+ )
+ }
+
+ private struct ResolverIdentifier: Hashable {
+ let id: Identifier?
+ let typeIdentifier: ObjectIdentifier
+ let description: String
+ }
+
+ private func errorMessage(_ message: String) -> String {
+ "SCInjectError - \(message)"
+ }
+}
+
+private protocol ReferenceResolver {
+ func resolve(with resolver: Resolver) -> Any
+}
+
+private final class TransientReferenceResolver: ReferenceResolver {
+ private let factory: (Resolver) -> Any
+
+ init(factory: @escaping (Resolver) -> Any) {
+ self.factory = factory
+ }
+
+ func resolve(with resolver: Resolver) -> Any {
+ factory(resolver)
+ }
+}
+
+private final class ContainerReferenceResolver: ReferenceResolver {
+ private var instance: Any?
+
+ private let factory: (Resolver) -> Any
+
+ init(factory: @escaping (Resolver) -> Any) {
+ self.factory = factory
+ }
+
+ func resolve(with resolver: Resolver) -> Any {
+ if let instance {
+ return instance
+ }
+ let newInstance = factory(resolver)
+ instance = newInstance
+ return newInstance
+ }
+}
diff --git a/Sources/SCInject/Identifier.swift b/Sources/SCInject/Identifier.swift
new file mode 100644
index 0000000..b0d9901
--- /dev/null
+++ b/Sources/SCInject/Identifier.swift
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 Marcin Iwanicki and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+
+public struct Identifier: RawRepresentable, CustomStringConvertible, Hashable {
+ public let rawValue: String
+
+ public init(rawValue string: String) {
+ rawValue = string
+ }
+
+ public var description: String {
+ "SCInject.Identifier(\(rawValue))"
+ }
+}
diff --git a/Sources/SCInject/Registry.swift b/Sources/SCInject/Registry.swift
new file mode 100644
index 0000000..a0dec6f
--- /dev/null
+++ b/Sources/SCInject/Registry.swift
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 Marcin Iwanicki and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+
+public protocol Registry {
+ func register(_ type: T.Type, closure: @escaping (Resolver) -> T)
+ func register(_ type: T.Type, _ scope: Scope, closure: @escaping (Resolver) -> T)
+
+ func register(_ type: T.Type, id: String, closure: @escaping (Resolver) -> T)
+ func register(_ type: T.Type, id: String, _ scope: Scope, closure: @escaping (Resolver) -> T)
+
+ func register(_ type: T.Type, id: Identifier, closure: @escaping (Resolver) -> T)
+ func register(_ type: T.Type, id: Identifier, _ scope: Scope, closure: @escaping (Resolver) -> T)
+}
diff --git a/Sources/SCInject/Resolver.swift b/Sources/SCInject/Resolver.swift
new file mode 100644
index 0000000..580348e
--- /dev/null
+++ b/Sources/SCInject/Resolver.swift
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 Marcin Iwanicki and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+
+public protocol Resolver: AnyObject {
+ func resolve(_ type: T.Type) -> T
+ func resolve(_ type: T.Type, id: String) -> T
+ func resolve(_ type: T.Type, id: Identifier) -> T
+}
diff --git a/Sources/SCInject/Scope.swift b/Sources/SCInject/Scope.swift
new file mode 100644
index 0000000..ad33092
--- /dev/null
+++ b/Sources/SCInject/Scope.swift
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2024 Marcin Iwanicki and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public enum Scope {
+ case transient
+ case container
+}
diff --git a/Tests/SCInject/ContainerTests.swift b/Tests/SCInject/ContainerTests.swift
new file mode 100644
index 0000000..de4f0f8
--- /dev/null
+++ b/Tests/SCInject/ContainerTests.swift
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 Marcin Iwanicki and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+@testable import SCInject
+import XCTest
+
+// swiftlint:disable identifier_name
+final class ContainerTests: XCTestCase {
+ func testRegister_transientClass() {
+ // Given
+ let container = DefaultContainer()
+ container.register(TestClass1.self) { _ in
+ TestClass1(value: "TestClass1_Instance")
+ }
+ container.register(TestClass2.self) { r in
+ TestClass2(value: r.resolve(TestClass1.self))
+ }
+
+ // When
+ let class1 = container.tryResolve(TestClass1.self)
+ let class2 = container.tryResolve(TestClass2.self)
+ let class1_id = container.tryResolve(TestClass1.self, id: "Test")
+ let class2_id = container.tryResolve(TestClass2.self, id: "Test")
+ let class1_id_second = container.tryResolve(TestClass1.self, id: .init(rawValue: "Test"))
+ let class2_id_second = container.tryResolve(TestClass2.self, id: .init(rawValue: "Test"))
+ let class1_second = container.tryResolve(TestClass1.self)
+ let class2_second = container.tryResolve(TestClass2.self)
+
+ // Then
+ XCTAssertNotNil(class1)
+ XCTAssertNotNil(class2)
+ XCTAssertNotNil(class1_second)
+ XCTAssertNotNil(class2_second)
+ XCTAssertNil(class1_id)
+ XCTAssertNil(class2_id)
+ XCTAssertNil(class1_id_second)
+ XCTAssertNil(class2_id_second)
+ XCTAssertTrue(class1 !== class1_second)
+ XCTAssertTrue(class2 !== class2_second)
+ XCTAssertTrue(class2?.value !== class1)
+ }
+
+ func testRegister_transientClassWithId() {
+ let second: Identifier = .init(rawValue: "second")
+ let container = DefaultContainer()
+ container.register(TestClass1.self) { _ in
+ TestClass1(value: "TestClass1_Instance")
+ }
+ container.register(TestClass1.self, id: second) { _ in
+ TestClass1(value: "TestClass1_Second_Instance")
+ }
+ container.register(TestClass2.self) { r in
+ TestClass2(value: r.resolve(TestClass1.self, id: second))
+ }
+
+ // When
+ let class1 = container.tryResolve(TestClass1.self)
+ let class2 = container.tryResolve(TestClass2.self)
+ let class1_id = container.tryResolve(TestClass1.self, id: second)
+ let class2_id = container.tryResolve(TestClass2.self, id: second)
+
+ // Then
+ XCTAssertNotNil(class1)
+ XCTAssertNotNil(class2)
+ XCTAssertNotNil(class1_id)
+ XCTAssertNil(class2_id)
+ XCTAssertEqual(class1_id?.rawValue, "TestClass1_Second_Instance")
+ XCTAssertEqual(class2?.value.rawValue, "TestClass1_Second_Instance")
+ XCTAssertTrue(class1 !== class1_id)
+ XCTAssertTrue(class2?.value !== class1_id)
+ }
+}
+
+// swiftlint:enable identifier_name
diff --git a/Tests/SCInject/TestUtils/Stubs.swift b/Tests/SCInject/TestUtils/Stubs.swift
new file mode 100644
index 0000000..177ff20
--- /dev/null
+++ b/Tests/SCInject/TestUtils/Stubs.swift
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 Marcin Iwanicki and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+
+class TestClass1 {
+ let rawValue: String
+
+ init(value: String) {
+ rawValue = value
+ }
+}
+
+class TestClass2 {
+ let value: TestClass1
+
+ init(value: TestClass1) {
+ self.value = value
+ }
+}