diff --git a/.github/config/.swiftlint.yml b/.github/config/.swiftlint.yml new file mode 100644 index 0000000..acc2239 --- /dev/null +++ b/.github/config/.swiftlint.yml @@ -0,0 +1,8 @@ +child_config: ../../.swiftlint.yml + +disabled_rules: +# E.g: +# - todo + +excluded: +# In case you want to exclude folders for the swiftlint running in the CI \ No newline at end of file diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 2627cc6..4151650 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -1,10 +1,8 @@ ### Integrations workflow ### # -# Runs Danger checks, Swiftlint and tests. -# -# github.token - is automatically assigned to your workflow by Github +# Runs Danger checks and tests. # -# Note: To enable Fastlane tests, make sure that `CommonFastfile` is imported in Fastfile from the shared ios-fastlane repository. +# github.token - is automatically assigned to your workflow by Github # name: Integrations @@ -18,33 +16,34 @@ on: jobs: integrations: # runs-on: macos-latest # use this for repos outside of the STRV Github org - runs-on: [self-hosted, macOS, ios-integrations] + runs-on: [self-hosted, macOS, mobile-ci] timeout-minutes: 10 steps: - # cancel in-progress integrations - not yet supported by Github Actions out of the box + # cancel in-progress integrations - not yet supported by Github Actions out of the box - name: Cancel previous runs uses: styfle/cancel-workflow-action@0.4.1 with: access_token: ${{ github.token }} - # checkout the project repository + # checkout the project repository - name: Checkout uses: actions/checkout@v1 - # install dependencies from Gemfile & CocoaPods - - name: Dependencies - run: | - bundle install + # Install Ruby and Danger via Mise + - name: Install Mise + uses: jdx/mise-action@v2 + with: + install: true + cache: false + experimental: true # run Danger - name: Run Danger - run: bundle exec danger --fail-on-errors=true + run: danger --fail-on-errors=true env: GITHUB_TOKEN: ${{ github.token }} # run tests - name: Run tests - run: bundle exec fastlane tests - env: - DEVICES: "iPhone 13,iPad Air,Apple TV,My Mac,Apple Watch Series 6" + run: swift test --enable-code-coverage diff --git a/.gitignore b/.gitignore index 664114c..a53c9ce 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,6 @@ fastlane/test_output # Ruby gems vendor/ + +# Claude +.claude/settings.local.json diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..211bb0e --- /dev/null +++ b/.mise.toml @@ -0,0 +1,3 @@ +[tools] +ruby = "3.3" +"gem:danger" = "8.2" diff --git a/CHANGELOG.md b/CHANGELOG.md index b545202..0fc24e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,19 @@ __Sections__ - `Removed` for deprecated features removed in this release. - `Fixed` for any bug fixes. +## [1.0.5] + +### Added + +- Support for multiple arguments. Code clean up. +- Using parameter packs for multiple arguments + +## [1.0.4] + +### Fixed + +- Async dependency injection fixes + ## [1.0.3] ### Fixed diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 36cee07..0000000 --- a/Gemfile +++ /dev/null @@ -1,11 +0,0 @@ -# By Jan Schwarz 03/24/2021 -# STRV s.r.o. 2021 -# STRV - -source 'https://rubygems.org' -gem 'bundler', '~> 2.6.3' -gem 'danger', '~> 8.2.0' -gem 'fastlane', '~> 2.226.0' - -plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') -eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 1e1ec5a..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,263 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) - artifactory (3.0.17) - atomos (0.1.3) - aws-eventstream (1.3.1) - aws-partitions (1.1050.0) - aws-sdk-core (3.218.1) - aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.992.0) - aws-sigv4 (~> 1.9) - base64 - jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.98.0) - aws-sdk-core (~> 3, >= 3.216.0) - aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.181.0) - aws-sdk-core (~> 3, >= 3.216.0) - aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.5) - aws-sigv4 (1.11.0) - aws-eventstream (~> 1, >= 1.0.2) - babosa (1.0.4) - base64 (0.2.0) - claide (1.1.0) - claide-plugins (0.9.2) - cork - nap - open4 (~> 1.3) - colored (1.2) - colored2 (3.1.2) - commander (4.6.0) - highline (~> 2.0.0) - cork (0.3.0) - colored2 (~> 3.1) - danger (8.2.3) - claide (~> 1.0) - claide-plugins (>= 0.9.2) - colored2 (~> 3.1) - cork (~> 0.1) - faraday (>= 0.9.0, < 2.0) - faraday-http-cache (~> 2.0) - git (~> 1.7) - kramdown (~> 2.3) - kramdown-parser-gfm (~> 1.0) - no_proxy_fix - octokit (~> 4.7) - terminal-table (>= 1, < 4) - declarative (0.0.20) - digest-crc (0.7.0) - rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20240107) - dotenv (2.8.1) - emoji_regex (3.2.3) - excon (0.112.0) - faraday (1.10.4) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) - faraday (>= 0.8.0) - http-cookie (~> 1.0.0) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-http-cache (2.5.1) - faraday (>= 0.8) - faraday-httpclient (1.0.1) - faraday-multipart (1.1.0) - multipart-post (~> 2.0) - faraday-net_http (1.0.2) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - faraday_middleware (1.2.1) - faraday (~> 1.0) - fastimage (2.4.0) - fastlane (2.217.0) - CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.8, < 3.0.0) - artifactory (~> 3.0) - aws-sdk-s3 (~> 1.0) - babosa (>= 1.0.3, < 2.0.0) - bundler (>= 1.12.0, < 3.0.0) - colored - commander (~> 4.6) - dotenv (>= 2.1.1, < 3.0.0) - emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) - faraday (~> 1.0) - faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 1.0) - fastimage (>= 2.1.0, < 3.0.0) - gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.3) - google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-storage (~> 1.31) - highline (~> 2.0) - http-cookie (~> 1.0.5) - json (< 3.0.0) - jwt (>= 2.1.0, < 3) - mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (>= 2.0.0, < 3.0.0) - naturally (~> 2.2) - optparse (~> 0.1.1) - plist (>= 3.1.0, < 4.0.0) - rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) - simctl (~> 1.6.3) - terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (~> 3) - tty-screen (>= 0.6.3, < 1.0.0) - tty-spinner (>= 0.8.0, < 1.0.0) - word_wrap (~> 1.0.0) - xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) - gh_inspector (1.1.3) - git (1.19.1) - addressable (~> 2.8) - rchardet (~> 1.8) - google-apis-androidpublisher_v3 (0.76.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-core (0.16.0) - addressable (~> 2.5, >= 2.5.1) - googleauth (~> 1.9) - httpclient (>= 2.8.3, < 3.a) - mini_mime (~> 1.0) - mutex_m - representable (~> 3.0) - retriable (>= 2.0, < 4.a) - google-apis-iamcredentials_v1 (0.22.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-playcustomapp_v1 (0.16.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.49.0) - google-apis-core (>= 0.15.0, < 2.a) - google-cloud-core (1.7.1) - google-cloud-env (>= 1.0, < 3.a) - google-cloud-errors (~> 1.0) - google-cloud-env (2.2.1) - faraday (>= 1.0, < 3.a) - google-cloud-errors (1.4.0) - google-cloud-storage (1.55.0) - addressable (~> 2.8) - digest-crc (~> 0.4) - google-apis-core (~> 0.13) - google-apis-iamcredentials_v1 (~> 0.18) - google-apis-storage_v1 (>= 0.42) - google-cloud-core (~> 1.6) - googleauth (~> 1.9) - mini_mime (~> 1.0) - google-logging-utils (0.1.0) - googleauth (1.13.1) - faraday (>= 1.0, < 3.a) - google-cloud-env (~> 2.2) - google-logging-utils (~> 0.1) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - highline (2.0.3) - http-cookie (1.0.8) - domain_name (~> 0.5) - httpclient (2.8.3) - jmespath (1.6.2) - json (2.10.1) - jwt (2.10.1) - base64 - kramdown (2.5.1) - rexml (>= 3.3.9) - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - mini_magick (4.13.2) - mini_mime (1.1.5) - multi_json (1.15.0) - multipart-post (2.4.1) - mutex_m (0.3.0) - nanaimo (0.4.0) - nap (1.1.0) - naturally (2.2.1) - nkf (0.2.0) - no_proxy_fix (0.1.2) - octokit (4.25.1) - faraday (>= 1, < 3) - sawyer (~> 0.9) - open4 (1.3.4) - optparse (0.1.1) - os (1.1.4) - plist (3.7.2) - public_suffix (6.0.1) - rake (13.2.1) - rchardet (1.9.0) - representable (3.2.0) - declarative (< 0.1.0) - trailblazer-option (>= 0.1.1, < 0.2.0) - uber (< 0.2.0) - retriable (3.1.2) - rexml (3.4.1) - rouge (2.0.7) - ruby2_keywords (0.0.5) - rubyzip (2.4.1) - sawyer (0.9.2) - addressable (>= 2.3.5) - faraday (>= 0.17.3, < 3) - security (0.1.3) - signet (0.19.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - simctl (1.6.10) - CFPropertyList - naturally - terminal-notifier (2.0.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - trailblazer-option (0.1.2) - tty-cursor (0.7.1) - tty-screen (0.8.2) - tty-spinner (0.9.3) - tty-cursor (~> 0.7) - uber (0.1.0) - unicode-display_width (2.6.0) - word_wrap (1.0.0) - xcodeproj (1.27.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.4.0) - rexml (>= 3.3.6, < 4.0) - xcpretty (0.3.0) - rouge (~> 2.0.7) - xcpretty-travis-formatter (1.0.1) - xcpretty (~> 0.2, >= 0.0.7) - -PLATFORMS - arm64-darwin-24 - ruby - -DEPENDENCIES - bundler (~> 2.6.3) - danger (~> 8.2.0) - fastlane (~> 2.217.0) - -BUNDLED WITH - 2.6.3 diff --git a/README.md b/README.md index 5dc6917..3326919 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Platforms](https://img.shields.io/badge/Platforms-iOS_iPadOS_macOS_tvOS_watchOS-lightgrey?style=flat-square)](https://img.shields.io/badge/Platforms-iOS_iPadOS_macOS_tvOS_watchOS-lightgrey?style=flat-square) [![Swift](https://img.shields.io/badge/Swift-5.3_5.4_5.5-blue?style=flat-square)](https://img.shields.io/badge/Swift-5.3_5.4_5.5-blue?style=flat-square) -The lightweight library for dependency injection in Swift +The lightweight library for dependency injection in Swift. For detailed API documentation, see the generated DocC documentation in Xcode (Product → Build Documentation) or browse the source code. ## Requirements @@ -29,13 +29,17 @@ If you are new to the concept of Dependency Injection, you can check [Wikipedia] ## Usage -A container is a key component of Dependency Injection. A container manages dependencies of your codebase. First, you register your dependencies within the container identified by either their types, or protocols or classes they conform to or inherit from respectively. Then, you use the container to get (i.e. resolve) instances of the registered dependencies. The class [Container](Sources/Container/Container.swift) represents the Dependency Injection container. +A container is a key component of Dependency Injection. A container manages dependencies of your codebase. First, you register your dependencies within the container identified by either their types, or protocols or classes they conform to or inherit from respectively. Then, you use the container to get (i.e. resolve) instances of the registered dependencies. + +The library provides two container types: +- **`Container`** - Synchronous container for traditional dependency injection +- **`AsyncContainer`** - Asynchronous, actor-based container for Swift concurrency (thread-safe) Other terminology that might be useful: - **[Factory](Sources/Protocols/Registration/DependencyRegistering.swift)** - A function or closure instantiating a dependency - **[Scope](Sources/Models/DependencyScope.swift)** - A scope of a registered dependency can be either `new` or `shared`. When a dependency is registered with `new` scope, a new instance of the dependency is created each time the dependency is resolved from the container. When a dependency is registered with `shared` scope, a new instance of the dependency is created only the first time it is resolved from the container. The created instance is cached and it is returned for all upcoming resolution requests, i.e. it is a singleton -- **[Registration with an argument](Sources/Protocols/Registration/DependencyWithArgumentRegistering.swift)** - All dependencies must be initialized and their initializers often have parameters. Typically, the objects that are passed as the input parameters are resolved from the same container. But you might want to have a registered dependency which requires a parameter in its initializer that can't be registered in the container. In such case, you register the dependency with a variable argument and you specify a value of the argument when the dependency is being resolved; the value is passed as an input parameter to the dependency factory. +- **Registration with arguments** - All dependencies must be initialized and their initializers often have parameters. Typically, the objects that are passed as the input parameters are resolved from the same container. But you might want to have a registered dependency which requires a parameter in its initializer that can't be registered in the container. In such case, you register the dependency with variable arguments (1, 2, or 3 arguments supported) and you specify values of the arguments when the dependency is being resolved; the values are passed as input parameters to the dependency factory. ### Registration @@ -131,7 +135,7 @@ let dependency = container.resolve(type: Dependency.self) let dependency2: Dependency = container.resolve() ``` -Or a dependency registered with an argument like this: +Or a dependency registered with arguments like this: ```swift let container = Container() container.register { container, number in @@ -145,6 +149,15 @@ let dependency = container.resolve(type: Dependency.self, argument: 42) let dependency2: Dependency = container.resolve(argument: 42) ``` +The library also supports 2 and 3 arguments: +```swift +container.register { container, userId, apiKey in + AuthenticatedService(userId: userId, apiKey: apiKey) +} + +let service: AuthenticatedService = container.resolve(argument1: "user123", argument2: "key456") +``` + ### Property wrappers The package contains also two convenient property wrappers `@Injected` and `@LazyInjected`. As long as you are fine with using the `Container.shared` or any other static container instance, you can use the following syntactic sugar to resolve dependencies: @@ -183,7 +196,29 @@ class Object { } } ``` -In the example above the dependencies aren't resolved immediately when an instance of `Object` is initialized but only when the `doStuff` method is called for the first time. +In the example above the dependencies aren't resolved immediately when an instance of `Object` is initialized but only when the `doStuff` method is called for the first time. + +### AsyncContainer + +For Swift concurrency, use `AsyncContainer` which is an actor-based container providing thread-safe dependency injection: + +```swift +let container = AsyncContainer.shared + +// Register dependencies (async) +await container.register { resolver in + await MyService( + apiClient: await resolver.resolve(type: APIClient.self) + ) +} + +// Resolve dependencies (async) +let service: MyService = await container.resolve() +``` + +All `AsyncContainer` operations are `async` and thread-safe. Use it when working with Swift concurrency or when thread safety is required. + +**Note:** Property wrappers (`@Injected` and `@LazyInjected`) work only with `Container`, not `AsyncContainer`. ## Roadmap @@ -194,7 +229,7 @@ In the example above the dependencies aren't resolved immediately when an instan - [x] Convenient property wrapper - [x] Autoregister - [x] SPM package -- [ ] Register an instance with multiple arguments +- [x] Register an instance with multiple arguments +- [x] AsyncContainer for Swift concurrency (thread-safe) - [ ] Container hierarchy -- [ ] Thread-safety - [ ] Detect circular dependencies diff --git a/Sources/Container/Async/AsyncContainer.swift b/Sources/Container/Async/AsyncContainer.swift index 653ca64..abd6b90 100644 --- a/Sources/Container/Async/AsyncContainer.swift +++ b/Sources/Container/Async/AsyncContainer.swift @@ -13,7 +13,7 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin public static let shared: AsyncContainer = .init() private var registrations = [RegistrationIdentifier: AsyncRegistration]() - private var sharedInstances = [RegistrationIdentifier: Any]() + private var sharedInstances = [RegistrationIdentifier: any Sendable]() /// Create new instance of ``AsyncContainer`` public init() {} @@ -52,23 +52,19 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin sharedInstances[registration.identifier] = nil } - // MARK: Register dependency with argument + // MARK: Register dependency with arguments - /// Register a dependency with an argument + /// Register a dependency with variable arguments /// - /// The argument is typically a parameter in an initiliazer of the dependency that is not registered in the same container, - /// therefore, it needs to be passed in `resolve` call - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// The arguments are typically parameters in an initializer of the dependency that are not registered in the same container, + /// therefore, they need to be passed in `resolve` call. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. /// /// - Parameters: /// - type: Type of the dependency to register /// - factory: Closure that is called when the dependency is being resolved - public func register(type: Dependency.Type, factory: @escaping FactoryWithArgument) async { + public func register(type: Dependency.Type, factory: @escaping FactoryWithArguments) async { let registration = AsyncRegistration(type: type, scope: .new, factory: factory) registrations[registration.identifier] = registration @@ -78,46 +74,56 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// Resolve a dependency that was previously registered with `register` method /// - /// If a dependency of the given type with the given argument wasn't registered before this method call + /// If a dependency of the given type wasn't registered before this method call /// the method throws ``ResolutionError.dependencyNotRegistered`` /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument: Argument that will passed as an input parameter to the factory method that was defined with `register` method - public func tryResolve(type: Dependency.Type, argument: Argument) async throws -> Dependency { - let identifier = RegistrationIdentifier(type: type, argument: Argument.self) + public func tryResolve(type: Dependency.Type) async throws -> Dependency { + let identifier = RegistrationIdentifier(type: type) let registration = try getRegistration(with: identifier) - return try await getDependency(from: registration, with: argument) as Dependency + return try await getDependency(from: registration) as Dependency } - /// Resolve a dependency that was previously registered with `register` method + /// Resolve a dependency with variable arguments that was previously registered with `register` method /// - /// If a dependency of the given type wasn't registered before this method call + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// If a dependency of the given type with the given arguments wasn't registered before this method call /// the method throws ``ResolutionError.dependencyNotRegistered`` /// /// - Parameters: /// - type: Type of the dependency that should be resolved - public func tryResolve(type: Dependency.Type) async throws -> Dependency { - let identifier = RegistrationIdentifier(type: type) + /// - arguments: Arguments that will be passed as input parameters to the factory method (Important: only 1-3 arguments supported. Entering more arguments will cause error in runtime.) + public func tryResolve(type: Dependency.Type, arguments: repeat each Argument) async throws -> Dependency { + let identifier = RegistrationIdentifier(type: type, argumentTypes: repeat (each Argument).self) let registration = try getRegistration(with: identifier) - return try await getDependency(from: registration) as Dependency + // Pack arguments into a tuple for storage - this matches how AsyncRegistration expects them + let argumentsTuple = (repeat each arguments) + + return try await getDependency(from: registration, with: argumentsTuple) as Dependency } } // MARK: Private methods private extension AsyncContainer { func getRegistration(with identifier: RegistrationIdentifier) throws -> AsyncRegistration { - guard let registration = registrations[identifier] else { - throw ResolutionError.dependencyNotRegistered( - message: "Dependency of type \(identifier.description) wasn't registered in container \(self)" + if let registration = registrations[identifier] { + return registration + } + + if let matchingIdentifier = registrations.keys.first(where: { $0.typeIdentifier == identifier.typeIdentifier }) { + throw ResolutionError.unmatchingArgumentType( + message: "Registration of type \(matchingIdentifier.description) doesn't accept arguments of type \(identifier.description)" ) } - return registration + throw ResolutionError.dependencyNotRegistered( + message: "Dependency of type \(identifier.description) wasn't registered in container \(self)" + ) } func getDependency(from registration: AsyncRegistration, with argument: (any Sendable)? = nil) async throws -> Dependency { diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index b99695b..59d62d4 100644 --- a/Sources/Container/Sync/Container.swift +++ b/Sources/Container/Sync/Container.swift @@ -8,7 +8,7 @@ import Foundation /// Dependency Injection Container where dependencies are registered and from where they are consequently retrieved (i.e. resolved) -open class Container: DependencyWithArgumentAutoregistering, DependencyAutoregistering, DependencyWithArgumentResolving, @unchecked Sendable { +open class Container: DependencyAutoregistering, DependencyResolving, DependencyRegistering, @unchecked Sendable { /// Shared singleton public static let shared: Container = .init() @@ -48,23 +48,19 @@ open class Container: DependencyWithArgumentAutoregistering, DependencyAutoregis sharedInstances[registration.identifier] = nil } - // MARK: Register dependency with argument, Autoregister dependency with argument + // MARK: Register dependency with arguments - /// Register a dependency with an argument + /// Register a dependency with variable arguments /// - /// The argument is typically a parameter in an initiliazer of the dependency that is not registered in the same container, - /// therefore, it needs to be passed in `resolve` call - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// The arguments are typically parameters in an initializer of the dependency that are not registered in the same container, + /// therefore, they need to be passed in `resolve` call. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. /// /// - Parameters: /// - type: Type of the dependency to register /// - factory: Closure that is called when the dependency is being resolved - open func register(type: Dependency.Type, factory: @escaping FactoryWithArgument) { + open func register(type: Dependency.Type, factory: @escaping FactoryWithArguments) { let registration = Registration(type: type, scope: .new, factory: factory) registrations[registration.identifier] = registration @@ -74,46 +70,56 @@ open class Container: DependencyWithArgumentAutoregistering, DependencyAutoregis /// Resolve a dependency that was previously registered with `register` method /// - /// If a dependency of the given type with the given argument wasn't registered before this method call + /// If a dependency of the given type wasn't registered before this method call /// the method throws ``ResolutionError.dependencyNotRegistered`` /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument: Argument that will passed as an input parameter to the factory method that was defined with `register` method - open func tryResolve(type: Dependency.Type, argument: Argument) throws -> Dependency { - let identifier = RegistrationIdentifier(type: type, argument: Argument.self) + open func tryResolve(type: Dependency.Type) throws -> Dependency { + let identifier = RegistrationIdentifier(type: type) let registration = try getRegistration(with: identifier) - return try getDependency(from: registration, with: argument) as Dependency + return try getDependency(from: registration) as Dependency } - /// Resolve a dependency that was previously registered with `register` method + /// Resolve a dependency with variable arguments that was previously registered with `register` method /// - /// If a dependency of the given type wasn't registered before this method call + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// If a dependency of the given type with the given arguments wasn't registered before this method call /// the method throws ``ResolutionError.dependencyNotRegistered`` /// /// - Parameters: /// - type: Type of the dependency that should be resolved - open func tryResolve(type: Dependency.Type) throws -> Dependency { - let identifier = RegistrationIdentifier(type: type) + /// - arguments: Arguments that will be passed as input parameters to the factory method (Important: only 1-3 arguments supported. Entering more arguments will cause error in runtime.) + open func tryResolve(type: Dependency.Type, arguments: repeat each Argument) throws -> Dependency { + let identifier = RegistrationIdentifier(type: type, argumentTypes: repeat (each Argument).self) let registration = try getRegistration(with: identifier) - return try getDependency(from: registration) as Dependency + // Pack arguments into a tuple for storage - this matches how Registration expects them + let argumentsTuple = (repeat each arguments) + + return try getDependency(from: registration, with: argumentsTuple) as Dependency } } // MARK: Private methods private extension Container { func getRegistration(with identifier: RegistrationIdentifier) throws -> Registration { - guard let registration = registrations[identifier] else { - throw ResolutionError.dependencyNotRegistered( - message: "Dependency of type \(identifier.description) wasn't registered in container \(self)" + if let registration = registrations[identifier] { + return registration + } + + if let matchingIdentifier = registrations.keys.first(where: { $0.typeIdentifier == identifier.typeIdentifier }) { + throw ResolutionError.unmatchingArgumentType( + message: "Registration of type \(matchingIdentifier.description) doesn't accept arguments of type \(identifier.description)" ) } - return registration + throw ResolutionError.dependencyNotRegistered( + message: "Dependency of type \(identifier.description) wasn't registered in container \(self)" + ) } func getDependency(from registration: Registration, with argument: Any? = nil) throws -> Dependency { diff --git a/Sources/Models/Async/AsyncRegistration.swift b/Sources/Models/Async/AsyncRegistration.swift index 0022e2c..b7eedf5 100644 --- a/Sources/Models/Async/AsyncRegistration.swift +++ b/Sources/Models/Async/AsyncRegistration.swift @@ -16,24 +16,28 @@ struct AsyncRegistration: Sendable { let asyncRegistrationFactory: AsyncRegistrationFactory /// Initializer for registrations that don't need any variable argument - init(type: T.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving) async -> T) { + init(type: Dependency.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving) async -> Dependency) { identifier = RegistrationIdentifier(type: type) self.scope = scope asyncRegistrationFactory = { resolver, _ in await factory(resolver) } } - /// Initializer for registrations that expect a variable argument passed to the factory closure when the dependency is being resolved - init(type: T.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, Argument) async -> T) { - let registrationIdentifier = RegistrationIdentifier(type: type, argument: Argument.self) + /// Initializer for registrations that expect variable arguments passed to the factory closure when the dependency is being resolved + /// + /// Uses Swift parameter packs to support 1-3 arguments with a single initializer. Entering more arguments will cause error in runtime. + init(type: Dependency.Type, scope: DependencyScope, factory: @Sendable @escaping (any AsyncDependencyResolving, repeat each Argument) async -> Dependency) { + let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: repeat (each Argument).self) identifier = registrationIdentifier self.scope = scope asyncRegistrationFactory = { resolver, arg in - guard let argument = arg as? Argument else { - throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept an argument of type \(Argument.self)") + guard let arguments = arg as? (repeat each Argument) else { + throw ResolutionError.unmatchingArgumentType( + message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(Swift.type(of: arg))" + ) } - return await factory(resolver, argument) + return await factory(resolver, repeat each arguments) } } } diff --git a/Sources/Models/RegistrationIdentifier.swift b/Sources/Models/RegistrationIdentifier.swift index 9f248e5..8ad361b 100644 --- a/Sources/Models/RegistrationIdentifier.swift +++ b/Sources/Models/RegistrationIdentifier.swift @@ -7,19 +7,39 @@ import Foundation +/// Maximum number of arguments supported for dependency resolution +private enum Constant { + static let maximumArgumentCount = 3 +} + /// Object that uniquely identifies a registered dependency -struct RegistrationIdentifier { +struct RegistrationIdentifier: Sendable { let typeIdentifier: ObjectIdentifier - let argumentIdentifier: ObjectIdentifier? + let argumentIdentifiers: [ObjectIdentifier] - init(type: Dependency.Type, argument _: Argument.Type) { + /// Initializer using parameter packs for any number of argument types + /// + /// - Parameters: + /// - type: Type of the dependency + /// - argumentTypes: Variadic argument types using parameter packs. Only 1-3 arguments are supported. Entering more arguments will cause error in runtime. + init(type: Dependency.Type, argumentTypes _: repeat (each Argument).Type) { typeIdentifier = ObjectIdentifier(type) - argumentIdentifier = ObjectIdentifier(type) + + var identifiers: [ObjectIdentifier] = [] + repeat identifiers.append(ObjectIdentifier((each Argument).self)) + + precondition( + identifiers.count <= Constant.maximumArgumentCount, + "Maximum number of arguments is \(Constant.maximumArgumentCount). Got \(identifiers.count)." + ) + + argumentIdentifiers = identifiers } + /// Convenience initializer for dependencies without arguments init(type: Dependency.Type) { typeIdentifier = ObjectIdentifier(type) - argumentIdentifier = nil + argumentIdentifiers = [] } } @@ -29,9 +49,14 @@ extension RegistrationIdentifier: Hashable {} // MARK: Debug information extension RegistrationIdentifier: CustomStringConvertible { var description: String { - """ + let argumentsDescription: String = if argumentIdentifiers.isEmpty { + "nil" + } else { + argumentIdentifiers.map(\.debugDescription).joined(separator: ", ") + } + return """ Type: \(typeIdentifier.debugDescription) - Argument: \(argumentIdentifier?.debugDescription ?? "nil") + Arguments: \(argumentsDescription) """ } } diff --git a/Sources/Models/Sync/Registration.swift b/Sources/Models/Sync/Registration.swift index 84e1cc2..112a06b 100644 --- a/Sources/Models/Sync/Registration.swift +++ b/Sources/Models/Sync/Registration.swift @@ -11,27 +11,31 @@ import Foundation struct Registration { let identifier: RegistrationIdentifier let scope: DependencyScope - let factory: (DependencyWithArgumentResolving, Any?) throws -> Any + let factory: (DependencyResolving, Any?) throws -> Any /// Initializer for registrations that don't need any variable argument - init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyResolving) -> T) { + init(type: Dependency.Type, scope: DependencyScope, factory: @escaping (DependencyResolving) -> Dependency) { identifier = RegistrationIdentifier(type: type) self.scope = scope self.factory = { resolver, _ in factory(resolver) } } - /// Initializer for registrations that expect a variable argument passed to the factory closure when the dependency is being resolved - init(type: T.Type, scope: DependencyScope, factory: @escaping (DependencyWithArgumentResolving, Argument) -> T) { - let registrationIdentifier = RegistrationIdentifier(type: type, argument: Argument.self) + /// Initializer for registrations that expect variable arguments passed to the factory closure when the dependency is being resolved + /// + /// Uses Swift parameter packs to support 1-3 arguments with a single initializer. Entering more arguments will cause error in runtime. + init(type: Dependency.Type, scope: DependencyScope, factory: @escaping (DependencyResolving, repeat each Argument) -> Dependency) { + let registrationIdentifier = RegistrationIdentifier(type: type, argumentTypes: repeat (each Argument).self) identifier = registrationIdentifier self.scope = scope self.factory = { resolver, arg in - guard let argument = arg as? Argument else { - throw ResolutionError.unmatchingArgumentType(message: "Registration of type \(registrationIdentifier.description) doesn't accept an argument of type \(Argument.self)") + guard let arguments = arg as? (repeat each Argument) else { + throw ResolutionError.unmatchingArgumentType( + message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(Swift.type(of: arg))" + ) } - return factory(resolver, argument) + return factory(resolver, repeat each arguments) } } } diff --git a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift index 20a4f1f..4c8282b 100644 --- a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift +++ b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift @@ -9,11 +9,11 @@ import Foundation /// A type that is able to register a dependency public protocol AsyncDependencyRegistering { - /// Factory closure that instantiates the required dependency + /// Factory closure that instantiates the required dependency without arguments typealias Factory = @Sendable (any AsyncDependencyResolving) async -> Dependency - /// Factory closure that instantiates the required dependency with the given variable argument - typealias FactoryWithArgument = @Sendable (any AsyncDependencyResolving, Argument) async -> Dependency + /// Factory closure that instantiates the required dependency with variable arguments (1-3 supported) + typealias FactoryWithArguments = @Sendable (any AsyncDependencyResolving, repeat each Argument) async -> Dependency /// Register a dependency /// @@ -23,21 +23,17 @@ public protocol AsyncDependencyRegistering { /// - factory: Closure that is called when the dependency is being resolved func register(type: Dependency.Type, in scope: DependencyScope, factory: @escaping Factory) async - /// Register a dependency with a variable argument + /// Register a dependency with variable arguments /// - /// The argument is typically a parameter in an initiliazer of the dependency that is not registered in the same resolver (i.e. container), - /// therefore, it needs to be passed in `resolve` call - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// The arguments are typically parameters in an initializer of the dependency that are not registered in the same resolver (i.e. container), + /// therefore, they need to be passed in `resolve` call. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. /// /// - Parameters: /// - type: Type of the dependency to register /// - factory: Closure that is called when the dependency is being resolved - func register(type: Dependency.Type, factory: @escaping FactoryWithArgument) async + func register(type: Dependency.Type, factory: @escaping FactoryWithArguments) async } // MARK: Overloaded factory methods @@ -49,15 +45,6 @@ public extension AsyncDependencyRegistering { DependencyScope.shared } - /// Register a dependency in the default ``DependencyScope``, i.e. in the `shared` scope - /// - /// - Parameters: - /// - type: Type of the dependency to register - /// - factory: Closure that is called when the dependency is being resolved - func register(type: Dependency.Type, factory: @escaping Factory) async { - await register(type: type, in: Self.defaultScope, factory: factory) - } - /// Register a dependency with an implicit type determined by the factory closure return type /// /// - Parameters: @@ -67,28 +54,16 @@ public extension AsyncDependencyRegistering { await register(type: Dependency.self, in: scope, factory: factory) } - /// Register a dependency with an implicit type determined by the factory closure return type and in the default ``DependencyScope``, i.e. in the `shared` scope - /// - /// - Parameters: - /// - factory: Closure that is called when the dependency is being resolved - func register(factory: @escaping Factory) async { - await register(type: Dependency.self, in: Self.defaultScope, factory: factory) - } - - /// Register a dependency with a variable argument. The type of the dependency is determined implicitly based on the factory closure return type - /// - /// The argument is typically a parameter in an initializer of the dependency that is not registered in the same resolver (i.e. container), - /// therefore, it needs to be passed in `resolve` call + /// Register a dependency with variable arguments. The type of the dependency is determined implicitly based on the factory closure return type /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// The arguments are typically parameters in an initializer of the dependency that are not registered in the same resolver (i.e. container), + /// therefore, they need to be passed in `resolve` call. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. /// /// - Parameters: /// - factory: Closure that is called when the dependency is being resolved - func register(factory: @escaping FactoryWithArgument) async { + func register(factory: @escaping FactoryWithArguments) async { await register(type: Dependency.self, factory: factory) } } diff --git a/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift b/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift index 3146f41..7ce3339 100644 --- a/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyAutoregistering.swift @@ -7,208 +7,318 @@ import Foundation -/// A type that is able to register a dependency with a given initializer instead of a factory closure. All the initializer's parameters must be resolvable from the same container +/// A type that is able to register a dependency with a given initializer instead of a factory closure. All the initializer's parameters must be resolvable from the same container, or can be passed as variable arguments during resolution public protocol DependencyAutoregistering: DependencyRegistering { - /// Autoregister a dependency with the provided initializer method that has no parameters + /// Autoregister a dependency with the provided initializer method. All parameters of the initializer + /// must be dependencies that are registered within the same container. + /// + /// Uses Swift parameter packs to support any number of parameters with a single method signature. /// /// - Parameters: /// - type: Type of the dependency to register /// - scope: Scope of the dependency. If `.new` is used, the initializer is called on each `resolve` call. If `.shared` is used, the initializer is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( + func autoregister( type: Dependency.Type, in scope: DependencyScope, - initializer: @escaping () -> Dependency + initializer: @escaping (repeat each Parameter) -> Dependency ) - /// Autoregister a dependency with the provided initializer method that has one parameter which is a dependency that is registered within the same container + // MARK: Autoregister with variable argument + + /// Autoregister a dependency with a variable argument and with the provided initializer method that has one parameter where the variable argument is passed. + /// This registration method doesn't have any scope parameter for a reason - the container should always return a new instance for dependencies with arguments. + /// + /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. + /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. + /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? + /// Shared instances are typically not dependent on variable input parameters by definition. + /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. /// /// - Parameters: /// - type: Type of the dependency to register - /// - scope: Scope of the dependency. If `.new` is used, the initializer is called on each `resolve` call. If `.shared` is used, the initializer is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton + /// - argument: Type of the variable argument /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( + func autoregister( type: Dependency.Type, - in scope: DependencyScope, - initializer: @escaping (Parameter) -> Dependency + argument: Argument.Type, + initializer: @escaping (Argument) -> Dependency ) - /// Autoregister a dependency with the provided initializer method that has two parameters which are dependencies that are registered within the same container + /// Autoregister a dependency with a variable argument and with the provided initializer method that has two parameters; the first is the variable argument, the second is a dependency that is registered within the same container. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. + /// + /// The `Argument` and `Parameter` are both parameters of the given initializer. + /// However, `Parameter` is a dependency registered in the same resolver (i.e. container), + /// whereas `Argument` is not registered in the same container and it is typically variable, + /// therefore, it needs to be handled separately /// /// - Parameters: /// - type: Type of the dependency to register - /// - scope: Scope of the dependency. If `.new` is used, the initializer is called on each `resolve` call. If `.shared` is used, the initializer is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton + /// - argument: Type of the variable argument /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( + func autoregister( type: Dependency.Type, - in scope: DependencyScope, - initializer: @escaping (Parameter1, Parameter2) -> Dependency + argument: Argument.Type, + initializer: @escaping (Argument, Parameter) -> Dependency ) - /// Autoregister a dependency with the provided initializer method that has three parameters which are dependencies that are registered within the same container + /// Autoregister a dependency with a variable argument and with the provided initializer method that has two parameters; the first is a dependency that is registered + /// within the same container, the second is the variable argument. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. + /// + /// The `Argument` and `Parameter` are both parameters of the given initializer. + /// However, `Parameter` is a dependency registered in the same resolver (i.e. container), + /// whereas `Argument` is not registered in the same container and it is typically variable, + /// therefore, it needs to be handled separately /// /// - Parameters: /// - type: Type of the dependency to register - /// - scope: Scope of the dependency. If `.new` is used, the initializer is called on each `resolve` call. If `.shared` is used, the initializer is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton + /// - argument: Type of the variable argument /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( + func autoregister( type: Dependency.Type, - in scope: DependencyScope, - initializer: @escaping (Parameter1, Parameter2, Parameter3) -> Dependency + argument: Argument.Type, + initializer: @escaping (Parameter, Argument) -> Dependency ) - /// Autoregister a dependency with the provided initializer method that has four parameters which are dependencies that are registered within the same container + /// Autoregister a dependency with a variable argument and with the provided initializer method that has three parameters; the first is the variable argument, the second and the third are dependencies that are registered within the same container. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. + /// + /// The `Argument`, `Parameter1` and `Parameter2` are parameters of the given initializer. + /// However, `Parameter1` and `Parameter2` are dependencies registered in the same resolver (i.e. container), + /// whereas `Argument` is not registered in the same container and it is typically variable, + /// therefore, it needs to be handled separately /// /// - Parameters: /// - type: Type of the dependency to register - /// - scope: Scope of the dependency. If `.new` is used, the initializer is called on each `resolve` call. If `.shared` is used, the initializer is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton + /// - argument: Type of the variable argument /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( + func autoregister( type: Dependency.Type, - in scope: DependencyScope, - initializer: @escaping (Parameter1, Parameter2, Parameter3, Parameter4) -> Dependency + argument: Argument.Type, + initializer: @escaping (Argument, Parameter1, Parameter2) -> Dependency ) - /// Autoregister a dependency with the provided initializer method that has five parameters which are dependencies that are registered within the same container + /// Autoregister a dependency with a variable argument and with the provided initializer method that has three parameters; the first and the third are dependencies that + /// are registered within the same container, the second is the variable argument. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. + /// + /// The `Argument`, `Parameter1` and `Parameter2` are parameters of the given initializer. + /// However, `Parameter1` and `Parameter2` are dependencies registered in the same resolver (i.e. container), + /// whereas `Argument` is not registered in the same container and it is typically variable, + /// therefore, it needs to be handled separately /// /// - Parameters: /// - type: Type of the dependency to register - /// - scope: Scope of the dependency. If `.new` is used, the initializer is called on each `resolve` call. If `.shared` is used, the initializer is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton + /// - argument: Type of the variable argument /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( + func autoregister( type: Dependency.Type, - in scope: DependencyScope, - initializer: @escaping (Parameter1, Parameter2, Parameter3, Parameter4, Parameter5) -> Dependency + argument: Argument.Type, + initializer: @escaping (Parameter1, Argument, Parameter2) -> Dependency + ) + + /// Autoregister a dependency with a variable argument and with the provided initializer method that has three parameters; the first and the second are dependencies + /// that are registered within the same container, the third is the variable argument. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. + /// + /// The `Argument`, `Parameter1` and `Parameter2` are parameters of the given initializer. + /// However, `Parameter1` and `Parameter2` are dependencies registered in the same resolver (i.e. container), + /// whereas `Argument` is not registered in the same container and it is typically variable, + /// therefore, it needs to be handled separately + /// + /// - Parameters: + /// - type: Type of the dependency to register + /// - argument: Type of the variable argument + /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container + func autoregister( + type: Dependency.Type, + argument: Argument.Type, + initializer: @escaping (Parameter1, Parameter2, Argument) -> Dependency ) } // MARK: Default implementation public extension DependencyAutoregistering { - /// Autoregister a dependency with the provided initializer method that has no parameters + /// Autoregister a dependency with the provided initializer method. All parameters of the initializer + /// must be dependencies that are registered within the same container. + /// + /// Uses Swift parameter packs to support any number of parameters with a single method signature. /// /// - Parameters: /// - type: Type of the dependency to register. Default value is implicitly inferred from the initializer return type /// - scope: Scope of the dependency. If `.new` is used, the initializer is called on each `resolve` call. If `.shared` is used, the initializer is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton. The default value is `.shared` /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( + func autoregister( type: Dependency.Type = Dependency.self, in scope: DependencyScope = Self.defaultScope, - initializer: @escaping () -> Dependency + initializer: @escaping (repeat each Parameter) -> Dependency ) { - let factory: Factory = { _ in - initializer() + let factory: Factory = { resolver in + initializer(repeat resolver.resolve(type: (each Parameter).self)) } register(type: type, in: scope, factory: factory) } - /// Autoregister a dependency with the provided initializer method that has one parameter which is a dependency that is registered within the same container + // MARK: Default implementation for autoregister with variable argument + + /// Autoregister a dependency with a variable argument and with the provided initializer method that has just one parameter where the variable argument is passed. + /// This registration method doesn't have any scope parameter for a reason - the container should always return a new instance for dependencies with arguments. /// /// - Parameters: /// - type: Type of the dependency to register. Default value is implicitly inferred from the initializer return type - /// - scope: Scope of the dependency. If `.new` is used, the initializer is called on each `resolve` call. If `.shared` is used, the initializer is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton. The default value is `.shared` + /// - argument: Type of the variable argument /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( + func autoregister( type: Dependency.Type = Dependency.self, - in scope: DependencyScope = Self.defaultScope, - initializer: @escaping (Parameter) -> Dependency + argument: Argument.Type, + initializer: @escaping (Argument) -> Dependency ) { - let factory: Factory = { resolver in + let factory: FactoryWithArguments = { _, argument in + initializer(argument) + } + + register(type: type, factory: factory) + } + + /// Autoregister a dependency with a variable argument and with the provided initializer method that has two parameters; the first is the variable argument, the second + /// is a dependency that is registered within the same container. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. + /// + /// The `Argument` and `Parameter` are both parameters of the given initializer. + /// However, `Parameter` is a dependency registered in the same resolver (i.e. container), + /// whereas `Argument` is not registered in the same container and it is typically variable, + /// therefore, it needs to be handled separately + /// + /// - Parameters: + /// - type: Type of the dependency to register. Default value is implicitly inferred from the initializer return type + /// - argument: Type of the variable argument + /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container + func autoregister( + type: Dependency.Type = Dependency.self, + argument: Argument.Type, + initializer: @escaping (Argument, Parameter) -> Dependency + ) { + let factory: FactoryWithArguments = { resolver, argument in initializer( + argument, resolver.resolve(type: Parameter.self) ) } - register(type: type, in: scope, factory: factory) + register(type: type, factory: factory) } - /// Autoregister a dependency with the provided initializer method that has two parameters which are dependencies that are registered within the same container + /// Autoregister a dependency with a variable argument and with the provided initializer method that has two parameters; the first is a dependency that is registered within the same container, the second is the variable argument. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. + /// + /// The `Argument` and `Parameter` are both parameters of the given initializer. + /// However, `Parameter` is a dependency registered in the same resolver (i.e. container), + /// whereas `Argument` is not registered in the same container and it is typically variable, + /// therefore, it needs to be handled separately /// /// - Parameters: /// - type: Type of the dependency to register. Default value is implicitly inferred from the initializer return type - /// - scope: Scope of the dependency. If `.new` is used, the initializer is called on each `resolve` call. If `.shared` is used, the initializer is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton. The default value is `.shared` + /// - argument: Type of the variable argument /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( + func autoregister( type: Dependency.Type = Dependency.self, - in scope: DependencyScope = Self.defaultScope, - initializer: @escaping (Parameter1, Parameter2) -> Dependency + argument: Argument.Type, + initializer: @escaping (Parameter, Argument) -> Dependency ) { - let factory: Factory = { resolver in + let factory: FactoryWithArguments = { resolver, argument in initializer( - resolver.resolve(type: Parameter1.self), - resolver.resolve(type: Parameter2.self) + resolver.resolve(type: Parameter.self), + argument ) } - register(type: type, in: scope, factory: factory) + register(type: type, factory: factory) } - /// Autoregister a dependency with the provided initializer method that has three parameters which are dependencies that are registered within the same container + /// Autoregister a dependency with a variable argument and with the provided initializer method that has three parameters; the first is the variable argument, the second and the third are dependencies that are registered within the same container. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. + /// + /// The `Argument`, `Parameter1` and `Parameter2` are parameters of the given initializer. + /// However, `Parameter1` and `Parameter2` are dependencies registered in the same resolver (i.e. container), + /// whereas `Argument` is not registered in the same container and it is typically variable, + /// therefore, it needs to be handled separately /// /// - Parameters: /// - type: Type of the dependency to register. Default value is implicitly inferred from the initializer return type - /// - scope: Scope of the dependency. If `.new` is used, the initializer is called on each `resolve` call. If `.shared` is used, the initializer is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton. The default value is `.shared` + /// - argument: Type of the variable argument /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( + func autoregister( type: Dependency.Type = Dependency.self, - in scope: DependencyScope = Self.defaultScope, - initializer: @escaping (Parameter1, Parameter2, Parameter3) -> Dependency + argument: Argument.Type, + initializer: @escaping (Argument, Parameter1, Parameter2) -> Dependency ) { - let factory: Factory = { resolver in + let factory: FactoryWithArguments = { resolver, argument in initializer( + argument, resolver.resolve(type: Parameter1.self), - resolver.resolve(type: Parameter2.self), - resolver.resolve(type: Parameter3.self) + resolver.resolve(type: Parameter2.self) ) } - register(type: type, in: scope, factory: factory) + register(type: type, factory: factory) } - /// Autoregister a dependency with the provided initializer method that has four parameters which are dependencies that are registered within the same container + /// Autoregister a dependency with a variable argument and with the provided initializer method that has three parameters; the first and the third are dependencies that are registered within the same container, the second is the variable argument. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. + /// + /// The `Argument`, `Parameter1` and `Parameter2` are parameters of the given initializer. + /// However, `Parameter1` and `Parameter2` are dependencies registered in the same resolver (i.e. container), + /// whereas `Argument` is not registered in the same container and it is typically variable, + /// therefore, it needs to be handled separately /// /// - Parameters: /// - type: Type of the dependency to register. Default value is implicitly inferred from the initializer return type - /// - scope: Scope of the dependency. If `.new` is used, the initializer is called on each `resolve` call. If `.shared` is used, the initializer is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton. The default value is `.shared` + /// - argument: Type of the variable argument /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( + func autoregister( type: Dependency.Type = Dependency.self, - in scope: DependencyScope = Self.defaultScope, - initializer: @escaping (Parameter1, Parameter2, Parameter3, Parameter4) -> Dependency + argument: Argument.Type, + initializer: @escaping (Parameter1, Argument, Parameter2) -> Dependency ) { - let factory: Factory = { resolver in + let factory: FactoryWithArguments = { resolver, argument in initializer( resolver.resolve(type: Parameter1.self), - resolver.resolve(type: Parameter2.self), - resolver.resolve(type: Parameter3.self), - resolver.resolve(type: Parameter4.self) + argument, + resolver.resolve(type: Parameter2.self) ) } - register(type: type, in: scope, factory: factory) + register(type: type, factory: factory) } - /// Autoregister a dependency with the provided initializer method that has five parameters which are dependencies that are registered within the same container + /// Autoregister a dependency with a variable argument and with the provided initializer method that has three parameters; the first and the second are dependencies that are registered within the same container, the third is the variable argument. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. + /// + /// The `Argument`, `Parameter1` and `Parameter2` are parameters of the given initializer. + /// However, `Parameter1` and `Parameter2` are dependencies registered in the same resolver (i.e. container), + /// whereas `Argument` is not registered in the same container and it is typically variable, + /// therefore, it needs to be handled separately /// /// - Parameters: /// - type: Type of the dependency to register. Default value is implicitly inferred from the initializer return type - /// - scope: Scope of the dependency. If `.new` is used, the initializer is called on each `resolve` call. If `.shared` is used, the initializer is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton. The default value is `.shared` + /// - argument: Type of the variable argument /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( + func autoregister( type: Dependency.Type = Dependency.self, - in scope: DependencyScope = Self.defaultScope, - initializer: @escaping (Parameter1, Parameter2, Parameter3, Parameter4, Parameter5) -> Dependency + argument: Argument.Type, + initializer: @escaping (Parameter1, Parameter2, Argument) -> Dependency ) { - let factory: Factory = { resolver in + let factory: FactoryWithArguments = { resolver, argument in initializer( resolver.resolve(type: Parameter1.self), resolver.resolve(type: Parameter2.self), - resolver.resolve(type: Parameter3.self), - resolver.resolve(type: Parameter4.self), - resolver.resolve(type: Parameter5.self) + argument ) } - register(type: type, in: scope, factory: factory) + register(type: type, factory: factory) } } diff --git a/Sources/Protocols/Registration/Sync/DependencyRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyRegistering.swift index 733d4bd..7c4f932 100644 --- a/Sources/Protocols/Registration/Sync/DependencyRegistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyRegistering.swift @@ -9,9 +9,12 @@ import Foundation /// A type that is able to register a dependency public protocol DependencyRegistering { - /// Factory closure that instantiates the required dependency + /// Factory closure that instantiates the required dependency without arguments typealias Factory = (DependencyResolving) -> Dependency + /// Factory closure that instantiates the required dependency with variable arguments (1-3 supported) + typealias FactoryWithArguments = (DependencyResolving, repeat each Argument) -> Dependency + /// Register a dependency /// /// - Parameters: @@ -19,6 +22,18 @@ public protocol DependencyRegistering { /// - scope: Scope of the dependency. If `.new` is used, the `factory` closure is called on each `resolve` call. If `.shared` is used, the `factory` closure is called only the first time, the instance is cached and it is returned for all subsequent `resolve` calls, i.e. it is a singleton /// - factory: Closure that is called when the dependency is being resolved func register(type: Dependency.Type, in scope: DependencyScope, factory: @escaping Factory) + + /// Register a dependency with variable arguments + /// + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// The arguments are typically parameters in an initializer of the dependency that are not registered in the same resolver (i.e. container), + /// therefore, they need to be passed in `resolve` call. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. + /// + /// - Parameters: + /// - type: Type of the dependency to register + /// - factory: Closure that is called when the dependency is being resolved + func register(type: Dependency.Type, factory: @escaping FactoryWithArguments) } // MARK: Overloaded factory methods @@ -30,15 +45,6 @@ public extension DependencyRegistering { DependencyScope.shared } - /// Register a dependency in the default ``DependencyScope``, i.e. in the `shared` scope - /// - /// - Parameters: - /// - type: Type of the dependency to register - /// - factory: Closure that is called when the dependency is being resolved - func register(type: Dependency.Type, factory: @escaping Factory) { - register(type: type, in: Self.defaultScope, factory: factory) - } - /// Register a dependency with an implicit type determined by the factory closure return type /// /// - Parameters: @@ -48,12 +54,17 @@ public extension DependencyRegistering { register(type: Dependency.self, in: scope, factory: factory) } - /// Register a dependency with an implicit type determined by the factory closure return type and in the default ``DependencyScope``, i.e. in the `shared` scope + /// Register a dependency with variable arguments. The type of the dependency is determined implicitly based on the factory closure return type + /// + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// The arguments are typically parameters in an initializer of the dependency that are not registered in the same resolver (i.e. container), + /// therefore, they need to be passed in `resolve` call. This registration method doesn't have any scope parameter for a reason - the container + /// should always return a new instance for dependencies with arguments. /// /// - Parameters: /// - factory: Closure that is called when the dependency is being resolved - func register(factory: @escaping Factory) { - register(type: Dependency.self, in: Self.defaultScope, factory: factory) + func register(factory: @escaping FactoryWithArguments) { + register(type: Dependency.self, factory: factory) } } diff --git a/Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift deleted file mode 100644 index 10aae96..0000000 --- a/Sources/Protocols/Registration/Sync/DependencyWithArgumentAutoregistering.swift +++ /dev/null @@ -1,344 +0,0 @@ -// -// DependencyWithArgumentAutoregistering.swift -// -// -// Created by Jan Schwarz on 05.08.2021. -// - -import Foundation - -/// A type that is able to register a dependency that needs a variable argument in order to be resolved later. The dependency is registered with a given initializer instead of a factory closure. All the initializer's parameters must be resolvable from the same container -public protocol DependencyWithArgumentAutoregistering: DependencyWithArgumentRegistering { - // MARK: Initializer with a variable argument and no other parameter - - /// Autoregister a dependency with a variable argument and with the provided initializer method that has one parameter where the variable argument is passed - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - type: Type of the dependency to register - /// - argument: Type of the variable argument - /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( - type: Dependency.Type, - argument: Argument.Type, - initializer: @escaping (Argument) -> Dependency - ) - - // MARK: Initializer with a variable argument and 1 parameter - - /// Autoregister a dependency with a variable argument and with the provided initializer method that has two parameters; the first is the variable argument, the second is a dependency that is registered within the same container - /// - /// The `Argument` and `Parameter` are both parameters of the given initializer. - /// However, `Parameter` is a dependency registered in the same resolver (i.e. container), - /// whereas `Argument` is not registered in the same container and it is typically variable, - /// therefore, it needs to be handled separately - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - type: Type of the dependency to register - /// - argument: Type of the variable argument - /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( - type: Dependency.Type, - argument: Argument.Type, - initializer: @escaping (Argument, Parameter) -> Dependency - ) - - /// Autoregister a dependency with a variable argument and with the provided initializer method that has two parameters; the first is a dependency that is registered within the same container, the second is the variable argument - /// - /// The `Argument` and `Parameter` are both parameters of the given initializer. - /// However, `Parameter` is a dependency registered in the same resolver (i.e. container), - /// whereas `Argument` is not registered in the same container and it is typically variable, - /// therefore, it needs to be handled separately - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - type: Type of the dependency to register - /// - argument: Type of the variable argument - /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( - type: Dependency.Type, - argument: Argument.Type, - initializer: @escaping (Parameter, Argument) -> Dependency - ) - - // MARK: Initializer with a variable argument and 2 parameters - - /// Autoregister a dependency with a variable argument and with the provided initializer method that has three parameters; the first is the variable argument, the second and the third are dependencies that are registered within the same container - /// - /// The `Argument`, `Parameter1` and `Parameter2` are parameters of the given initializer. - /// However, `Parameter1` and `Parameter2` are dependencies registered in the same resolver (i.e. container), - /// whereas `Argument` is not registered in the same container and it is typically variable, - /// therefore, it needs to be handled separately - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - type: Type of the dependency to register - /// - argument: Type of the variable argument - /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( - type: Dependency.Type, - argument: Argument.Type, - initializer: @escaping (Argument, Parameter1, Parameter2) -> Dependency - ) - - /// Autoregister a dependency with a variable argument and with the provided initializer method that has three parameters; the first and the third are dependencies that are registered within the same container, the second is the variable argument - /// - /// The `Argument`, `Parameter1` and `Parameter2` are parameters of the given initializer. - /// However, `Parameter1` and `Parameter2` are dependencies registered in the same resolver (i.e. container), - /// whereas `Argument` is not registered in the same container and it is typically variable, - /// therefore, it needs to be handled separately - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - type: Type of the dependency to register - /// - argument: Type of the variable argument - /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( - type: Dependency.Type, - argument: Argument.Type, - initializer: @escaping (Parameter1, Argument, Parameter2) -> Dependency - ) - - /// Autoregister a dependency with a variable argument and with the provided initializer method that has three parameters; the first and the second are dependencies that are registered within the same container, the third is the variable argument - /// - /// The `Argument`, `Parameter1` and `Parameter2` are parameters of the given initializer. - /// However, `Parameter1` and `Parameter2` are dependencies registered in the same resolver (i.e. container), - /// whereas `Argument` is not registered in the same container and it is typically variable, - /// therefore, it needs to be handled separately - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - type: Type of the dependency to register - /// - argument: Type of the variable argument - /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( - type: Dependency.Type, - argument: Argument.Type, - initializer: @escaping (Parameter1, Parameter2, Argument) -> Dependency - ) -} - -// MARK: Default implementation for an initializer with a variable argument -public extension DependencyWithArgumentAutoregistering { - /// Autoregister a dependency with a variable argument and with the provided initializer method that has just one parameter where the variable argument is passed - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - type: Type of the dependency to register. Default value is implicitly inferred from the initializer return type - /// - argument: Type of the variable argument - /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( - type: Dependency.Type = Dependency.self, - argument: Argument.Type, - initializer: @escaping (Argument) -> Dependency - ) { - let factory: FactoryWithArgument = { _, argument in - initializer( - argument - ) - } - - register(type: type, factory: factory) - } -} - -// MARK: Default implementation for an initializer with a variable argument and 1 parameter -public extension DependencyWithArgumentAutoregistering { - /// Autoregister a dependency with a variable argument and with the provided initializer method that has two parameters; the first is the variable argument, the second is a dependency that is registered within the same container - /// - /// The `Argument` and `Parameter` are both parameters of the given initializer. - /// However, `Parameter` is a dependency registered in the same resolver (i.e. container), - /// whereas `Argument` is not registered in the same container and it is typically variable, - /// therefore, it needs to be handled separately - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - type: Type of the dependency to register. Default value is implicitly inferred from the initializer return type - /// - argument: Type of the variable argument - /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( - type: Dependency.Type = Dependency.self, - argument: Argument.Type, - initializer: @escaping (Argument, Parameter) -> Dependency - ) { - let factory: FactoryWithArgument = { resolver, argument in - initializer( - argument, - resolver.resolve(type: Parameter.self) - ) - } - - register(type: type, factory: factory) - } - - /// Autoregister a dependency with a variable argument and with the provided initializer method that has two parameters; the first is a dependency that is registered within the same container, the second is the variable argument - /// - /// The `Argument` and `Parameter` are both parameters of the given initializer. - /// However, `Parameter` is a dependency registered in the same resolver (i.e. container), - /// whereas `Argument` is not registered in the same container and it is typically variable, - /// therefore, it needs to be handled separately - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - type: Type of the dependency to register. Default value is implicitly inferred from the initializer return type - /// - argument: Type of the variable argument - /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister(type: Dependency.Type = Dependency.self, argument: Argument.Type, initializer: @escaping (Parameter, Argument) -> Dependency) { - let factory: FactoryWithArgument = { resolver, argument in - initializer( - resolver.resolve(type: Parameter.self), - argument - ) - } - - register(type: type, factory: factory) - } -} - -// MARK: Default implementation for an initializer with a variable argument and 2 parameters -public extension DependencyWithArgumentAutoregistering { - /// Autoregister a dependency with a variable argument and with the provided initializer method that has three parameters; the first is the variable argument, the second and the third are dependencies that are registered within the same container - /// - /// The `Argument`, `Parameter1` and `Parameter2` are parameters of the given initializer. - /// However, `Parameter1` and `Parameter2` are dependencies registered in the same resolver (i.e. container), - /// whereas `Argument` is not registered in the same container and it is typically variable, - /// therefore, it needs to be handled separately - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - type: Type of the dependency to register. Default value is implicitly inferred from the initializer return type - /// - argument: Type of the variable argument - /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( - type: Dependency.Type = Dependency.self, - argument: Argument.Type, - initializer: @escaping (Argument, Parameter1, Parameter2) -> Dependency - ) { - let factory: FactoryWithArgument = { resolver, argument in - initializer( - argument, - resolver.resolve(type: Parameter1.self), - resolver.resolve(type: Parameter2.self) - ) - } - - register(type: type, factory: factory) - } - - /// Autoregister a dependency with a variable argument and with the provided initializer method that has three parameters; the first and the third are dependencies that are registered within the same container, the second is the variable argument - /// - /// The `Argument`, `Parameter1` and `Parameter2` are parameters of the given initializer. - /// However, `Parameter1` and `Parameter2` are dependencies registered in the same resolver (i.e. container), - /// whereas `Argument` is not registered in the same container and it is typically variable, - /// therefore, it needs to be handled separately - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - type: Type of the dependency to register. Default value is implicitly inferred from the initializer return type - /// - argument: Type of the variable argument - /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( - type: Dependency.Type = Dependency.self, - argument: Argument.Type, - initializer: @escaping (Parameter1, Argument, Parameter2) -> Dependency - ) { - let factory: FactoryWithArgument = { resolver, argument in - initializer( - resolver.resolve(type: Parameter1.self), - argument, - resolver.resolve(type: Parameter2.self) - ) - } - - register(type: type, factory: factory) - } - - /// Autoregister a dependency with a variable argument and with the provided initializer method that has three parameters; the first and the second are dependencies that are registered within the same container, the third is the variable argument - /// - /// The `Argument`, `Parameter1` and `Parameter2` are parameters of the given initializer. - /// However, `Parameter1` and `Parameter2` are dependencies registered in the same resolver (i.e. container), - /// whereas `Argument` is not registered in the same container and it is typically variable, - /// therefore, it needs to be handled separately - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - type: Type of the dependency to register. Default value is implicitly inferred from the initializer return type - /// - argument: Type of the variable argument - /// - initializer: Initializer method of the `Dependency` that should be used to instantiate the dependency when it is being resolved from the container - func autoregister( - type: Dependency.Type = Dependency.self, - argument: Argument.Type, - initializer: @escaping (Parameter1, Parameter2, Argument) -> Dependency - ) { - let factory: FactoryWithArgument = { resolver, argument in - initializer( - resolver.resolve(type: Parameter1.self), - resolver.resolve(type: Parameter2.self), - argument - ) - } - - register(type: type, factory: factory) - } -} diff --git a/Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift deleted file mode 100644 index 1674f59..0000000 --- a/Sources/Protocols/Registration/Sync/DependencyWithArgumentRegistering.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// DependencyWithArgumentRegistering.swift -// -// -// Created by Jan Schwarz on 26.03.2021. -// - -import Foundation - -/// A type that is able to register a dependency that needs a variable argument in order to be resolved later -public protocol DependencyWithArgumentRegistering: DependencyRegistering { - /// Factory closure that instantiates the required dependency with the given variable argument - typealias FactoryWithArgument = (DependencyWithArgumentResolving, Argument) -> Dependency - - /// Register a dependency with a variable argument - /// - /// The argument is typically a parameter in an initiliazer of the dependency that is not registered in the same resolver (i.e. container), - /// therefore, it needs to be passed in `resolve` call - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - type: Type of the dependency to register - /// - factory: Closure that is called when the dependency is being resolved - func register(type: Dependency.Type, factory: @escaping FactoryWithArgument) -} - -// MARK: Overloaded factory methods -public extension DependencyWithArgumentRegistering { - /// Register a dependency with a variable argument. The type of the dependency is determined implicitly based on the factory closure return type - /// - /// The argument is typically a parameter in an initializer of the dependency that is not registered in the same resolver (i.e. container), - /// therefore, it needs to be passed in `resolve` call - /// - /// DISCUSSION: This registration method doesn't have any scope parameter for a reason. - /// The container should always return a new instance for dependencies with arguments as the behaviour for resolving shared instances with arguments is undefined. - /// Should the argument conform to ``Equatable`` to compare the arguments to tell whether a shared instance with a given argument was already resolved? - /// Shared instances are typically not dependent on variable input parameters by definition. - /// If you need to support this usecase, please, keep references to the variable singletons outside of the container. - /// - /// - Parameters: - /// - factory: Closure that is called when the dependency is being resolved - func register(factory: @escaping FactoryWithArgument) { - register(type: Dependency.self, factory: factory) - } -} diff --git a/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift b/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift index 46de4f7..a10a102 100644 --- a/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift +++ b/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift @@ -15,16 +15,18 @@ public protocol AsyncDependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - func tryResolve(type: T.Type) async throws -> T + func tryResolve(type: Dependency.Type) async throws -> Dependency - /// Resolve a dependency with a variable argument that was previously registered within the container + /// Resolve a dependency with variable arguments that was previously registered within the container /// - /// If the container doesn't contain any registration for a dependency with the given type or if an argument of a different type than expected is passed, ``ResolutionError`` is thrown + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// If the container doesn't contain any registration for a dependency with the given type + /// or if arguments of different types than expected are passed, ``ResolutionError`` is thrown /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument: Argument that will be passed as an input parameter to the factory method - func tryResolve(type: T.Type, argument: Argument) async throws -> T + /// - arguments: Arguments that will be passed as input parameters to the factory method (1-3 arguments supported) + func tryResolve(type: Dependency.Type, arguments: repeat each Argument) async throws -> Dependency } public extension AsyncDependencyResolving { @@ -34,36 +36,40 @@ public extension AsyncDependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - func resolve(type: T.Type) async -> T { + func resolve(type: Dependency.Type) async -> Dependency { try! await tryResolve(type: type) } /// Resolve a dependency that was previously registered within the container. A type of the required dependency is inferred from the return type /// /// If the container doesn't contain any registration for a dependency with the given type, a runtime error occurs - /// - func resolve() async -> T { - await resolve(type: T.self) + func resolve() async -> Dependency { + await resolve(type: Dependency.self) } - /// Resolve a dependency with a variable argument that was previously registered within the container + /// Resolve a dependency with variable arguments that was previously registered within the container /// - /// If the container doesn't contain any registration for a dependency with the given type or if an argument of a different type than expected is passed, a runtime error occurs + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// If the container doesn't contain any registration for a dependency with the given type + /// or if arguments of different types than expected are passed, a runtime error occurs /// /// - Parameters: /// - type: Type of the dependency that should be resolved - /// - argument: Argument that will be passed as an input parameter to the factory method - func resolve(type: T.Type, argument: Argument) async -> T { - try! await tryResolve(type: type, argument: argument) + /// - arguments: Arguments that will be passed as input parameters to the factory method (1-3 arguments supported) + func resolve(type: Dependency.Type, arguments: repeat each Argument) async -> Dependency { + try! await tryResolve(type: type, arguments: repeat each arguments) } - /// Resolve a dependency with a variable argument that was previously registered within the container. The type of the required dependency is inferred from the return type + /// Resolve a dependency with variable arguments that was previously registered within the container. + /// The type of the required dependency is inferred from the return type. /// - /// If the container doesn't contain any registration for a dependency with the given type or if an argument of a different type than expected is passed, a runtime error occurs + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// If the container doesn't contain any registration for a dependency with the given type + /// or if arguments of different types than expected are passed, a runtime error occurs /// /// - Parameters: - /// - argument: Argument that will be passed as an input parameter to the factory method - func resolve(argument: Argument) async -> T { - await resolve(type: T.self, argument: argument) + /// - arguments: Arguments that will be passed as input parameters to the factory method (1-3 arguments supported) + func resolve(arguments: repeat each Argument) async -> Dependency { + await resolve(type: Dependency.self, arguments: repeat each arguments) } } diff --git a/Sources/Protocols/Resolution/Sync/DependencyResolving.swift b/Sources/Protocols/Resolution/Sync/DependencyResolving.swift index 7d2e47e..a3af0a5 100644 --- a/Sources/Protocols/Resolution/Sync/DependencyResolving.swift +++ b/Sources/Protocols/Resolution/Sync/DependencyResolving.swift @@ -15,7 +15,18 @@ public protocol DependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - func tryResolve(type: T.Type) throws -> T + func tryResolve(type: Dependency.Type) throws -> Dependency + + /// Resolve a dependency with variable arguments that was previously registered within the container + /// + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// If the container doesn't contain any registration for a dependency with the given type + /// or if arguments of different types than expected are passed, ``ResolutionError`` is thrown + /// + /// - Parameters: + /// - type: Type of the dependency that should be resolved + /// - arguments: Arguments that will be passed as input parameters to the factory method (1-3 arguments supported) + func tryResolve(type: Dependency.Type, arguments: repeat each Argument) throws -> Dependency } public extension DependencyResolving { @@ -25,17 +36,40 @@ public extension DependencyResolving { /// /// - Parameters: /// - type: Type of the dependency that should be resolved - func resolve(type: T.Type) -> T { + func resolve(type: Dependency.Type) -> Dependency { try! tryResolve(type: type) } /// Resolve a dependency that was previously registered within the container. A type of the required dependency is inferred from the return type /// /// If the container doesn't contain any registration for a dependency with the given type, a runtime error occurs + func resolve() -> Dependency { + resolve(type: Dependency.self) + } + + /// Resolve a dependency with variable arguments that was previously registered within the container + /// + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// If the container doesn't contain any registration for a dependency with the given type + /// or if arguments of different types than expected are passed, a runtime error occurs /// /// - Parameters: /// - type: Type of the dependency that should be resolved - func resolve() -> T { - resolve(type: T.self) + /// - arguments: Arguments that will be passed as input parameters to the factory method (1-3 arguments supported) + func resolve(type: Dependency.Type, arguments: repeat each Argument) -> Dependency { + try! tryResolve(type: type, arguments: repeat each arguments) + } + + /// Resolve a dependency with variable arguments that was previously registered within the container. + /// The type of the required dependency is inferred from the return type. + /// + /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. + /// If the container doesn't contain any registration for a dependency with the given type + /// or if arguments of different types than expected are passed, a runtime error occurs + /// + /// - Parameters: + /// - arguments: Arguments that will be passed as input parameters to the factory method (1-3 arguments supported) + func resolve(arguments: repeat each Argument) -> Dependency { + resolve(type: Dependency.self, arguments: repeat each arguments) } } diff --git a/Sources/Protocols/Resolution/Sync/DependencyWithArgumentResolving.swift b/Sources/Protocols/Resolution/Sync/DependencyWithArgumentResolving.swift deleted file mode 100644 index c688a19..0000000 --- a/Sources/Protocols/Resolution/Sync/DependencyWithArgumentResolving.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// DependencyWithArgumentResolving.swift -// -// -// Created by Jan Schwarz on 26.03.2021. -// - -import Foundation - -/// A type that is able to resolve a dependency with a given variable argument -public protocol DependencyWithArgumentResolving: DependencyResolving { - /// Resolve a dependency with a variable argument that was previously registered within the container - /// - /// If the container doesn't contain any registration for a dependency with the given type or if an argument of a different type than expected is passed, ``ResolutionError`` is thrown - /// - /// - Parameters: - /// - type: Type of the dependency that should be resolved - /// - argument: Argument that will be passed as an input parameter to the factory method - func tryResolve(type: T.Type, argument: Argument) throws -> T -} - -public extension DependencyWithArgumentResolving { - /// Resolve a dependency with a variable argument that was previously registered within the container - /// - /// If the container doesn't contain any registration for a dependency with the given type or if an argument of a different type than expected is passed, a runtime error occurs - /// - /// - Parameters: - /// - type: Type of the dependency that should be resolved - /// - argument: Argument that will be passed as an input parameter to the factory method - func resolve(type: T.Type, argument: Argument) -> T { - try! tryResolve(type: type, argument: argument) - } - - /// Resolve a dependency with a variable argument that was previously registered within the container. The type of the required dependency is inferred from the return type - /// - /// If the container doesn't contain any registration for a dependency with the given type or if an argument of a different type than expected is passed, a runtime error occurs - /// - /// - Parameters: - /// - type: Type of the dependency that should be resolved - /// - argument: Argument that will be passed as an input parameter to the factory method - func resolve(argument: Argument) -> T { - resolve(type: T.self, argument: argument) - } -} diff --git a/Tests/Common/Dependencies.swift b/Tests/Common/Dependencies.swift index 6bd0456..4260ca4 100644 --- a/Tests/Common/Dependencies.swift +++ b/Tests/Common/Dependencies.swift @@ -107,3 +107,47 @@ final class DependencyWithAsyncInitWithParameter: Sendable { self.subDependency = subDependency } } + +final class DependencyWithTwoArguments: Sendable { + let argument1: StructureDependency + let argument2: String + + init(argument1: StructureDependency, argument2: String) { + self.argument1 = argument1 + self.argument2 = argument2 + } +} + +final class DependencyWithThreeArguments: Sendable { + let argument1: StructureDependency + let argument2: String + let argument3: Int + + init(argument1: StructureDependency, argument2: String, argument3: Int) { + self.argument1 = argument1 + self.argument2 = argument2 + self.argument3 = argument3 + } +} + +final class DependencyWithAsyncInitWithTwoArguments: Sendable { + let argument1: StructureDependency + let argument2: String + + init(argument1: StructureDependency, argument2: String) async { + self.argument1 = argument1 + self.argument2 = argument2 + } +} + +final class DependencyWithAsyncInitWithThreeArguments: Sendable { + let argument1: StructureDependency + let argument2: String + let argument3: Int + + init(argument1: StructureDependency, argument2: String, argument3: Int) async { + self.argument1 = argument1 + self.argument2 = argument2 + self.argument3 = argument3 + } +} diff --git a/Tests/Container/Async/AsyncArgumentTests.swift b/Tests/Container/Async/AsyncArgumentTests.swift index 9e916d0..5166d34 100644 --- a/Tests/Container/Async/AsyncArgumentTests.swift +++ b/Tests/Container/Async/AsyncArgumentTests.swift @@ -15,7 +15,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { } let argument = StructureDependency(property1: "48") - let resolvedDependency: DependencyWithValueTypeParameter = await container.resolve(argument: argument) + let resolvedDependency: DependencyWithValueTypeParameter = await container.resolve(arguments: argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } @@ -26,12 +26,38 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { } let argument = StructureDependency(property1: "48") - let resolvedDependency: DependencyWithValueTypeParameter = await container.resolve(argument: argument) + let resolvedDependency: DependencyWithValueTypeParameter = await container.resolve(arguments: argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } - func testUnmatchingArgumentType() async { + func testUnmatchingArgumentType_ZeroArguments() async { + await container.register(in: .shared) { _ -> SimpleDependency in + SimpleDependency() + } + + let argument = 48 + + do { + _ = try await container.tryResolve(type: SimpleDependency.self, arguments: argument) + + XCTFail("Expected to throw error") + } catch { + guard let resolutionError = error as? ResolutionError else { + XCTFail("Incorrect error type") + return + } + + switch resolutionError { + case .unmatchingArgumentType: + XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") + default: + XCTFail("Incorrect resolution error: \(resolutionError)") + } + } + } + + func testUnmatchingArgumentType_OneArgument() async { await container.register { _, argument -> DependencyWithValueTypeParameter in DependencyWithValueTypeParameter(subDependency: argument) } @@ -39,7 +65,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { let argument = 48 do { - _ = try await container.tryResolve(type: DependencyWithValueTypeParameter.self, argument: argument) + _ = try await container.tryResolve(type: DependencyWithValueTypeParameter.self, arguments: argument) XCTFail("Expected to throw error") } catch { @@ -52,7 +78,7 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { case .unmatchingArgumentType: XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") default: - XCTFail("Incorrect resolution error") + XCTFail("Incorrect resolution error: \(resolutionError)") } } } @@ -63,8 +89,147 @@ final class AsyncContainerArgumentTests: AsyncDITestCase { } let argument = StructureDependency(property1: "48") - let resolvedDependency: DependencyWithAsyncInitWithParameter = await container.resolve(argument: argument) + let resolvedDependency: DependencyWithAsyncInitWithParameter = await container.resolve(arguments: argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } + + func testRegistrationWithTwoArguments() async { + await container.register { _, argument1, argument2 -> DependencyWithTwoArguments in + DependencyWithTwoArguments(argument1: argument1, argument2: argument2) + } + + let argument1 = StructureDependency(property1: "test1") + let argument2 = "test2" + let resolvedDependency: DependencyWithTwoArguments = await container.resolve(arguments: argument1, argument2) + + XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") + XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") + } + + func testRegistrationWithTwoArgumentsWithExplicitType() async { + await container.register(type: DependencyWithTwoArguments.self) { _, argument1, argument2 in + DependencyWithTwoArguments(argument1: argument1, argument2: argument2) + } + + let argument1 = StructureDependency(property1: "test1") + let argument2 = "test2" + let resolvedDependency: DependencyWithTwoArguments = await container.resolve(arguments: argument1, argument2) + + XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") + XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") + } + + func testUnmatchingArgumentType_TwoArguments() async { + await container.register { _, argument1, argument2 -> DependencyWithTwoArguments in + DependencyWithTwoArguments(argument1: argument1, argument2: argument2) + } + + let argument1 = 48 + let argument2 = "test" + + do { + _ = try await container.tryResolve(type: DependencyWithTwoArguments.self, arguments: argument1, argument2) + + XCTFail("Expected to throw error") + } catch { + guard let resolutionError = error as? ResolutionError else { + XCTFail("Incorrect error type") + return + } + + switch resolutionError { + case .unmatchingArgumentType: + XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") + default: + XCTFail("Incorrect resolution error: \(resolutionError)") + } + } + } + + func testRegistrationWithThreeArguments() async { + await container.register { _, argument1, argument2, argument3 -> DependencyWithThreeArguments in + DependencyWithThreeArguments(argument1: argument1, argument2: argument2, argument3: argument3) + } + + let argument1 = StructureDependency(property1: "test1") + let argument2 = "test2" + let argument3 = 42 + let resolvedDependency: DependencyWithThreeArguments = await container.resolve(arguments: argument1, argument2, argument3) + + XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") + XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") + XCTAssertEqual(argument3, resolvedDependency.argument3, "Container returned dependency with different third argument") + } + + func testRegistrationWithThreeArgumentsWithExplicitType() async { + await container.register(type: DependencyWithThreeArguments.self) { _, argument1, argument2, argument3 in + DependencyWithThreeArguments(argument1: argument1, argument2: argument2, argument3: argument3) + } + + let argument1 = StructureDependency(property1: "test1") + let argument2 = "test2" + let argument3 = 42 + let resolvedDependency: DependencyWithThreeArguments = await container.resolve(arguments: argument1, argument2, argument3) + + XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") + XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") + XCTAssertEqual(argument3, resolvedDependency.argument3, "Container returned dependency with different third argument") + } + + func testUnmatchingArgumentType_ThreeArguments() async { + await container.register { _, argument1, argument2, argument3 -> DependencyWithThreeArguments in + DependencyWithThreeArguments(argument1: argument1, argument2: argument2, argument3: argument3) + } + + let argument1 = 48 + let argument2 = "test" + let argument3 = 42 + + do { + _ = try await container.tryResolve(type: DependencyWithThreeArguments.self, arguments: argument1, argument2, argument3) + + XCTFail("Expected to throw error") + } catch { + guard let resolutionError = error as? ResolutionError else { + XCTFail("Incorrect error type") + return + } + + switch resolutionError { + case .unmatchingArgumentType: + XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") + default: + XCTFail("Incorrect resolution error: \(resolutionError)") + } + } + } + + func testRegistrationWithAsyncInitWithTwoArguments() async { + await container.register { _, argument1, argument2 -> DependencyWithAsyncInitWithTwoArguments in + await DependencyWithAsyncInitWithTwoArguments(argument1: argument1, argument2: argument2) + } + + let argument1 = StructureDependency(property1: "test1") + let argument2 = "test2" + let resolvedDependency: DependencyWithAsyncInitWithTwoArguments = await container.resolve(arguments: argument1, argument2) + + XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") + XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") + } + + func testRegistrationWithAsyncInitWithThreeArguments() async { + await container.register { _, argument1, argument2, argument3 -> DependencyWithAsyncInitWithThreeArguments in + await DependencyWithAsyncInitWithThreeArguments(argument1: argument1, argument2: argument2, argument3: argument3) + } + + let argument1 = StructureDependency(property1: "test1") + let argument2 = "test2" + let argument3 = 42 + let resolvedDependency: DependencyWithAsyncInitWithThreeArguments = await container.resolve(arguments: argument1, argument2, argument3) + + XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") + XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") + XCTAssertEqual(argument3, resolvedDependency.argument3, "Container returned dependency with different third argument") + } } diff --git a/Tests/Container/Async/AsyncBaseTests.swift b/Tests/Container/Async/AsyncBaseTests.swift index 1638d41..bc17f4b 100644 --- a/Tests/Container/Async/AsyncBaseTests.swift +++ b/Tests/Container/Async/AsyncBaseTests.swift @@ -10,7 +10,7 @@ import XCTest final class AsyncBaseTests: AsyncDITestCase { func testDependencyRegisteredInDefaultScope() async { - await container.register { _ -> SimpleDependency in + await container.register(in: .shared) { _ -> SimpleDependency in SimpleDependency() } @@ -21,7 +21,7 @@ final class AsyncBaseTests: AsyncDITestCase { } func testDependencyRegisteredInDefaultScopeWithExplicitType() async { - await container.register(type: SimpleDependency.self) { _ -> SimpleDependency in + await container.register(type: SimpleDependency.self, in: .shared) { _ -> SimpleDependency in SimpleDependency() } diff --git a/Tests/Container/Async/AsyncComplexTests.swift b/Tests/Container/Async/AsyncComplexTests.swift index 4ddd0aa..be192fe 100644 --- a/Tests/Container/Async/AsyncComplexTests.swift +++ b/Tests/Container/Async/AsyncComplexTests.swift @@ -10,7 +10,7 @@ import XCTest final class AsyncComplexTests: AsyncDITestCase { func testCleanContainer() async { - await container.register { _ in + await container.register(in: .shared) { _ in SimpleDependency() } @@ -111,8 +111,8 @@ final class AsyncComplexTests: AsyncDITestCase { let resolvedDependency1: DependencyWithParameter = await container.resolve() let resolvedDependency2: DependencyWithParameter = await container.resolve() - let resolvedDependency3 = await container.resolve(type: DependencyWithParameter3.self, argument: argumentDependency1) - let resolvedDependency4 = await container.resolve(type: DependencyWithParameter3.self, argument: argumentDependency2) + let resolvedDependency3 = await container.resolve(type: DependencyWithParameter3.self, arguments: argumentDependency1) + let resolvedDependency4 = await container.resolve(type: DependencyWithParameter3.self, arguments: argumentDependency2) XCTAssertTrue(resolvedDependency1 === resolvedDependency2, "Resolved different instances") XCTAssertTrue(resolvedDependency1.subDependency === resolvedDependency2.subDependency, "Resolved different instances") @@ -155,8 +155,8 @@ final class AsyncComplexTests: AsyncDITestCase { let resolvedDependency1: DependencyWithParameter = await container.resolve() let resolvedDependency2: DependencyWithParameter = await container.resolve() - let resolvedDependency3 = await container.resolve(type: DependencyWithParameter3.self, argument: argumentDependency1) - let resolvedDependency4 = await container.resolve(type: DependencyWithParameter3.self, argument: argumentDependency2) + let resolvedDependency3 = await container.resolve(type: DependencyWithParameter3.self, arguments: argumentDependency1) + let resolvedDependency4 = await container.resolve(type: DependencyWithParameter3.self, arguments: argumentDependency2) XCTAssertTrue(resolvedDependency1 === resolvedDependency2, "Resolved different instances") XCTAssertTrue(resolvedDependency1.subDependency === resolvedDependency2.subDependency, "Resolved different instances") diff --git a/Tests/Container/Sync/ArgumentTests.swift b/Tests/Container/Sync/ArgumentTests.swift index 29ebca6..7f7d0d0 100644 --- a/Tests/Container/Sync/ArgumentTests.swift +++ b/Tests/Container/Sync/ArgumentTests.swift @@ -15,7 +15,7 @@ final class ContainerArgumentTests: DITestCase { } let argument = StructureDependency(property1: "48") - let resolvedDependency: DependencyWithValueTypeParameter = container.resolve(argument: argument) + let resolvedDependency: DependencyWithValueTypeParameter = container.resolve(arguments: argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } @@ -26,12 +26,37 @@ final class ContainerArgumentTests: DITestCase { } let argument = StructureDependency(property1: "48") - let resolvedDependency: DependencyWithValueTypeParameter = container.resolve(argument: argument) + let resolvedDependency: DependencyWithValueTypeParameter = container.resolve(arguments: argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } - func testUnmatchingArgumentType() { + func testUnmatchingArgumentType_ZeroArguments() { + container.register(in: .shared) { _ -> SimpleDependency in + SimpleDependency() + } + + let argument = 48 + + XCTAssertThrowsError( + try container.tryResolve(type: SimpleDependency.self, arguments: argument), + "Resolver didn't throw an error" + ) { error in + guard let resolutionError = error as? ResolutionError else { + XCTFail("Incorrect error type") + return + } + + switch resolutionError { + case .unmatchingArgumentType: + XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") + default: + XCTFail("Incorrect resolution error: \(resolutionError)") + } + } + } + + func testUnmatchingArgumentType_OneArgument() { container.register { _, argument -> DependencyWithValueTypeParameter in DependencyWithValueTypeParameter(subDependency: argument) } @@ -39,7 +64,116 @@ final class ContainerArgumentTests: DITestCase { let argument = 48 XCTAssertThrowsError( - try container.tryResolve(type: DependencyWithValueTypeParameter.self, argument: argument), + try container.tryResolve(type: DependencyWithValueTypeParameter.self, arguments: argument), + "Resolver didn't throw an error" + ) { error in + guard let resolutionError = error as? ResolutionError else { + XCTFail("Incorrect error type") + return + } + + switch resolutionError { + case .unmatchingArgumentType: + XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") + default: + XCTFail("Incorrect resolution error: \(resolutionError)") + } + } + } + + func testRegistrationWithTwoArguments() { + container.register { _, argument1, argument2 -> DependencyWithTwoArguments in + DependencyWithTwoArguments(argument1: argument1, argument2: argument2) + } + + let argument1 = StructureDependency(property1: "test1") + let argument2 = "test2" + let resolvedDependency: DependencyWithTwoArguments = container.resolve(arguments: argument1, argument2) + + XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") + XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") + } + + func testRegistrationWithTwoArgumentsWithExplicitType() { + container.register(type: DependencyWithTwoArguments.self) { _, argument1, argument2 in + DependencyWithTwoArguments(argument1: argument1, argument2: argument2) + } + + let argument1 = StructureDependency(property1: "test1") + let argument2 = "test2" + let resolvedDependency: DependencyWithTwoArguments = container.resolve(arguments: argument1, argument2) + + XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") + XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") + } + + func testUnmatchingArgumentType_TwoArguments() { + container.register { _, argument1, argument2 -> DependencyWithTwoArguments in + DependencyWithTwoArguments(argument1: argument1, argument2: argument2) + } + + let argument1 = 48 + let argument2 = "test" + + XCTAssertThrowsError( + try container.tryResolve(type: DependencyWithTwoArguments.self, arguments: argument1, argument2), + "Resolver didn't throw an error" + ) { error in + guard let resolutionError = error as? ResolutionError else { + XCTFail("Incorrect error type") + return + } + + switch resolutionError { + case .unmatchingArgumentType: + XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") + default: + XCTFail("Incorrect resolution error: \(resolutionError)") + } + } + } + + func testRegistrationWithThreeArguments() { + container.register { _, argument1, argument2, argument3 -> DependencyWithThreeArguments in + DependencyWithThreeArguments(argument1: argument1, argument2: argument2, argument3: argument3) + } + + let argument1 = StructureDependency(property1: "test1") + let argument2 = "test2" + let argument3 = 42 + let resolvedDependency: DependencyWithThreeArguments = container.resolve(arguments: argument1, argument2, argument3) + + XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") + XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") + XCTAssertEqual(argument3, resolvedDependency.argument3, "Container returned dependency with different third argument") + } + + func testRegistrationWithThreeArgumentsWithExplicitType() { + container.register(type: DependencyWithThreeArguments.self) { _, argument1, argument2, argument3 in + DependencyWithThreeArguments(argument1: argument1, argument2: argument2, argument3: argument3) + } + + let argument1 = StructureDependency(property1: "test1") + let argument2 = "test2" + let argument3 = 42 + let resolvedDependency: DependencyWithThreeArguments = container.resolve(arguments: argument1, argument2, argument3) + + XCTAssertEqual(argument1, resolvedDependency.argument1, "Container returned dependency with different first argument") + XCTAssertEqual(argument2, resolvedDependency.argument2, "Container returned dependency with different second argument") + XCTAssertEqual(argument3, resolvedDependency.argument3, "Container returned dependency with different third argument") + } + + func testUnmatchingArgumentType_ThreeArguments() { + container.register { _, argument1, argument2, argument3 -> DependencyWithThreeArguments in + DependencyWithThreeArguments(argument1: argument1, argument2: argument2, argument3: argument3) + } + + let argument1 = 48 + let argument2 = "test" + let argument3 = 42 + + XCTAssertThrowsError( + try container.tryResolve(type: DependencyWithThreeArguments.self, arguments: argument1, argument2, argument3), "Resolver didn't throw an error" ) { error in guard let resolutionError = error as? ResolutionError else { @@ -51,7 +185,7 @@ final class ContainerArgumentTests: DITestCase { case .unmatchingArgumentType: XCTAssertNotEqual(resolutionError.localizedDescription, "", "Error description is empty") default: - XCTFail("Incorrect resolution error") + XCTFail("Incorrect resolution error: \(resolutionError)") } } } diff --git a/Tests/Container/Sync/AutoregistrationWithArgumentTest.swift b/Tests/Container/Sync/AutoregistrationWithArgumentTest.swift index 728a887..b5804bf 100644 --- a/Tests/Container/Sync/AutoregistrationWithArgumentTest.swift +++ b/Tests/Container/Sync/AutoregistrationWithArgumentTest.swift @@ -13,7 +13,7 @@ final class AutoregistrationWithArgumentTest: DITestCase { container.autoregister(argument: StructureDependency.self, initializer: DependencyWithValueTypeParameter.init) let argument = StructureDependency(property1: "48") - let resolvedDependency: DependencyWithValueTypeParameter = container.resolve(argument: argument) + let resolvedDependency: DependencyWithValueTypeParameter = container.resolve(arguments: argument) XCTAssertEqual(argument, resolvedDependency.subDependency, "Container returned dependency with different argument") } @@ -25,8 +25,8 @@ final class AutoregistrationWithArgumentTest: DITestCase { let argument = SimpleDependency() - let firstResolved: DependencyWithParameter2 = container.resolve(argument: argument) - let secondResolved: DependencyWithParameter2 = container.resolve(argument: argument) + let firstResolved: DependencyWithParameter2 = container.resolve(arguments: argument) + let secondResolved: DependencyWithParameter2 = container.resolve(arguments: argument) XCTAssertTrue(argument === firstResolved.subDependency1, "Container returned dependency with different argument") XCTAssertTrue(argument === secondResolved.subDependency1, "Container returned dependency with different argument") @@ -40,8 +40,8 @@ final class AutoregistrationWithArgumentTest: DITestCase { let argument = DependencyWithValueTypeParameter() - let firstResolved: DependencyWithParameter2 = container.resolve(argument: argument) - let secondResolved: DependencyWithParameter2 = container.resolve(argument: argument) + let firstResolved: DependencyWithParameter2 = container.resolve(arguments: argument) + let secondResolved: DependencyWithParameter2 = container.resolve(arguments: argument) XCTAssertTrue(argument === firstResolved.subDependency2, "Container returned dependency with different argument") XCTAssertTrue(argument === secondResolved.subDependency2, "Container returned dependency with different argument") @@ -58,8 +58,8 @@ final class AutoregistrationWithArgumentTest: DITestCase { let argument = SimpleDependency() - let firstResolved: DependencyWithParameter3 = container.resolve(argument: argument) - let secondResolved: DependencyWithParameter3 = container.resolve(argument: argument) + let firstResolved: DependencyWithParameter3 = container.resolve(arguments: argument) + let secondResolved: DependencyWithParameter3 = container.resolve(arguments: argument) XCTAssertTrue(argument === firstResolved.subDependency1, "Container returned dependency with different argument") XCTAssertTrue(argument === secondResolved.subDependency1, "Container returned dependency with different argument") @@ -75,8 +75,8 @@ final class AutoregistrationWithArgumentTest: DITestCase { let argument = DependencyWithValueTypeParameter() - let firstResolved: DependencyWithParameter3 = container.resolve(argument: argument) - let secondResolved: DependencyWithParameter3 = container.resolve(argument: argument) + let firstResolved: DependencyWithParameter3 = container.resolve(arguments: argument) + let secondResolved: DependencyWithParameter3 = container.resolve(arguments: argument) XCTAssertTrue(argument === firstResolved.subDependency2, "Container returned dependency with different argument") XCTAssertTrue(argument === secondResolved.subDependency2, "Container returned dependency with different argument") @@ -93,8 +93,8 @@ final class AutoregistrationWithArgumentTest: DITestCase { let argument = DependencyWithParameter(subDependency: SimpleDependency()) - let firstResolved: DependencyWithParameter3 = container.resolve(argument: argument) - let secondResolved: DependencyWithParameter3 = container.resolve(argument: argument) + let firstResolved: DependencyWithParameter3 = container.resolve(arguments: argument) + let secondResolved: DependencyWithParameter3 = container.resolve(arguments: argument) XCTAssertTrue(argument === firstResolved.subDependency3, "Container returned dependency with different argument") XCTAssertTrue(argument === secondResolved.subDependency3, "Container returned dependency with different argument") diff --git a/Tests/Container/Sync/BaseTests.swift b/Tests/Container/Sync/BaseTests.swift index 27093b4..bb0f9cd 100644 --- a/Tests/Container/Sync/BaseTests.swift +++ b/Tests/Container/Sync/BaseTests.swift @@ -37,7 +37,7 @@ final class BaseTests: DITestCase { } func testDependencyRegisteredInDefaultScope() { - container.register { _ -> SimpleDependency in + container.register(in: .shared) { _ -> SimpleDependency in SimpleDependency() } @@ -48,7 +48,7 @@ final class BaseTests: DITestCase { } func testDependencyRegisteredInDefaultScopeWithExplicitType() { - container.register(type: SimpleDependency.self) { _ -> SimpleDependency in + container.register(type: SimpleDependency.self, in: .shared) { _ -> SimpleDependency in SimpleDependency() } diff --git a/Tests/Container/Sync/ComplexTests.swift b/Tests/Container/Sync/ComplexTests.swift index 333b678..fb78de2 100644 --- a/Tests/Container/Sync/ComplexTests.swift +++ b/Tests/Container/Sync/ComplexTests.swift @@ -97,8 +97,8 @@ final class ComplexTests: DITestCase { let resolvedDependency1: DependencyWithParameter = container.resolve() let resolvedDependency2: DependencyWithParameter = container.resolve() - let resolvedDependency3 = container.resolve(type: DependencyWithParameter3.self, argument: argumentDependency1) - let resolvedDependency4 = container.resolve(type: DependencyWithParameter3.self, argument: argumentDependency2) + let resolvedDependency3 = container.resolve(type: DependencyWithParameter3.self, arguments: argumentDependency1) + let resolvedDependency4 = container.resolve(type: DependencyWithParameter3.self, arguments: argumentDependency2) XCTAssertTrue(resolvedDependency1 === resolvedDependency2, "Resolved different instances") XCTAssertTrue(resolvedDependency1.subDependency === resolvedDependency2.subDependency, "Resolved different instances") @@ -141,8 +141,8 @@ final class ComplexTests: DITestCase { let resolvedDependency1: DependencyWithParameter = container.resolve() let resolvedDependency2: DependencyWithParameter = container.resolve() - let resolvedDependency3 = container.resolve(type: DependencyWithParameter3.self, argument: argumentDependency1) - let resolvedDependency4 = container.resolve(type: DependencyWithParameter3.self, argument: argumentDependency2) + let resolvedDependency3 = container.resolve(type: DependencyWithParameter3.self, arguments: argumentDependency1) + let resolvedDependency4 = container.resolve(type: DependencyWithParameter3.self, arguments: argumentDependency2) XCTAssertTrue(resolvedDependency1 === resolvedDependency2, "Resolved different instances") XCTAssertTrue(resolvedDependency1.subDependency === resolvedDependency2.subDependency, "Resolved different instances") diff --git a/fastlane/Fastfile b/fastlane/Fastfile deleted file mode 100644 index d30bd7f..0000000 --- a/fastlane/Fastfile +++ /dev/null @@ -1,9 +0,0 @@ -default_platform(:ios) - -desc "Run tests" -lane :tests do - spm( - command: "test", - enable_code_coverage: "true" - ) -end diff --git a/fastlane/README.md b/fastlane/README.md deleted file mode 100644 index d7c7819..0000000 --- a/fastlane/README.md +++ /dev/null @@ -1,28 +0,0 @@ -fastlane documentation -================ -# Installation - -Make sure you have the latest version of the Xcode command line tools installed: - -``` -xcode-select --install -``` - -Install _fastlane_ using -``` -[sudo] gem install fastlane -NV -``` -or alternatively using `brew install fastlane` - -# Available Actions -### tests -``` -fastlane tests -``` -Run tests - ----- - -This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. -More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). -The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).